diff --git a/.env b/.env index 4c65060..5c15e73 100644 --- a/.env +++ b/.env @@ -5,3 +5,4 @@ DOME_EVENTS_CONTRACT_ABI=[ { "anonymous": false, "inputs": [ { "indexed": false, DOME_PRODUCTION_BLOCK_NUMBER=118733266 RPC_ADDRESS=https://red-t.alastria.io/v0/9461d9f4292b41230527d57ee90652a6 ISS=0x43b27fef24cfe8a0b797ed8a36de2884f9963c0c2a0da640e3ec7ad6cd0c493d +NETWORK=foo \ No newline at end of file diff --git a/docs/blockchainInterfaceDefinition.yaml b/docs.old/blockchainInterfaceDefinition.yaml similarity index 100% rename from docs/blockchainInterfaceDefinition.yaml rename to docs.old/blockchainInterfaceDefinition.yaml diff --git a/package.json b/package.json index d8b3a68..138812d 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,18 @@ "binary-search": "^1.3.6", "dotenv": "^16.3.1", "ethers": "^5.7.1", - "express": "^4.17.3", + "express": "^4.21.2", "jest": "^29.7.0", "morgan": "^1.10.0", "nodemon": "^3.0.2", "supertest": "^6.3.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "ts-jest": "^29.1.1", "ts-node": "^10.7.0", - "typescript": "^4.7.4" + "tsoa": "^6.6.0", + "typescript": "^4.7.4", + "yamljs": "^0.3.0" }, "scripts": { "dev": "nodemon src/server.ts", diff --git a/src/api/DLTInterface.ts b/src/api/DLTInterface.ts index a3d17e7..6529aa2 100644 --- a/src/api/DLTInterface.ts +++ b/src/api/DLTInterface.ts @@ -11,9 +11,10 @@ import { DOMEEvent } from "../utils/types"; const debugLog = debug("DLT Interface Service: "); const errorLog = debug("DLT Interface Service:error "); + /** - * Publish DOME event as a blockchain event. - * + * Publishes DOME event as a blockchain event + * * @param eventType the name of the dome event * @param dataLocation the storage or location of the data associated with the event. * @param relevantMetadata additional information or metadata relevant to the event. @@ -77,6 +78,9 @@ export async function publishDOMEEvent( relevantMetadata, }); + debugLog(" > Adding netwotk:",process.env.NETWORK); + const metadata = [...relevantMetadata, process.env.NETWORK]; + const provider = new ethers.providers.JsonRpcProvider(rpcAddress); const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!, provider); @@ -98,7 +102,8 @@ export async function publishDOMEEvent( previousEntityHash, eventType, dataLocation, - relevantMetadata + // relevantMetadata + metadata ); debugLog(" > Transaction waiting to be mined..."); await tx.wait(); @@ -372,7 +377,6 @@ export async function getActiveDOMEEventsByDate( return allActiveDOMEEvents; } - /** * Returns all the DOME active blockchain events from the blockchain between given dates * @param DOMEEvents some DOME blockchain events @@ -452,4 +456,103 @@ async function getAllActiveDOMEBlockchainEventsBetweenDates(DOMEEvents: ethers.E } return activeEvents; +} + +/** + * Returns all the DOME active events from the blockchain + * + * @param rpcAddress the blockchain node RPC address + * @returns a JSON with all the DOME active events from the blockchain + */ +export async function getAllDOMEEvents( + rpcAddress: string +): Promise{ + if(rpcAddress === ""){ + throw new IllegalArgumentError("The RPC address is blank"); + } + + debugLog(">>>> Getting active events"); + + const provider = new ethers.providers.JsonRpcProvider(rpcAddress); + const DOMEEventsContract = new ethers.Contract( + process.env.DOME_EVENT_CONTRACT_ADDRESS!, + process.env.DOME_EVENTS_CONTRACT_ABI!, + provider + ); + debugLog(">>>> Connecting to blockchain node..."); + debugLog(" >> rpcAddress: " + rpcAddress); + let blockNumber = await provider.getBlockNumber(); + debugLog(" >> Blockchain block number is " + blockNumber); + let allDOMEEvents = await DOMEEventsContract.queryFilter( + "*", + parseInt(process.env.DOME_PRODUCTION_BLOCK_NUMBER!), + blockNumber + ); + allDOMEEvents = allDOMEEvents.slice(1); + let DOMEEvents: DOMEEvent[] = []; + allDOMEEvents.forEach((event) => { + if(event.args == undefined || event.args!.length == 0){ + debugLog("Event with no args, passing to next...") + return; + } + + let eventJson: DOMEEvent = { + id: event.args![0], + timestamp: event.args![1], + eventType: event.args![5], + dataLocation: event.args![6], + relevantMetadata: event.args![7], + entityId: event.args![3], + previousEntityHash: event.args![4] + }; + + debugLog(eventJson); + DOMEEvents.push(eventJson); + }); + debugLog(">>>> Events found: " + DOMEEvents.length); + return DOMEEvents; +} + + +/** + * Subscribes to all DOME Events + * + * @param rpcAddress the blockchain node address to be used for event subscription + * @param ownIss the organization identifier hash + * @param notificationEndpoint the user's endpoint to be notified to of the events of interest. + * The notification is sent as a POST + * @param handler an optional function to handle the events + */ +export async function subscribeToAllEvents( + rpcAddress: string, + ownIss: string, + notificationEndpoint?: string, + handler?: (event: object) => void +){ + if (rpcAddress === null || rpcAddress === undefined) { + throw new IllegalArgumentError("The rpc address is null."); + } + if (ownIss === "") { + throw new IllegalArgumentError("The ownIss is blank."); + } + if (ownIss === null || ownIss === undefined) { + throw new IllegalArgumentError("The ownIss is null."); + } + try{ + debugLog(">>>> Retrieving all DOME Events..."); + const allEvents = await getAllDOMEEvents(rpcAddress); + debugLog(">>>> Subscribing to " + allEvents.length + " events"); + allEvents.forEach(async (event) => { + await subscribeToDOMEEvents( + [event.eventType], + rpcAddress, + ownIss, + notificationEndpoint, + handler + ); + }); + } catch (error) { + errorLog(" > !! Error subscribing to DOME Events"); + throw error; + } } \ No newline at end of file diff --git a/src/routes/routes.ts b/src/routes/routes.ts index bc2bce9..a360742 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -1,13 +1,87 @@ -import { subscribeToDOMEEvents, publishDOMEEvent, getActiveDOMEEventsByDate } from "../api/DLTInterface"; +import { subscribeToDOMEEvents, publishDOMEEvent, getActiveDOMEEventsByDate, getAllDOMEEvents } from "../api/DLTInterface"; import express from "express"; import debug from "debug"; import { IllegalArgumentError } from "../exceptions/IllegalArgumentError"; import { NotificationEndpointError } from "../exceptions/NotificationEndpointError"; +import { all } from "axios"; const router = express.Router(); const debugLog = debug("Routes: "); const errorLog = debug("Routes:error "); + +/** + * @swagger + * tags: + * - name: DLT Adapter + * components: + * schemas: + * v2_publishEvent_body: + * type: object + * additionalProperties: false + * properties: + * eventType: + * type: string + * description: The name of the DOME event + * dataLocation: + * type: string + * description: The storage or location of the data associated with the event + * relevantMetadata: + * type: array + * items: + * type: string + * description: Additional information or metadata relevant to the event + * entityId: + * type: string + * description: The organization identifier hash + * previousEntityHash: + * type: string + * description: Entity identifier hash + * v2_subscribe_body: + * type: object + * additionalProperties: false + * properties: + * eventTypes: + * description: The type of the events of interest for the user + * type: array + * items: + * type: string + * notificationEndpoint: + * description: The user's endpoint to be notified of the events of interest. + * The notification is sent as a POST. + * type: string + * v2_subscribeToAll_body: + * type: object + * additionalProperties: false + * properties: + * notificationEndpoint: + * description: The user's endpoint to be notified of the events of interest. + * The notification is sent as a POST. + * type: string + */ + +/** + * @swagger + * /health: + * get: + * description: Healthcheck endpoint + * tags: + * - DLT Adapter + * responses: + * '200': + * description: Auto generated using Swagger Inspector + * content: + * text/html; charset=utf-8: + * schema: + * type: string + * example: + * status: UP + * checks: + * - name: DLT Adapter health check + * status: UP + * servers: + * - url: http://localhost:8080 + */ router.get("/health", (req: any, resp: any) => { const healthCheckResponse = { status: "UP", @@ -21,7 +95,40 @@ router.get("/health", (req: any, resp: any) => { resp.status(200).json(healthCheckResponse); }); -router.post("/api/v1/publishEvent", (req: any, resp: any) => { +/** + * @swagger + * /api/v2/publishEvent: + * post: + * description: Publishes a DOME Event to the blockchain network set by environment variables through the node RPC Address and with the iss identifier provided. It is not mandatory to provide those two if planning to use the ones declared in the respective environment variables. + * tags: + * - DLT Adapter + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v2_publishEvent_body' + * examples: + * '0': + * value: "{\r\n \"eventType\": \"ProductAdded\",\r\n \"eventDataLocation\": \"x\",\r\n \"relevantMetadata\": [\"veryRelevant1\", \"veryRelevant2\"],\r\n \"entityId\": \"0x626c756500000000000000000000000000000000000000000000000000000000\",\r\n \"previousEntityHash\": \"0x626c756500000000000000000000000000000000000000000000000000000000\"}" + * responses: + * '201': + * description: Returns the blockchain UNIX timestamp of publication of the event + * content: + * text/html; charset=utf-8: + * schema: + * type: string + * example: 1706616455 + * '400': + * description: Auto generated using Swagger Inspector + * content: + * text/html; charset=utf-8: + * schema: + * type: string + * example: Error connecting to the blockchain node. + * servers: + * - url: http://localhost:8080 + */ +router.post("/api/v2/publishEvent", (req: any, resp: any) => { (async () => { debugLog("Entry call from origin: ", req.headers.origin); try { @@ -32,7 +139,7 @@ router.post("/api/v1/publishEvent", (req: any, resp: any) => { req.body.iss ?? process.env.ISS, req.body.entityId, req.body.previousEntityHash, - req.body.rpcAddress ?? process.env.RPC_ADDRESS + req.body.rpcAddress ?? process.env.RPC_ADDRESS ); resp.status(201).json(eventTimestamp); } catch (error: any) { @@ -47,7 +154,28 @@ router.post("/api/v1/publishEvent", (req: any, resp: any) => { })(); }); -router.post("/api/v1/subscribe", (req: any, resp: any) => { +/** + * @swagger + * /api/v2/subscribe: + * post: + * description: Subscribes to the DOME Events of the specified type and notifies the corresponding DOME Events to the specified endpoint. + * tags: + * - DLT Adapter + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v2_subscribe_body' + * examples: + * '0': + * value: "{\r\n \"eventTypes\": [\"ProductAdded1\", \"ProductAdded2\"],\r\n \"notificationEndpoint\": \"http://localhost:8080/api/v1/testSubscribedUser\"\r\n}" + * responses: + * '200': + * description: OK + * servers: + * - url: http://localhost:8080 + */ +router.post("/api/v2/subscribe", (req: any, resp: any) => { (async () => { debugLog("Entry call from origin: ", req.headers.origin); try { @@ -70,7 +198,60 @@ router.post("/api/v1/subscribe", (req: any, resp: any) => { })(); }); -router.get('/api/v1/events', async (req: any, resp: any) => { +/** + * @swagger + * /api/v2/events: + * get: + * tags: + * - DLT Adapter + * parameters: + * - in: query + * name: startDate + * schema: + * type: string + * - in: query + * name: endDate + * schema: + * type: string + * + * responses: + * '200': + * description: Returns the DOME Events that are still active between the given dates + * content: + * application/json: + * examples: + * '0': + * value: [ + * { + * "id": 702, + * "timestamp": 1705401561000, + * "eventType": "ProductOffering", + * "dataLocation": "http://scorpio:9090/ngsi-ld/v1/entities/urn:ngsi-ld:product-offering:443734333?hl=0xb1331924fa7a2dc86ebedfbef5b159449cf91112b5ec4336ca8342cc71ac060e", + * "relevantMetadata": [], + * "entityId": "0x82ca976389b8c45f0a5923c21f8d0185d0d632061a683830e687c29e8bdc91b6", + * "previousEntityHash": "0x82ca976389b8c45f0a5923c21f8d0185d0d632061a683830e687c29e8bdc91b6" + * }, + * { + * "id": 689, + * "timestamp": 1705047233000, + * "eventType": "ProductOffering", + * "dataLocation": "http://scorpio:9090/ngsi-ld/v1/entities/urn:ngsi-ld:product-offering:443734334?hl=0xe9501808af2401bc84d387383e37bf52362017f4c8e51a702f0f0480dced8a82", + * "relevantMetadata": [], + * "entityId": "0x65c4b136290052a864ec06978838bfcad47fc5234c467d34e372a37bc1aa91e4", + * "previousEntityHash": "0x65c4b136290052a864ec06978838bfcad47fc5234c467d34e372a37bc1aa91e4" + * } + * ] + * '400': + * description: Auto generated using Swagger Inspector + * content: + * text/html; charset=utf-8: + * schema: + * type: string + * example: Error connecting to the blockchain node. + * servers: + * - url: http://localhost:8080 + */ +router.get('/api/v2/events', async (req: any, resp: any) => { (async() => { debugLog("Entry call from origin: ", req.headers.origin); @@ -87,4 +268,38 @@ router.get('/api/v1/events', async (req: any, resp: any) => { } })(); }) + + +/** + * @swagger + * /api/v2/subscribeToAll: + * post: + * description: Subscribes to all DOME events and notifies to endpoint + * tags: + * - DLT Adapter + * operationId: subscribeToAllDOMEEvents + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/v2_subscribeToAll_body' + * examples: + * '0': + * value: "{\r\n \"notificationEndpoint\": \"http://localhost:8080/api/v1/testSubscribedUser\"\r\n}" + * responses: + * 200: + * description: OK + */ +router.get('/api/v2/subscribeToAll', async (req: any, resp: any) => { + (async() => { + debugLog("Entry call from origin: ", req.headers.origin); + try{ + let allEvents = await getAllDOMEEvents(process.env.RPC_ADDRESS!); + resp.status(200).json(allEvents); + }catch (error: any){ + errorLog("Error: \n", error); + resp.status(400).send(error.message); + } + }) +}) export = router; diff --git a/src/server.ts b/src/server.ts index 450288c..202b1a8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,6 +4,9 @@ dotenv.config(); const express = require("express") const morgan = require("morgan") +const swaggerUi = require('swagger-ui-express') +const swaggerSpec = require('../swaggerSpec') + import router from "./routes/routes" @@ -53,6 +56,8 @@ router.use((req: any, res: any, next: any) => { /** Routes */ app.use("/", router) +app.use('/docs',swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + /** Error handling */ app.use((req: any, res: any, next: any) => { @@ -60,8 +65,6 @@ app.use((req: any, res: any, next: any) => { return res.status(404).json({ message: error.message }); }); - - app.listen(port, () => { console.log(`DLT Interface API listening at http://localhost:${port}`) }) diff --git a/swaggerSpec.ts b/swaggerSpec.ts new file mode 100644 index 0000000..aee4973 --- /dev/null +++ b/swaggerSpec.ts @@ -0,0 +1,23 @@ +import { title } from "process"; + +const swaggerJsdoc = require('swagger-jsdoc'); + +const options = { + swaggerDefinition: { + openapi: '3.0.0', + info: { + title: 'Blockchain Interface DOME', + description: 'The component to be used when interacting with the blockchain layer in DOME', + version: '0.1' + }, + servers: [ + { + url: 'http://localhost:8080' + } + ] + }, + apis: ['./src/**/*.ts'] +} + +const swaggerSpec = swaggerJsdoc(options); +module.exports = swaggerSpec; \ No newline at end of file diff --git a/tsoa.json b/tsoa.json new file mode 100644 index 0000000..a980bf7 --- /dev/null +++ b/tsoa.json @@ -0,0 +1,13 @@ +{ + "entryFile": "src/server.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "spec": { + "outputDirectory": "old", + "specVersion": 3, + "basePath": "/api", + "specFileBaseName": "swagger" + }, + "routes": { + "routesDir": "src/routes" + } +}