Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding ISO20022 API translation capability #502

Draft
wants to merge 14 commits into
base: feat/fx-impl
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ defaults_docker_Dependencies: &defaults_docker_Dependencies |
apk --no-cache add curl
apk --no-cache add openssh-client
apk --no-cache add bash bash-doc bash-completion
apk --no-cache add -t build-dependencies make gcc g++ python3 libtool autoconf automake
apk --no-cache add -t build-dependencies make gcc g++ python3 py3-pip libtool autoconf automake
apk --no-cache add librdkafka-dev
apk --no-cache add py3-setuptools

## Default 'default-machine' executor dependencies
defaults_machine_Dependencies: &defaults_machine_Dependencies |
Expand Down
15 changes: 14 additions & 1 deletion audit-ci.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@
"GHSA-78xj-cgh5-2h22",
// https://github.com/advisories/GHSA-rm97-x556-q36h
"GHSA-rm97-x556-q36h",
"GHSA-wf5p-g6vw-rhxx" // https://github.com/advisories/GHSA-wf5p-g6vw-rhxx
"GHSA-wf5p-g6vw-rhxx", // https://github.com/advisories/GHSA-wf5p-g6vw-rhxx,
"GHSA-2p57-rm9w-gvfp",
"GHSA-8hc4-vh64-cxmj",
"GHSA-952p-6rrq-rcjv",
"GHSA-9wv6-86v2-598j",
"GHSA-cgfm-xwp7-2cvr",
"GHSA-cm22-4g7w-348p",
"GHSA-ghr5-ch3p-vcr6",
"GHSA-grv7-fg5c-xmjg",
"GHSA-m6fv-jmcg-4jfg",
"GHSA-m95q-7qp3-xv42",
"GHSA-qw6h-vgh9-j6wx",
"GHSA-qwcr-r2fm-qrc7",
"GHSA-rv95-896h-c2vc",
]
}
41 changes: 41 additions & 0 deletions modules/api-svc/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import jest from "eslint-plugin-jest";
import globals from "globals";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});

