@mojaloop/sdk-scheme-adapter 12.0.2 → 12.2.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.
@@ -0,0 +1,294 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2020 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * Matt Kingston - matt.kingston@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ // This server has deliberately been written separate from any other server in the SDK. There is
12
+ // some reasonable argument that it could be part of the outbound or test server. It has not been
13
+ // incorporated in either as, at the time of writing, it is intended to be maintained in a
14
+ // proprietary fork. Therefore, keeping it independent of other servers will avoid the maintenance
15
+ // burden that would otherwise be associated with incorporating it with those.
16
+ //
17
+ // It inherits from the Server class from the 'ws' websocket library for Node, which in turn
18
+ // inherits from EventEmitter. We exploit this to emit an event when a reconfigure message is sent
19
+ // to this server. Then, when this server's reconfigure method is called, it reconfigures itself
20
+ // and sends a message to all clients notifying them of the new application configuration.
21
+ //
22
+ // It expects new configuration to be supplied as an array of JSON patches. It therefore exposes
23
+ // the current configuration to
24
+
25
+ const assert = require('assert').strict;
26
+
27
+ const ws = require('ws');
28
+ const jsonPatch = require('fast-json-patch');
29
+
30
+ const randomPhrase = require('~/lib/randomphrase');
31
+
32
+ /**************************************************************************
33
+ * The message protocol messages, verbs, and errors
34
+ *************************************************************************/
35
+ const MESSAGE = {
36
+ CONFIGURATION: 'CONFIGURATION',
37
+ ERROR: 'ERROR',
38
+ };
39
+
40
+ const VERB = {
41
+ READ: 'READ',
42
+ NOTIFY: 'NOTIFY',
43
+ PATCH: 'PATCH'
44
+ };
45
+
46
+ const ERROR = {
47
+ UNSUPPORTED_MESSAGE: 'UNSUPPORTED_MESSAGE',
48
+ UNSUPPORTED_VERB: 'UNSUPPORTED_VERB',
49
+ JSON_PARSE_ERROR: 'JSON_PARSE_ERROR',
50
+ };
51
+
52
+ /**************************************************************************
53
+ * Events emitted by the control server
54
+ *************************************************************************/
55
+ const EVENT = {
56
+ RECONFIGURE: 'RECONFIGURE',
57
+ };
58
+
59
+ /**************************************************************************
60
+ * Private convenience functions
61
+ *************************************************************************/
62
+ const serialise = JSON.stringify;
63
+ const deserialise = (msg) => {
64
+ //reviver function
65
+ return JSON.parse(msg.toString(), (k, v) => {
66
+ if (
67
+ v !== null &&
68
+ typeof v === 'object' &&
69
+ 'type' in v &&
70
+ v.type === 'Buffer' &&
71
+ 'data' in v &&
72
+ Array.isArray(v.data)) {
73
+ return new Buffer(v.data);
74
+ }
75
+ return v;
76
+ });
77
+ };
78
+
79
+ const buildMsg = (verb, msg, data, id = randomPhrase()) => serialise({
80
+ verb,
81
+ msg,
82
+ data,
83
+ id,
84
+ });
85
+
86
+ const buildPatchConfiguration = (oldConf, newConf, id) => {
87
+ const patches = jsonPatch.compare(oldConf, newConf);
88
+ return buildMsg(VERB.PATCH, MESSAGE.CONFIGURATION, patches, id);
89
+ };
90
+
91
+ const getWsIp = (req) => [
92
+ req.socket.remoteAddress,
93
+ ...(
94
+ req.headers['x-forwarded-for']
95
+ ? req.headers['x-forwarded-for'].split(/\s*,\s*/)
96
+ : []
97
+ )
98
+ ];
99
+
100
+ /**************************************************************************
101
+ * build
102
+ *
103
+ * Public object exposing an API to build valid protocol messages.
104
+ * It is not the only way to build valid messages within the protocol.
105
+ *************************************************************************/
106
+ const build = {
107
+ CONFIGURATION: {
108
+ PATCH: buildPatchConfiguration,
109
+ READ: (id) => buildMsg(VERB.READ, MESSAGE.CONFIGURATION, {}, id),
110
+ NOTIFY: (config, id) => buildMsg(VERB.NOTIFY, MESSAGE.CONFIGURATION, config, id),
111
+ },
112
+ ERROR: {
113
+ NOTIFY: {
114
+ UNSUPPORTED_MESSAGE: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_MESSAGE, id),
115
+ UNSUPPORTED_VERB: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_VERB, id),
116
+ JSON_PARSE_ERROR: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.JSON_PARSE_ERROR, id),
117
+ }
118
+ },
119
+ };
120
+
121
+ /**************************************************************************
122
+ * Client
123
+ *
124
+ * The Control Client. Client for the websocket control API.
125
+ * Used to hot-restart the SDK.
126
+ *
127
+ * logger - Logger- see SDK logger used elsewhere
128
+ * address - address of control server
129
+ * port - port of control server
130
+ *************************************************************************/
131
+ class Client extends ws {
132
+ constructor({ address = 'localhost', port, logger }) {
133
+ super(`ws://${address}:${port}`);
134
+ this._logger = logger;
135
+ }
136
+
137
+ // Really only exposed so that a user can import only the client for convenience
138
+ get Build() {
139
+ return build;
140
+ }
141
+
142
+ static async Create(...args) {
143
+ const result = new Client(...args);
144
+ await new Promise((resolve, reject) => {
145
+ result.on('open', resolve);
146
+ result.on('error', reject);
147
+ });
148
+ return result;
149
+ }
150
+
151
+ async send(msg) {
152
+ const data = typeof msg === 'string' ? msg : serialise(msg);
153
+ this._logger.push({ data }).log('Sending message');
154
+ return new Promise((resolve) => super.send.call(this, data, resolve));
155
+ }
156
+
157
+ // Receive a single message
158
+ async receive() {
159
+ return new Promise((resolve) => this.once('message', (data) => {
160
+ const msg = deserialise(data);
161
+ this._logger.push({ msg }).log('Received');
162
+ resolve(msg);
163
+ }));
164
+ }
165
+ }
166
+
167
+ /**************************************************************************
168
+ * Server
169
+ *
170
+ * The Control Server. Exposes a websocket control API.
171
+ * Used to hot-restart the SDK.
172
+ *
173
+ * logger - Logger- see SDK logger used elsewhere
174
+ * port - HTTP port to host on
175
+ * appConfig - The configuration for the entire application- supplied here as this class uses it to
176
+ * validate reconfiguration requests- it is not used for configuration here, however
177
+ * server - optional HTTP/S server on which to serve the websocket
178
+ *************************************************************************/
179
+ class Server extends ws.Server {
180
+ constructor({ logger, port = 0, appConfig = {} }) {
181
+ super({ clientTracking: true, port });
182
+
183
+ this._logger = logger;
184
+ this._port = port;
185
+ this._appConfig = appConfig;
186
+ this._clientData = new Map();
187
+
188
+ this.on('error', err => {
189
+ this._logger.push({ err })
190
+ .log('Unhandled websocket error occurred. Shutting down.');
191
+ process.exit(1);
192
+ });
193
+
194
+ this.on('connection', (socket, req) => {
195
+ const logger = this._logger.push({
196
+ url: req.url,
197
+ ip: getWsIp(req),
198
+ remoteAddress: req.socket.remoteAddress,
199
+ });
200
+ logger.log('Websocket connection received');
201
+ this._clientData.set(socket, { ip: req.connection.remoteAddress, logger });
202
+
203
+ socket.on('close', (code, reason) => {
204
+ logger.push({ code, reason }).log('Websocket connection closed');
205
+ this._clientData.delete(socket);
206
+ });
207
+
208
+ socket.on('message', this._handle(socket, logger));
209
+ });
210
+
211
+ this._logger.push(this.address()).log('running on');
212
+ }
213
+
214
+ // Close the server then wait for all the client sockets to close
215
+ async stop() {
216
+ await new Promise(this.close.bind(this));
217
+ this._logger.log('Control server shutdown complete');
218
+ }
219
+
220
+ reconfigure({ logger = this._logger, port = 0, appConfig = this._appConfig }) {
221
+ assert(port === this._port, 'Cannot reconfigure running port');
222
+ return () => {
223
+ const reconfigureClientLogger =
224
+ ({ logger: clientLogger }) => clientLogger.configure(logger);
225
+ this._clientData.values(reconfigureClientLogger);
226
+ this._logger = logger;
227
+ this._appConfig = appConfig;
228
+ this._logger.log('restarted');
229
+ };
230
+ }
231
+
232
+ async notifyClientsOfCurrentConfig() {
233
+ const updateConfMsg = build.CONFIGURATION.NOTIFY(this._appConfig);
234
+ const logError = (socket, message) => (err) =>
235
+ this._logger
236
+ .push({ message, ip: this._clientData.get(socket).ip, err })
237
+ .log('Error sending reconfigure notification to client');
238
+ const sendToAllClients = (msg) => Promise.all(
239
+ [...this.clients.values()].map((socket) =>
240
+ (new Promise((resolve) => socket.send(msg, resolve))).catch(logError(socket, msg))
241
+ )
242
+ );
243
+ return await sendToAllClients(updateConfMsg);
244
+ }
245
+
246
+ _handle(client, logger) {
247
+ return (data) => {
248
+ // TODO: json-schema validation of received message- should be pretty straight-forward
249
+ // and will allow better documentation of the API
250
+ let msg;
251
+ try {
252
+ msg = deserialise(data);
253
+ } catch (err) {
254
+ logger.push({ data }).log('Couldn\'t parse received message');
255
+ client.send(build.ERROR.NOTIFY.JSON_PARSE_ERROR());
256
+ }
257
+ logger.push({ msg }).log('Handling received message');
258
+ switch (msg.msg) {
259
+ case MESSAGE.CONFIGURATION:
260
+ switch (msg.verb) {
261
+ case VERB.READ:
262
+ client.send(build.CONFIGURATION.NOTIFY(this._appConfig, msg.id));
263
+ break;
264
+ case VERB.PATCH: {
265
+ // TODO: validate the incoming patch? Or assume clients have used the
266
+ // client library?
267
+ const dup = JSON.parse(JSON.stringify(this._appConfig)); // fast-json-patch explicitly mutates
268
+ jsonPatch.applyPatch(dup, msg.data);
269
+ logger.push({ oldConf: this._appConfig, newConf: dup }).log('Emitting new configuration');
270
+ this.emit(EVENT.RECONFIGURE, dup);
271
+ break;
272
+ }
273
+ default:
274
+ client.send(build.ERROR.NOTIFY.UNSUPPORTED_VERB(msg.id));
275
+ break;
276
+ }
277
+ break;
278
+ default:
279
+ client.send(build.ERROR.NOTIFY.UNSUPPORTED_MESSAGE(msg.id));
280
+ break;
281
+ }
282
+ };
283
+ }
284
+ }
285
+
286
+ module.exports = {
287
+ Client,
288
+ Server,
289
+ build,
290
+ MESSAGE,
291
+ VERB,
292
+ ERROR,
293
+ EVENT,
294
+ };
@@ -24,6 +24,7 @@ const Validate = require('../lib/validate');
24
24
  const router = require('../lib/router');
