diff --git a/.env b/.env index 1a1aecb..3d1dcfc 100644 --- a/.env +++ b/.env @@ -1,11 +1,20 @@ API_VERSION=v1 -AFRICAS_TALKING_VALID_IPS=127.0.0.1,0.0.0.0 +AFRICASTALKING_VALID_IPS=127.0.0.1,0.0.0.0 +CIC_CUSTODIAL_BALANCE_ENDPOINT=http://localhost:5000/api/balance +CIC_CUSTODAIL_REGISTER_ENDPOINT=http://localhost:5000/api/register +CIC_CUSTODIAL_TRANSFER_ENDPOINT=http://localhost:5000/api/transfer +CIC_GRAPH_GRAPHQQL_ENDPOINT=http://localhost:6080/graphql +CIC_GRAPH_HASURA_ADMIN_SECRET=admin DATABASE_URL=postgres://postgres:password@localhost:5432/cic_ussd LOG_LEVEL=debug +NATS_CLIENT_NAME=cic-ussd +NATS_DRAIN_ON_SHUTDOWN=true +NATS_URL=nats://localhost:4222 +NATS_SUBJECT=CHAIN.* NODE_ENV=development REDIS_DATABASE=0 REDIS_HOST=localhost REDIS_PASSWORD=xD REDIS_PORT=6379 SERVER_HOST=127.0.0.1 -SERVER_PORT=5000 +SERVER_PORT=5000 \ No newline at end of file diff --git a/.env.example b/.env.example index fbdac96..3d1dcfc 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,16 @@ API_VERSION=v1 -AFRICAS_TALKING_VALID_IPS=127.0.0.1,0.0.0.0 -CIC_GRAPH_GRAPHQL_ENDPOINT=http://localhost:6080/v1/graphql +AFRICASTALKING_VALID_IPS=127.0.0.1,0.0.0.0 +CIC_CUSTODIAL_BALANCE_ENDPOINT=http://localhost:5000/api/balance +CIC_CUSTODAIL_REGISTER_ENDPOINT=http://localhost:5000/api/register +CIC_CUSTODIAL_TRANSFER_ENDPOINT=http://localhost:5000/api/transfer +CIC_GRAPH_GRAPHQQL_ENDPOINT=http://localhost:6080/graphql CIC_GRAPH_HASURA_ADMIN_SECRET=admin -DATABASE_DSN=postgres://postgres:postgres@localhost:5432/cic_ussd +DATABASE_URL=postgres://postgres:password@localhost:5432/cic_ussd LOG_LEVEL=debug +NATS_CLIENT_NAME=cic-ussd +NATS_DRAIN_ON_SHUTDOWN=true +NATS_URL=nats://localhost:4222 +NATS_SUBJECT=CHAIN.* NODE_ENV=development REDIS_DATABASE=0 REDIS_HOST=localhost diff --git a/dev/env.sh b/dev/env.sh new file mode 100644 index 0000000..083b91a --- /dev/null +++ b/dev/env.sh @@ -0,0 +1,4 @@ +#!/bin/bash +PROJECT_ROOT_DIR=$(dirname "$(dirname "$(realpath "$0")")") +echo "PROJECT_ROOT_DIR=$PROJECT_ROOT_DIR" +eval "$(cat "$PROJECT_ROOT_DIR/.env" | sed 's/^/export /')" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index aa48939..d16aaf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,13 @@ "@prisma/client": "^4.8.0", "@types/qs": "^6.9.7", "@xstate/immer": "^0.3.1", - "dotenv": "^16.0.3", "fastify": "^4.9.2", + "fastify-plugin": "^4.5.0", "graphql-request": "^5.1.0", "immer": "^9.0.16", "ioredis": "^5.2.4", "libphonenumber-js": "^1.10.14", + "nats": "^2.11.0", "prisma": "^4.8.0", "qs": "^6.11.0", "redis-json": "^6.0.3", @@ -33,11 +34,13 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "@xstate/cli": "^0.3.3", + "dotenv": "^16.0.3", "eslint": "^8.27.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "prettier": "^2.7.1", "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.1.2", "typescript": "^4.8.4" }, "engines": { @@ -1531,6 +1534,7 @@ "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true, "engines": { "node": ">=12" } @@ -2062,9 +2066,9 @@ } }, "node_modules/fastify-plugin": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.4.0.tgz", - "integrity": "sha512-ovwFQG2qNy3jcCROiWpr94Hs0le+c7N/3t7m9aVwbFhkxcR/esp2xu25dP8e617HpQdmeDv+gFX4zagdUhDByw==" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.0.tgz", + "integrity": "sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==" }, "node_modules/fastq": { "version": "1.14.0", @@ -2760,6 +2764,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/nats": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nats/-/nats-2.11.0.tgz", + "integrity": "sha512-Zkjtw4LRT0F9IZwSpLr6O0aLbK0w8rX7kU6PHSdk52YzB0c/pkiXstuGcdUpYdkAYkBS06FHfmRE4BVvCn7Pag==", + "dependencies": { + "nkeys.js": "1.0.4", + "web-streams-polyfill": "^3.2.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2772,6 +2788,17 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/nkeys.js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.0.4.tgz", + "integrity": "sha512-xeNDE6Ha5I3b3PnlHyT9AbmBxq3Vb9KHzmaI/h4IXYg0PUVZSUXNHNhTfU20oBsubw2ZdV/1AdC6hnRuMiZfMQ==", + "dependencies": { + "tweetnacl": "1.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -3658,6 +3685,20 @@ "strip-json-comments": "^2.0.0" } }, + "node_modules/tsconfig-paths": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", + "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tsconfig/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3688,6 +3729,11 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3813,6 +3859,14 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -4988,7 +5042,8 @@ "dotenv": { "version": "16.0.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "dev": true }, "dynamic-dedupe": { "version": "0.3.0", @@ -5389,9 +5444,9 @@ } }, "fastify-plugin": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.4.0.tgz", - "integrity": "sha512-ovwFQG2qNy3jcCROiWpr94Hs0le+c7N/3t7m9aVwbFhkxcR/esp2xu25dP8e617HpQdmeDv+gFX4zagdUhDByw==" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.0.tgz", + "integrity": "sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==" }, "fastq": { "version": "1.14.0", @@ -5898,6 +5953,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "nats": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/nats/-/nats-2.11.0.tgz", + "integrity": "sha512-Zkjtw4LRT0F9IZwSpLr6O0aLbK0w8rX7kU6PHSdk52YzB0c/pkiXstuGcdUpYdkAYkBS06FHfmRE4BVvCn7Pag==", + "requires": { + "nkeys.js": "1.0.4", + "web-streams-polyfill": "^3.2.1" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5910,6 +5974,14 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "nkeys.js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.0.4.tgz", + "integrity": "sha512-xeNDE6Ha5I3b3PnlHyT9AbmBxq3Vb9KHzmaI/h4IXYg0PUVZSUXNHNhTfU20oBsubw2ZdV/1AdC6hnRuMiZfMQ==", + "requires": { + "tweetnacl": "1.0.3" + } + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -6527,6 +6599,17 @@ } } }, + "tsconfig-paths": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", + "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", + "dev": true, + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -6542,6 +6625,11 @@ "tslib": "^1.8.1" } }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6622,6 +6710,11 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 1fb5aa5..be76256 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "bugs": "https://github.com/grassrootseconomics/cic-ussd/issues", "license": "AGPL-3.0-or-later", "scripts": { - "dev": " ts-node-dev --exit-child src/index.ts ", + "dev": " ts-node-dev --exit-child -r tsconfig-paths/register src/index.ts", "build": "tsc -p tsconfig.json", "lint": "eslint src --ext .ts --fix" }, @@ -20,12 +20,13 @@ "@prisma/client": "^4.8.0", "@types/qs": "^6.9.7", "@xstate/immer": "^0.3.1", - "dotenv": "^16.0.3", "fastify": "^4.9.2", + "fastify-plugin": "^4.5.0", "graphql-request": "^5.1.0", "immer": "^9.0.16", "ioredis": "^5.2.4", "libphonenumber-js": "^1.10.14", + "nats": "^2.11.0", "prisma": "^4.8.0", "qs": "^6.11.0", "redis-json": "^6.0.3", @@ -38,11 +39,13 @@ "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "@xstate/cli": "^0.3.3", + "dotenv": "^16.0.3", "eslint": "^8.27.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "prettier": "^2.7.1", "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.1.2", "typescript": "^4.8.4" }, "prisma": { diff --git a/src/app.ts b/src/app.ts index 67a7319..d50eef8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,15 +1,19 @@ import fastifyCors from '@fastify/cors' import formBody from '@fastify/formbody' import fastifySensible from '@fastify/sensible' -import fastify, { FastifyInstance } from 'fastify' +import fastify from 'fastify' import { config } from './config' import qs from 'qs'; import * as dotenv from 'dotenv' +import natsService from '@plugins/nats' +import { initChainEventsHandler } from "@lib/events/handler"; -const build = (): FastifyInstance => { +const build = async () => { // TODO: [Philip] - Whereas this shifts from convict to dotenv, is it ideal for externally defined variables like ones stored in vault? dotenv.config() + + // create fastify app. const app = fastify({ disableRequestLogging: true, logger: { @@ -23,8 +27,10 @@ const build = (): FastifyInstance => { }) app.register(fastifyCors, { origin: true }) app.register(fastifySensible) + app.register(natsService) + - app.setErrorHandler(function (error, request, reply) { + app.setErrorHandler(function(error, request, reply) { app.log.error({ error: error.toString(), request: request }) reply.status(500).send({ diff --git a/src/config.ts b/src/config.ts index 67ef274..af6e37b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,20 @@ +function stringToList (value : string|undefined) : string[] | void { + if (value === undefined) { + return + } else { + return value.split(','); + } +} + export const config = { API: { VERSION: process.env.API_VERSION }, - AFRICAS_TALKING: { - VALID_IPS: process.env.AFRICASTALKING_VALID_IPS + AFRICASTALKING: { + VALID_IPS: stringToList(process.env.AFRICASTALKING_VALID_IPS) ?? ['0.0.0.0'] }, CIC_CUSTODIAL:{ + BALANCE_ENDPOINT: process.env.CIC_CUSTODIAL_BALANCE_ENDPOINT ?? 'http://localhost:5000/api/balance', REGISTER_ENDPOINT: process.env.CIC_CUSTODIAL_REGISTER_ENDPOINT ?? 'http://localhost:5000/api/register', TRANSFER_ENDPOINT: process.env.CIC_CUSTODIAL_TRANSFER_ENDPOINT ?? 'http://localhost:5000/api/transfer', }, @@ -20,6 +29,13 @@ export const config = { LOG: { LEVEL: process.env.LOG_LEVEL }, + NATS : { + CLIENT_NAME: process.env.NATS_CLIENT_NAME ?? 'cic-ussd', + DRAIN_ON_SHUTDOWN: process.env.NATS_DRAIN_ON_SHUTDOWN ?? true, + SUBJECT: process.env.NATS_SUBJECT ?? 'CHAIN.*', + URL: process.env.NATS_URL ?? 'nats://localhost:4222', + VERBOSE: process.env.NATS_VERBOSE ?? false + }, REDIS: { DATABASE: parseInt(process.env.REDIS_DATABASE ?? '0'), HOST: process.env.REDIS_HOST ?? 'localhost', diff --git a/src/index.ts b/src/index.ts index 240ea32..0b98240 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ import build from './app' import { config } from './config' +import { initChainEventsHandler } from "@lib/events/handler"; -const main = () => { - const app = build() - console.log(`PORT: ${config.SERVER.PORT}`) +const main = async () => { + console.log('Starting server.') + const app = await build() app.ready((error) => { if (error) { @@ -11,6 +12,9 @@ const main = () => { process.exit(1) } + // perform initializations + initChainEventsHandler(app); + app.listen( { host: config.SERVER.HOST, @@ -27,7 +31,7 @@ const main = () => { for (const signal of ['SIGINT', 'SIGTERM']) { process.once(signal, async () => { - app.log.info('Gracefully shutting down') + app.log.info('Gracefully shutting down.') await app.close() return process.exit(0) }) diff --git a/src/lib/custodail/base.ts b/src/lib/custodail/base.ts index 8895d31..4a37f1d 100644 --- a/src/lib/custodail/base.ts +++ b/src/lib/custodail/base.ts @@ -1,15 +1,39 @@ import { config } from "@src/config"; -import { RegistrationResponse } from "@lib/types/custodail/api"; +import { RegistrationResponse, Transfer, TransferResponse } from "@lib/types/custodail/api"; export class CicCustodial { //TODO[Philip]: Consider separating these member functions into separate files should functionality ever balloon. - static async createWallet() { - await fetch(config.CIC_CUSTODIAL.REGISTER_ENDPOINT, { + static async createWallet(): Promise { + const response = await fetch(config.CIC_CUSTODIAL.REGISTER_ENDPOINT, { + method: 'POST' + }); + + if (!response.ok) { + console.error('Failed to create wallet'); + throw new Error(`Failed to create wallet: ${response.status} ${response.statusText}`); + } + + console.log('Successfully created wallet'); + return await response.json(); + } + + static async transfer(payload: Transfer): Promise { + const response = await fetch(config.CIC_CUSTODIAL.TRANSFER_ENDPOINT, { method: 'POST', - }) - .then((response) => { + body: JSON.stringify(payload) + }); + + if (!response.ok) { + console.error('Failed to initiate transfer.'); + throw new Error(`Failed to transfer funds: ${response.status} ${response.statusText}`); + } + + console.log('Successfully initiated transfer.') + return await response.json(); + } - }) + static async queryBalance(publicKey: string): Promise { + //TODO[Philip]: Not implemented yet. } } \ No newline at end of file diff --git a/src/lib/events/handler.ts b/src/lib/events/handler.ts new file mode 100644 index 0000000..9e9e1dc --- /dev/null +++ b/src/lib/events/handler.ts @@ -0,0 +1,56 @@ +// create subscription to nats +import { FastifyInstance } from "fastify"; +import { config } from "@src/config"; +import { Codec, JSONCodec, Msg } from "nats"; + + +interface TransferEvent { + block: number; + from: string; + success: boolean; + to: string; + tokenAddress: string; + transactionHash: string; + transactionIndex: number; + value: number; +} + +export async function initChainEventsHandler(fastify: FastifyInstance){ + + // initialize subscription. + fastify.log.info(`Subscribing to NATS subject: ${config.NATS.SUBJECT}.`); + const subscription = fastify.natsConnection.subscribe(config.NATS.SUBJECT, { + + }); + + // create codec for decoding messages. + const codec = JSONCodec(); + + // iterate over subscription. + for await (const msg of subscription) { + await processMessage(codec, fastify, msg); + } +} + +async function processMessage(codec: Codec, fastify: FastifyInstance, msg: Msg) { + + // TODO: [Philip] - Implement message formatting for porting to Jessamy. + let message: TransferEvent | null = null; + switch(msg.subject){ + case "CHAIN.mintTo": + message = codec.decode(msg.data); + fastify.log.info(`MintedTo ${message.value} ${message.tokenAddress} to ${message.to}.`); + break; + case "CHAIN.transfer": + message = codec.decode(msg.data); + fastify.log.info(`Transferred ${message.value} ${message.tokenAddress} to ${message.to}.`); + break; + case "CHAIN.transferFrom": + message = codec.decode(msg.data); + fastify.log.info(`TransferredFrom ${message.value} ${message.tokenAddress} to ${message.to}.`); + break; + default: + fastify.log.warn(`Message subject: ${msg.subject} not recognized. Ignoring message`); + break; + } +} \ No newline at end of file diff --git a/src/plugins/nats.ts b/src/plugins/nats.ts new file mode 100644 index 0000000..e1ede51 --- /dev/null +++ b/src/plugins/nats.ts @@ -0,0 +1,66 @@ +import { FastifyInstance, FastifyPluginAsync } from "fastify"; +import fp from "fastify-plugin"; +import { config } from "@src/config"; +import { connect, ConnectionOptions, NatsConnection } from "nats"; + + +declare module 'fastify' { + interface FastifyInstance { + natsConnection: NatsConnection; + } +} +const natsService: FastifyPluginAsync = async (fastify, options) => { + let natsOptions: ConnectionOptions = { + name: config.NATS.CLIENT_NAME, + servers: [config.NATS.URL], + } + + if (config.DEV) { + natsOptions.debug = true; + natsOptions.verbose = false; + } + + if (natsOptions.servers === undefined || natsOptions.servers === null || natsOptions.servers.length < 1) { + throw new Error(`Must specify NATS Server/s URL.`) + } + // handle connection to nats. + await initConnectionToNats(fastify, natsOptions); +} + +async function initConnectionToNats(fastify: FastifyInstance, natsOptions: ConnectionOptions) { + + // initiate nats connection + let natsConnection: NatsConnection; + + try { + fastify.log.info(`Connecting to NATS server: ${config.NATS.URL}.`); + natsConnection = await connect(natsOptions); + + // pass the connection to the fastify instance + fastify.decorate("natsConnection", natsConnection); + } catch (error) { + fastify.log.error(`Failed to connect to NATS server: ${config.NATS.URL}.`); + throw error; + } + + // make sure to gracefully drain the client in the event of server shutdown. + // TODO: [Philip x Sohail] - What happens when a pod is killed/deleted? + // Does this get called? + // If so, should we track the last block processed and resume from there? + // How do we do that? + fastify.addHook("onClose", async (instance) => { + if (config.NATS.DRAIN_ON_SHUTDOWN) { + fastify.log.info("Draining NATS connection."); + await natsConnection.drain(); + } else { + fastify.log.info("Flushing NATS connection."); + await natsConnection.flush(); + await natsConnection.close(); + } + }); +} + +export default fp(natsService, { + fastify: "4.x", + name: "chain-events-handler", +}); \ No newline at end of file diff --git a/src/plugins/handler/ussdSession.ts b/src/plugins/ussdSession.ts similarity index 100% rename from src/plugins/handler/ussdSession.ts rename to src/plugins/ussdSession.ts diff --git a/tsconfig.json b/tsconfig.json index ef7408f..932c189 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src"], + "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"], "compilerOptions": { "module": "CommonJS", @@ -8,6 +8,7 @@ "rootDir": "src", "sourceMap": true, "strict": true, + "importHelpers": true, "esModuleInterop": true, "moduleResolution": "Node", "resolveJsonModule": true,