export default [...compat.extends("eslint:recommended"), {
plugins: {
jest,
},

languageOptions: {
ecmaVersion: 'latest',
globals: {
...globals.node,
...globals.jest,
structuredClone: 'readonly',
},
},

rules: {
indent: ["error", 4, {
SwitchCase: 1,
}],

"linebreak-style": [2, "unix"],
quotes: [2, "single"],
semi: [2, "always"],
"no-console": 2,
"no-prototype-builtins": "off",
},
}];
49 changes: 25 additions & 24 deletions modules/api-svc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,58 +63,59 @@
},
"dependencies": {
"@koa/cors": "^5.0.0",
"@mojaloop/api-snippets": "17.4.0",
"@mojaloop/central-services-error-handling": "^13.0.0",
"@mojaloop/central-services-logger": "^11.3.0",
"@mojaloop/api-snippets": "17.7.2",
"@mojaloop/central-services-error-handling": "^13.0.2",
"@mojaloop/central-services-logger": "^11.5.1",
"@mojaloop/central-services-metrics": "^12.0.8",
"@mojaloop/central-services-shared": "18.3.0",
"@mojaloop/event-sdk": "^14.0.1",
"@mojaloop/central-services-shared": "18.11.0",
"@mojaloop/event-sdk": "^14.1.1",
"@mojaloop/ml-schema-transformer-lib": "^1.1.6",
"@mojaloop/sdk-scheme-adapter-private-shared-lib": "workspace:^",
"@mojaloop/sdk-standard-components": "18.1.0",
"ajv": "8.12.0",
"axios": "^1.6.8",
"co-body": "^6.1.0",
"@mojaloop/sdk-standard-components": "19.2.0",
"ajv": "8.17.1",
"axios": "^1.7.7",
"co-body": "^6.2.0",
"dotenv": "^16.4.5",
"env-var": "^7.4.1",
"express": "^4.18.3",
"env-var": "^7.5.0",
"express": "^4.21.1",
"fast-json-patch": "^3.1.1",
"fast-safe-stringify": "^2.1.1",
"javascript-state-machine": "^3.1.0",
"js-yaml": "^4.1.0",
"json-schema-ref-parser": "^9.0.9",
"koa": "^2.15.1",
"koa": "^2.15.3",
"koa-body": "^6.0.1",
"lodash": "^4.17.21",
"module-alias": "^2.2.3",
"oauth2-server": "^4.0.0-dev.2",
"openapi-jsonschema-parameters": "^12.1.3",
"prom-client": "^15.1.0",
"prom-client": "^15.1.3",
"promise-timeout": "^1.3.0",
"random-word-slugs": "^0.1.7",
"redis": "^4.6.13",
"redis": "^4.7.0",
"uuidv4": "^6.2.13",
"ws": "^8.16.0"
"ws": "^8.18.0"
},
"devDependencies": {
"@babel/core": "^7.24.1",
"@babel/preset-env": "^7.24.1",
"@babel/core": "^7.25.9",
"@babel/preset-env": "^7.25.9",
"@redocly/openapi-cli": "^1.0.0-beta.94",
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.14",
"babel-jest": "^29.7.0",
"eslint": "^8.57.0",
"eslint": "^9.13.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.8.3",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"nock": "^13.5.4",
"nock": "^13.5.5",
"npm-check-updates": "^16.7.10",
"openapi-response-validator": "^12.1.3",
"openapi-typescript": "^6.7.5",
"openapi-typescript": "^7.4.1",
"redis-mock": "^0.56.3",
"replace": "^1.2.2",
"standard-version": "^9.5.0",
"supertest": "^6.3.4",
"supertest": "^7.0.0",
"swagger-cli": "^4.0.4"
},
"standard-version": {
Expand Down
2 changes: 1 addition & 1 deletion modules/api-svc/src/ControlAgent/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class Client extends ws {
let msg;
try {
msg = deserialise(data);
} catch (err) {
} catch {
this._logger.isErrorEnabled && this._logger.push({ data }).console.error();('Couldn\'t parse received message');
this.send(build.ERROR.NOTIFY.JSON_PARSE_ERROR());
}
Expand Down
2 changes: 1 addition & 1 deletion modules/api-svc/src/ControlServer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ class Server extends ws.Server {
let msg;
try {
msg = deserialise(data);
} catch (err) {
} catch {
logger.isErrorEnabled && logger.push({ data }).error('Couldn\'t parse received message');
client.send(build.ERROR.NOTIFY.JSON_PARSE_ERROR());
}
Expand Down
4 changes: 2 additions & 2 deletions modules/api-svc/src/InboundServer/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2197,7 +2197,7 @@ components:
API for signature verification, should contain the service URI. Required
if signature verification is used, for more information, see [the API
Signature
document](https://github.com/mojaloop/docs/tree/master/Specification%20Document%20Set).
document](https://github.com/mojaloop/docs/tree/main/Specification%20Document%20Set).
FSPIOP-HTTP-Method:
name: FSPIOP-HTTP-Method
in: header
Expand All @@ -2209,7 +2209,7 @@ components:
by the API for signature verification, should contain the service HTTP
method. Required if signature verification is used, for more
information, see [the API Signature
document](https://github.com/mojaloop/docs/tree/master/Specification%20Document%20Set).
document](https://github.com/mojaloop/docs/tree/main/Specification%20Document%20Set).
Type:
name: Type
in: path
Expand Down
40 changes: 40 additions & 0 deletions modules/api-svc/src/InboundServer/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ const {
TransfersModel,
} = require('../lib/model');

const { TransformFacades } = require('@mojaloop/ml-schema-transformer-lib');

// todo: find a sensible place to put API type consts, probably export them from sdk standard components
const ISO_API_TYPE = 'iso20022';

/**
* Handles a GET /authorizations/{id} request
*/
Expand Down Expand Up @@ -159,6 +164,13 @@ const postPartiesByTypeAndId = (ctx) => {
* Handles a POST /quotes request
*/
const postQuotes = async (ctx) => {
if(ctx.state.conf.apiType === ISO_API_TYPE) {
// we need to transform the incoming request body from iso20022 to fspiop
ctx.state.logger.isDebugEnabled && ctx.state.logger.push(ctx.request.body).debug('Transforming incoming ISO20222 post quotes body to FSPIOP');
const target = await TransformFacades.FSPIOPISO20022.quotes.post({ body: ctx.request.body });
ctx.request.body = target.body;
}

const sourceFspId = ctx.request.headers['fspiop-source'];
const quoteRequest = {
body: { ...ctx.request.body },
Expand Down Expand Up @@ -206,6 +218,13 @@ const postQuotes = async (ctx) => {
* Handles a POST /transfers request
*/
const postTransfers = async (ctx) => {
if(ctx.state.conf.apiType === ISO_API_TYPE) {
// we need to transform the incoming request body from iso20022 to fspiop
ctx.state.logger.isDebugEnabled && ctx.state.logger.push(ctx.request.body).debug('Transforming incoming ISO20222 post transfers body to FSPIOP');
const target = await TransformFacades.FSPIOPISO20022.transfers.post({ body: ctx.request.body });
ctx.request.body = target.body;
}

const sourceFspId = ctx.request.headers['fspiop-source'];
const transferRequest = {
body: { ...ctx.request.body },
Expand Down Expand Up @@ -433,6 +452,13 @@ const putParticipantsByTypeAndIdError = async(ctx) => {
* request.
*/
const putPartiesByTypeAndId = async (ctx) => {
if(ctx.state.conf.apiType === ISO_API_TYPE) {
// we need to transform the incoming request body from iso20022 to fspiop
ctx.state.logger.isDebugEnabled && ctx.state.logger.push(ctx.request.body).debug('Transforming incoming ISO20222 put parties body to FSPIOP');
const target = await TransformFacades.FSPIOPISO20022.parties.put({ body: ctx.request.body });
ctx.request.body = target.body;
}

const idType = ctx.state.path.params.Type;
const idValue = ctx.state.path.params.ID;
const idSubValue = ctx.state.path.params.SubId;
Expand All @@ -459,6 +485,13 @@ const putPartiesByTypeAndId = async (ctx) => {
* Handles a PUT /quotes/{ID}. This is a response to a POST /quotes request
*/
const putQuoteById = async (ctx) => {
if(ctx.state.conf.apiType === ISO_API_TYPE) {
// we need to transform the incoming request body from iso20022 to fspiop
ctx.state.logger.isDebugEnabled && ctx.state.logger.push(ctx.request.body).debug('Transforming incoming ISO20222 put quotes body to FSPIOP');
const target = await TransformFacades.FSPIOPISO20022.quotes.put({ body: ctx.request.body });
ctx.request.body = target.body;
}

// TODO: refactor legacy models to use QuotesModel
// - OutboundRequestToPayTransferModel
// - OutboundTransfersModel
Expand Down Expand Up @@ -627,6 +660,13 @@ const putTransactionRequestsByIdError = async (ctx) => {
* Handles a PUT /transfers/{ID}. This is a response to a POST|GET /transfers request
*/
const putTransfersById = async (ctx) => {
if(ctx.state.conf.apiType === ISO_API_TYPE) {
// we need to transform the incoming request body from iso20022 to fspiop
ctx.state.logger.isDebugEnabled && ctx.state.logger.push(ctx.request.body).debug('Transforming incoming ISO20222 put transfers body to FSPIOP');
const target = await TransformFacades.FSPIOPISO20022.transfers.put({ body: ctx.request.body });
ctx.request.body = target.body;
}

// TODO: refactor legacy models to use TransfersModel
// - OutboundRequestToPayTransferModel
// - OutboundTransfersModel
Expand Down
12 changes: 12 additions & 0 deletions modules/api-svc/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,16 @@ module.exports = {

fspiopApiServerMaxRequestBytes: env.get('FSPIOP_API_SERVER_MAX_REQUEST_BYTES').default('209715200').asIntPositive(), // Default is 200mb
backendApiServerMaxRequestBytes: env.get('BACKEND_API_SERVER_MAX_REQUEST_BYTES').default('209715200').asIntPositive(), // Default is 200mb

// ISO-20022 config options
// apiType can be one of:
// - fspiop
// - iso20022
apiType: env.get('API_TYPE').default('fspiop').asString(),

// ILP version options
// ilpVersion can be one of:
// - 1
// - 4
ilpVersion: env.get('ILP_VERSION').default('1').asString(),
};
2 changes: 1 addition & 1 deletion modules/api-svc/src/lib/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = Object.fromEntries(
try {
f.bind(assert)(...args);
return true;
} catch (err) {
} catch {
return false;
}
}])
Expand Down
9 changes: 6 additions & 3 deletions modules/api-svc/src/lib/model/InboundTransfersModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class InboundTransfersModel {
jwsSign: config.jwsSign,
jwsSigningKey: config.jwsSigningKey,
wso2: config.wso2,
resourceVersions: config.resourceVersions
resourceVersions: config.resourceVersions,
apiType: config.apiType,
});

this._backendRequests = new BackendRequests({
Expand All @@ -67,7 +68,9 @@ class InboundTransfersModel {

this._checkIlp = config.checkIlp;

this._ilp = new Ilp({
// default to ILP 1 unless v4 is set
const ilpVersion = config.ilpVersion === '4' ? Ilp.ILP_VERSIONS.v4 : Ilp.ILP_VERSIONS.v1;
this._ilp = Ilp.ilpFactory(ilpVersion, {
secret: config.ilpSecret,
logger: this._logger,
});
Expand Down Expand Up @@ -726,7 +729,7 @@ class InboundTransfersModel {
// TODO: Verify and align with actual schema for bulk transfers error endpoint
const mojaloopErrorResponse = {
bulkTransferState: FSPIOPBulkTransferStateEnum.REJECTED,
// eslint-disable-next-line no-unused-vars
individualTransferResults: individualTransferErrors.map(({ transferId, transferError }) => ({
transferId,
errorInformation: transferError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class OutboundRequestToPayTransferModel {
wso2: config.wso2,
});

this._ilp = new Ilp({
this._ilp = Ilp.ilpFactory(Ilp.ILP_VERSIONS.v1, {
secret: config.ilpSecret,
logger: this._logger,
});
Expand Down
5 changes: 4 additions & 1 deletion modules/api-svc/src/lib/model/OutboundTransfersModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ class OutboundTransfersModel {
jwsSigningKey: config.jwsSigningKey,
wso2: config.wso2,
resourceVersions: config.resourceVersions,
apiType: config.apiType,
});

this._ilp = new Ilp({
// default to ILP 1 unless v4 is set
const ilpVersion = config.ilpVersion === '4' ? Ilp.ILP_VERSIONS.v4 : Ilp.ILP_VERSIONS.v1;
this._ilp = Ilp.ilpFactory(ilpVersion, {
secret: config.ilpSecret,
logger: this._logger,
});
Expand Down
4 changes: 2 additions & 2 deletions modules/api-svc/src/lib/model/lib/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ const internalBulkQuotesResponseToMojaloop = (internal) => {
mojaloopApiErrorCode,
null,
);
} catch (e) {
} catch {
// If error status code isn't FSPIOP conforming, create generic
// FSPIOP error and include backend code and message in FSPIOP message.
error = new ErrorHandling.Factory.FSPIOPError(
Expand Down Expand Up @@ -542,7 +542,7 @@ const internalBulkTransfersResponseToMojaloop = (internal, fulfilments) => {
errorInformation: error.toApiErrorObject().errorInformation,
};

} catch (e) {
} catch {
// If error status code isn't FSPIOP conforming, create generic
// FSPIOP error and include backend code and message in FSPIOP message.
error = new ErrorHandling.Factory.FSPIOPError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,22 @@ MockMojaloopRequests.__putBulkTransfers = jest.fn(() => Promise.resolve());
MockMojaloopRequests.__putBulkTransfersError = jest.fn(() => Promise.resolve());
MockMojaloopRequests.__patchTransfers = jest.fn(() => Promise.resolve());

class MockIlp {
const MockIlp = {
ilpFactory: (version, options) => {
switch(version) {
case 'v1':
return new MockIlpV1(options);
case 'v4':
throw new Error('v4 not supported by mock');
}
},
ILP_VERSIONS: {
v1: 'v1',
v4: 'v4',
}
};

class MockIlpV1 {
constructor(config) {
assert(config.logger, 'Must supply a logger to Ilp constructor');
this.logger = config.logger;
Expand Down
Loading