25
25
  const handlers = require('./handlers');
26
26
  const middlewares = require('./middlewares');
27
+ const check = require('../lib/check');
27
28
 
28
29
  class InboundApi extends EventEmitter {
29
30
  constructor(conf, logger, cache, validator) {
@@ -43,7 +44,7 @@ class InboundApi extends EventEmitter {
43
44
  });
44
45
 
45
46
  if (conf.validateInboundJws) {
46
- this._jwsVerificationKeys = InboundApi._GetJwsKeys(conf.jwsVerificationKeysDirectory);
47
+ this._jwsVerificationKeys = conf.pm4mlEnabled ? conf.peerJWSKeys : InboundApi._GetJwsKeys(conf.jwsVerificationKeysDirectory);
47
48
  }
48
49
  this._api = InboundApi._SetupApi({
49
50
  conf,
@@ -187,6 +188,37 @@ class InboundServer extends EventEmitter {
187
188
  this._logger.log('inbound shut down complete');
188
189
  }
189
190
 
191
+ async reconfigure(conf, logger, cache) {
192
+ // It may be possible to extract the socket from an existing HTTP/HTTPS server and replace
193
+ // it in a new server of the other type, as Node's HTTP and HTTPS servers both eventually
194
+ // are subclasses of net.Server. This wasn't considered as a requirement at the time of
195
+ // writing.
196
+ assert(
197
+ this._conf.mutualTLS.inboundRequests.enabled === conf.mutualTLS.inboundRequests.enabled,
198
+ 'Cannot live-restart an HTTPS server as HTTP or vice versa',
199
+ );
200
+ const newApi = new InboundApi(conf, logger, cache, this._validator);
201
+ await newApi.start();
202
+ return () => {
203
+ this._logger = logger;
204
+ this._cache = cache;
205
+ // TODO: .tls might be undefined, causing an.. err.. undefined dereference..
206
+ const tlsCredsChanged = check.notDeepEqual(
207
+ conf.inbound.tls.creds,
208
+ this._conf.inbound.tls.creds
209
+ );
210
+ if (this._conf.mutualTLS.inboundRequests.enabled && tlsCredsChanged) {
211
+ this._server.setSecureContext(conf.inbound.tls.creds);
212
+ }
213
+ this._server.removeAllListeners('request');
214
+ this._server.on('request', newApi.callback());
215
+ this._api.stop();
216
+ this._api = newApi;
217
+ this._conf = conf;
218
+ this._logger.log('restarted');
219
+ };
220
+ }
221
+
190
222
  _createServer(tlsEnabled, tlsCreds, handler) {
191
223
  if (!tlsEnabled) {
192
224
  return http.createServer(handler);
@@ -132,6 +132,21 @@ class OutboundServer extends EventEmitter {
132
132
  }
133
133
  this._logger.log('Shut down complete');
134
134
  }
135
+
136
+ async reconfigure(conf, logger, cache, metricsClient) {
137
+ const newApi = new OutboundApi(conf, logger, cache, this._validator, metricsClient);
138
+ await newApi.start();
139
+ return () => {
140
+ this._logger = logger;
141
+ this._cache = cache;
142
+ this._server.removeAllListeners('request');
143
+ this._server.on('request', newApi.callback());
144
+ this._api.stop();
145
+ this._api = newApi;
146
+ this._conf = conf;
147
+ this._logger.log('restarted');
148
+ };
149
+ }
135
150
  }
136
151
 
137
152
  module.exports = OutboundServer;
@@ -11,6 +11,7 @@
11
11
  const Koa = require('koa');
12
12
  const ws = require('ws');
13
13
 
14
+ const assert = require('assert').strict;
14
15
  const http = require('http');
15
16
  const yaml = require('js-yaml');
16
17
  const fs = require('fs').promises;
@@ -210,6 +211,36 @@ class TestServer {
210
211
  }
211
212
  this._logger.log('Test server shutdown complete');
212
213
  }
214
+
215
+ async reconfigure({ port, logger, cache }) {
216
+ assert(port === this._port, 'Cannot reconfigure running port');
217
+ const newApi = new TestApi(logger, cache, this._validator);
218
+ const newWsApi = new WsServer(logger.push({ component: 'websocket-server' }), cache);
219
+ await newWsApi.start();
220
+
221
+ return () => {
222
+ const oldWsApi = this._wsapi;
223
+ this._logger = logger;
224
+ this._cache = cache;
225
+ this._wsapi = newWsApi;
226
+ this._api = newApi;
227
+ this._server.removeAllListeners('upgrade');
228
+ this._server.on('upgrade', (req, socket, head) => {
229
+ this._wsapi.handleUpgrade(req, socket, head, (ws) =>
230
+ this._wsapi.emit('connection', ws, req));
231
+ });
232
+ this._server.removeAllListeners('request');
233
+ this._server.on('request', newApi.callback());
234
+ // TODO: we can't guarantee client implementations. Therefore we can't guarantee
235
+ // reconnect logic/behaviour. Therefore instead of closing all websocket client
236
+ // connections as we do below, we should replace handlers.
237
+ oldWsApi.stop().catch((err) => {
238
+ this._logger.push({ err }).log('Error stopping websocket server during reconfigure');
239
+ });
240
+
241
+ this._logger.log('restarted');
242
+ };
243
+ }
213
244
  }
214
245
 
215
246
  module.exports = TestServer;
package/src/config.js CHANGED
@@ -58,6 +58,10 @@ const env = from(process.env, {
58
58
 
59
59
  module.exports = {
60
60
  __parseResourceVersion: parseResourceVersions,
61
+ control: {
62
+ mgmtAPIWsUrl: env.get('MGMT_API_WS_URL').required().asString(),
63
+ mgmtAPIWsPort: env.get('MGMT_API_WS_PORT').default('4005').asPortNumber()
64
+ },
61
65
  mutualTLS: {
62
66
  inboundRequests: {
63
67
  enabled: env.get('INBOUND_MUTUAL_TLS_ENABLED').default('false').asBool(),
@@ -160,4 +164,6 @@ module.exports = {
160
164
  // a transactionRequestId. this option decodes the ilp packet for
161
165
  // the `transactionId` to retrieve the quote from cache
162
166
  allowDifferentTransferTransactionId: env.get('ALLOW_DIFFERENT_TRANSFER_TRANSACTION_ID').default('false').asBool(),
167
+
168
+ pm4mlEnabled: env.get('PM4ML_ENABLED').default('false').asBool(),
163
169
  };
@@ -115,7 +115,7 @@ class OutboundBulkQuotesModel {
115
115
 
116
116
  case 'requestBulkQuote':
117
117
  return this._requestBulkQuote();
118
-
118
+
119
119
  case 'getBulkQuote':
120
120
  return this._getBulkQuote(this.data.bulkQuoteId);
121
121
 
@@ -266,7 +266,7 @@ class OutboundBulkQuotesModel {
266
266
  };
267
267
 
268
268
  individualQuote.note && (quote.note = individualQuote.note);
269
-
269
+
270
270
  if (individualQuote.extensions && individualQuote.extensions.length > 0) {
271
271
  bulkQuoteRequest.extensionList = {
272
272
  extension: individualQuote.extensions
@@ -440,7 +440,7 @@ class OutboundBulkQuotesModel {
440
440
  await this.stateMachine.requestBulkQuote();
441
441
  this._logger.log(`Quotes resolved for bulk quote ${this.data.bulkQuoteId}`);
442
442
  break;
443
-
443
+
444
444
  case 'getBulkQuote':
445
445
  await this.stateMachine.getBulkQuote();
446
446
  this._logger.log(`Get bulk quote ${this.data.bulkQuoteId} has been completed`);
@@ -257,7 +257,7 @@ class OutboundBulkTransfersModel {
257
257
  ilpPacket: individualTransfer.ilpPacket,
258
258
  condition: individualTransfer.condition,
259
259
  };
260
-
260
+
261
261
  if (individualTransfer.extensions && individualTransfer.extensions.length > 0) {
262
262
  bulkTransferRequest.extensionList = {
263
263
  extension: individualTransfer.extensions
@@ -297,7 +297,7 @@ class OutboundRequestToPayModel {
297
297
  this._logger.push({ transactionRequestResponse }).log('Transaction Request Response received');
298
298
  this.data.requestToPayState = transactionRequestResponse.transactionRequestState;
299
299
 
300
-
300
+
301
301
  return resolve(transactionRequestResponse);
302
302
  }
303
303
  catch(err) {
@@ -145,7 +145,7 @@ class OutboundRequestToPayTransferModel {
145
145
  }
146
146
  }
147
147
  break;
148
-
148
+
149
149
  case 'otpReceived':
150
150
  // next transition is executeTransfer
151
151
  await this.stateMachine.executeTransfer();
@@ -230,7 +230,7 @@ class OutboundRequestToPayTransferModel {
230
230
  }
231
231
 
232
232
  /**
233
- * This method is used to communicate back to the Payee that a rejection is being
233
+ * This method is used to communicate back to the Payee that a rejection is being
234
234
  * sent because the OTP did not match.
235
235
  */
236
236
  async rejectRequestToPay() {
@@ -241,7 +241,7 @@ class OutboundRequestToPayTransferModel {
241
241
  const response = {
242
242
  status : `${this.data.requestToPayTransactionId} has been REJECTED`
243
243
  };
244
- return JSON.stringify(response);
244
+ return JSON.stringify(response);
245
245
  }
246
246
 
247
247
 
@@ -471,7 +471,7 @@ class OutboundRequestToPayTransferModel {
471
471
  }
472
472
 
473
473
  /**
474
- * Sends request for
474
+ * Sends request for
475
475
  * Starts the quote resolution process by sending a POST /quotes request to the switch;
476
476
  * then waits for a notification from the cache that the quote response has been received
477
477
  */
@@ -480,7 +480,7 @@ class OutboundRequestToPayTransferModel {
480
480
  return new Promise(async (resolve, reject) => {
481
481
 
482
482
  if( this.data.initiatorType && this.data.initiatorType === 'BUSINESS') return resolve();
483
-
483
+
484
484
  // listen for events on the quoteId
485
485
  const otpKey = `otp_${this.data.requestToPayTransactionId}`;
486
486
 
@@ -501,9 +501,9 @@ class OutboundRequestToPayTransferModel {
501
501
 
502
502
  const otpResponseBody = otpResponse.data;
503
503
  this._logger.push({ otpResponseBody }).log('OTP response received');
504
-
504
+
505
505
  this.data.otpResponse = otpResponseBody;
506
-
506
+
507
507
  return resolve(otpResponse);
508
508
  }
509
509
  catch(err) {
@@ -587,7 +587,7 @@ class OutboundRequestToPayTransferModel {
587
587
  return quote;
588
588
  }
589
589
 
590
-
590
+
591
591
  /**
592
592
  * Executes a transfer
593
593
  * Starts the transfer process by sending a POST /transfers (prepare) request to the switch;
@@ -886,7 +886,7 @@ class OutboundRequestToPayTransferModel {
886
886
  }
887
887
 
888
888
 
889
-
889
+
890
890
  }
891
891
 
892
892
 
@@ -277,7 +277,7 @@ class OutboundTransfersModel {
277
277
  // a GET /parties request to the switch
278
278
  try {
279
279
  const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
280
- this.data.to.idSubValue);
280
+ this.data.to.idSubValue, this.data.to.fspId);
281
281
  this._logger.push({ peer: res }).log('Party lookup sent to peer');
282
282
  }
283
283
  catch(err) {
@@ -68,7 +68,7 @@ const mojaloopPartyToInternalParty = (external) => {
68
68
  internal.idType = external.partyIdInfo.partyIdType;
69
69
  internal.idValue = external.partyIdInfo.partyIdentifier;
70
70
  internal.idSubValue = external.partyIdInfo.partySubIdOrType;
71
- // Note: we dont map fspid to internal transferParty object
71
+ internal.fspId = external.partyIdInfo.fspId;
72
72
  if(external.partyIdInfo.extensionList){
73
73
  internal.extensionList = external.partyIdInfo.extensionList.extension;
74
74
  }
@@ -109,6 +109,7 @@ const mojaloopPartyIdInfoToInternalPartyIdInfo = (external) => {
109
109
  internal.idType = external.partyIdType;
110
110
  internal.idValue = external.partyIdentifier;
111
111
  internal.idSubValue = external.partySubIdOrType;
112
+ internal.fspId = external.fspId;
112
113
 
113
114
  return internal;
114
115
  };
@@ -411,19 +412,19 @@ const mojaloopBulkPrepareToInternalBulkTransfer = (external, bulkQuotes, ilp) =>
411
412
  if (bulkQuotes) {
412
413
  // create a map of internal individual quotes payees indexed by quotedId, for faster lookup
413
414
  const internalQuotesPayeesByQuoteId = {};
414
-
415
+
415
416
  for (const quote of bulkQuotes.internalRequest.individualQuotes) {
416
417
  internalQuotesPayeesByQuoteId[quote.quoteId] = quote.to;
417
418
  }
418
-
419
+
419
420
  // create a map of external individual transfers indexed by quotedId, for faster lookup
420
421
  const externalTransferIdsByQuoteId = {};
421
-
422
+
422
423
  for (const transfer of external.individualTransfers) {
423
424
  const transactionObject = ilp.getTransactionObject(transfer.ilpPacket);
424
425
  externalTransferIdsByQuoteId[transactionObject.quoteId] = transfer.transferId;
425
426
  }
426
-
427
+
427
428
  internal = {
428
429
  bulkTransferId: external.bulkTransferId,
429
430
  bulkQuotes: bulkQuotes.response,
@@ -131,3 +131,12 @@ TRANSFERS_ENDPOINT=ml-testing-toolkit:5000
131
131
  # The incoming transfer request should consists of an ILP packet and a matching condition in this case.
132
132
  # The fulfilment will be generated from the provided ILP packet, and must hash to the provided condition.
133
133
  ALLOW_TRANSFER_WITHOUT_QUOTE=false
134
+
135
+ # Management API websocket connection settings.
136
+ # The Management API uses this for exchanging connector management messages.
137
+ MGMT_API_WS_URL=127.0.0.1
138
+ MGMT_API_WS_PORT=4005
139
+
140
+ # Set to true to enable the use of PM4ML-related services e.g MCM, Management API service
141
+ # when running the scheme-adapter as a mojaloop connector component within Payment Manager for Mojaloop.
142
+ PM4ML_ENABLED=false