diff --git a/README.md b/README.md index 65e652cd..ff34cbd6 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ AMQP is a form on offline processing where the payload will be published on an A 1. A signer process subscribes to `Requests`. For each message it generates a signature, and publishes a message on `Signatures` 1. A submitter process subscribes to `Signatures` and submits to the chain. It publishes to `Finalizations`, for consumer applications to subscribe to -To use AMQP mode a message broker must be configured. The implementation assumes [ArtemisMQ](https://activemq.apache.org/components/artemis/) is used, with an AMQP acceptor. In theory any AMQP 1.0 compliant broker should work though. +When using AMQP mode a message broker should be configured. The current implementation assumes [ArtemisMQ](https://activemq.apache.org/components/artemis/) is used, with an AMQP acceptor. Other message queues can be implemented as needed. If using AMQP, it is strongly recommended to use a persistent data store (i.e postgres). There are two tables related to AMQP processing: `offline_tx` and `offline_event`: - `offline_tx` is a table for the submitter process. This provides a convenient way to query submitted transactions, and to detect ones rejected by the chain for some reason @@ -176,6 +176,8 @@ If using AMQP, it is strongly recommended to use a persistent data store (i.e po If using the project's compose file, an Artemis console will be exposed on `:8181` with `artemis` being both username and password. +If artemis config values are not set, then an in memory implementation will be defaulted to. This mode is not recommended for use in production environments since the messages are ephemeral. + ### Webhooks (alpha) Normally the endpoints that create transactions wait for block finalization before returning a response, which normally takes around 15 seconds. When processMode `submitWithCallback` is used the `webhookUrl` param must also be provided. The server will respond after submitting the transaction to the mempool with 202 (Accepted) status code instead of the usual 201 (Created). diff --git a/src/accounts/accounts.controller.ts b/src/accounts/accounts.controller.ts index 0b57d4f6..10d702e0 100644 --- a/src/accounts/accounts.controller.ts +++ b/src/accounts/accounts.controller.ts @@ -309,7 +309,7 @@ export class AccountsController { @ApiOperation({ summary: 'Get Account details', description: - 'This endpoint retrieves the Account details for the given Account address. This includes the associated Identity DID, primary account for that Identity and Secondary Accounts with the Permissions and the Subsidy details', + 'This endpoint retrieves the Account details for the given Account address. This includes the associated Identity DID, primary account for that Identity and Secondary Accounts with the Permissions and the Subsidy details', }) @ApiParam({ name: 'account', diff --git a/src/accounts/models/account-details.model.ts b/src/accounts/models/account-details.model.ts index 04ca5f0c..84c57a16 100644 --- a/src/accounts/models/account-details.model.ts +++ b/src/accounts/models/account-details.model.ts @@ -15,7 +15,8 @@ export class AccountDetailsModel { readonly identity?: IdentityModel; @ApiPropertyOptional({ - description: 'MultiSig Account details', + description: + 'The MultiSig for which the account is a signer for. Will not be set if the account is not a MultiSig signer', type: MultiSigDetailsModel, }) @Type(() => MultiSigDetailsModel) diff --git a/src/accounts/models/multi-sig-details.model.ts b/src/accounts/models/multi-sig-details.model.ts index 6893b40b..02972c3b 100644 --- a/src/accounts/models/multi-sig-details.model.ts +++ b/src/accounts/models/multi-sig-details.model.ts @@ -9,7 +9,7 @@ import { SignerModel } from '~/identities/models/signer.model'; export class MultiSigDetailsModel { @ApiProperty({ - description: 'Secondary accounts with permissions', + description: 'Signing accounts for the multiSig', isArray: true, type: SignerModel, }) diff --git a/src/app.module.ts b/src/app.module.ts index b74d3319..0840161d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,6 @@ import { ConfigModule } from '@nestjs/config'; import Joi from 'joi'; import { AccountsModule } from '~/accounts/accounts.module'; -import { ArtemisModule } from '~/artemis/artemis.module'; import { AssetsModule } from '~/assets/assets.module'; import { AuthModule } from '~/auth/auth.module'; import { AuthStrategy } from '~/auth/strategies/strategies.consts'; @@ -102,15 +101,10 @@ import { UsersModule } from '~/users/users.module'; MetadataModule, SubsidyModule, NftsModule, - ...(process.env.ARTEMIS_HOST - ? [ - ArtemisModule, - OfflineSignerModule, - OfflineSubmitterModule, - OfflineStarterModule, - OfflineRecorderModule, - ] - : []), + OfflineSignerModule, + OfflineSubmitterModule, + OfflineStarterModule, + OfflineRecorderModule, ], }) export class AppModule {} diff --git a/src/artemis/artemis.module.ts b/src/artemis/artemis.module.ts deleted file mode 100644 index 0ebb1ac5..00000000 --- a/src/artemis/artemis.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* istanbul ignore file */ - -import { Module } from '@nestjs/common'; - -import { ArtemisService } from '~/artemis/artemis.service'; -import { LoggerModule } from '~/logger/logger.module'; - -@Module({ - imports: [LoggerModule], - providers: [ArtemisService], - exports: [ArtemisService], -}) -export class ArtemisModule {} diff --git a/src/common/utils/amqp.ts b/src/common/utils/amqp.ts index c7c22fc0..502e1e66 100644 --- a/src/common/utils/amqp.ts +++ b/src/common/utils/amqp.ts @@ -14,8 +14,8 @@ export enum QueueName { EventsLog = 'EventsLog', Requests = 'Requests', - SignerRequests = 'SignerRequests', - SubmitterRequests = 'SubmitterRequests', Signatures = 'Signatures', + + Finalizations = 'Finalizations', } diff --git a/src/datastore/interfaces/postgres-config.interface.ts b/src/datastore/interfaces/postgres-config.interface.ts deleted file mode 100644 index ab393789..00000000 --- a/src/datastore/interfaces/postgres-config.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* istanbul ignore file */ - -export interface PostgresConfig { - type: 'postgres'; - host: string; - username: string; - password: string; - database: string; - port: number; -} diff --git a/src/message/artemis/artemis.module.ts b/src/message/artemis/artemis.module.ts new file mode 100644 index 00000000..378cc0ef --- /dev/null +++ b/src/message/artemis/artemis.module.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +import { Module } from '@nestjs/common'; + +import { LoggerModule } from '~/logger/logger.module'; +import { ArtemisService } from '~/message/artemis/artemis.service'; +import { MessageService } from '~/message/common/message.service'; + +@Module({ + imports: [LoggerModule], + providers: [{ provide: MessageService, useClass: ArtemisService }], + exports: [MessageService], +}) +export class ArtemisModule {} diff --git a/src/artemis/artemis.service.spec.ts b/src/message/artemis/artemis.service.spec.ts similarity index 77% rename from src/artemis/artemis.service.spec.ts rename to src/message/artemis/artemis.service.spec.ts index df959fed..f921d79d 100644 --- a/src/artemis/artemis.service.spec.ts +++ b/src/message/artemis/artemis.service.spec.ts @@ -1,14 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { IsString } from 'class-validator'; import { when } from 'jest-when'; import { EventContext } from 'rhea-promise'; -import { ArtemisService } from '~/artemis/artemis.service'; +import { AppInternalError } from '~/common/errors'; import { clearEventLoop } from '~/common/utils'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { ArtemisService } from '~/message/artemis/artemis.service'; +import { ArtemisConfig } from '~/message/artemis/utils'; +import { MessageService } from '~/message/common/message.service'; const mockSend = jest.fn(); const mockConnectionClose = jest.fn(); @@ -68,31 +72,60 @@ jest.mock('rhea-promise', () => { }; }); +const mockConfig: ArtemisConfig = { + type: 'artemis', + port: 1, + username: 'someUser', + password: 'somePassword', + host: 'http://example.com', + operationTimeoutInSeconds: 10, + transport: 'tcp', + configured: true, +}; + +const mockReceipt = { id: 1 }; +const otherMockReceipt = { id: 2 }; + +describe('ArtemisService generic test suite', () => { + const logger = createMock(); + + const service = new ArtemisService(logger, mockConfig); + + mockSend.mockResolvedValue(mockReceipt); + + MessageService.test(service); +}); + describe('ArtemisService', () => { let service: ArtemisService; let logger: DeepMocked; - let configSpy: jest.SpyInstance; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ArtemisService, mockPolymeshLoggerProvider], + providers: [ + ArtemisService, + mockPolymeshLoggerProvider, + { provide: 'ARTEMIS_CONFIG', useValue: mockConfig }, + ], }).compile(); service = module.get(ArtemisService); - configSpy = jest.spyOn(service, 'isConfigured'); - configSpy.mockReturnValue(true); logger = module.get(PolymeshLogger); + mockSend.mockResolvedValue(mockReceipt); }); it('should be defined', () => { expect(service).toBeDefined(); }); + it('should throw an error if constructed with unconfigured config', () => { + expect(() => new ArtemisService(logger, { configured: false, type: 'artemis' })).toThrow( + AppInternalError + ); + }); + describe('sendMessage', () => { it('should send a message', async () => { - const mockReceipt = 'mockReceipt'; - const otherMockReceipt = 'otherMockReceipt'; - const topicName = AddressName.Requests; const body = { payload: 'some payload' }; const otherBody = { other: 'payload' }; @@ -103,10 +136,12 @@ describe('ArtemisService', () => { .mockResolvedValue(otherMockReceipt); const receipt = await service.sendMessage(topicName, body); - expect(receipt).toEqual(mockReceipt); + expect(receipt).toEqual(expect.objectContaining({ id: new BigNumber(mockReceipt.id) })); const otherReceipt = await service.sendMessage(topicName, otherBody); - expect(otherReceipt).toEqual(otherMockReceipt); + expect(otherReceipt).toEqual( + expect.objectContaining({ id: new BigNumber(otherMockReceipt.id) }) + ); }); }); @@ -169,14 +204,5 @@ describe('ArtemisService', () => { expect(logger.error).toHaveBeenCalled(); }); - - it('should do no work if service is not configured', async () => { - configSpy.mockReturnValue(false); - - const initialCallCount = mockConnectionClose.mock.calls.length; - await service.onApplicationShutdown(); - - expect(mockConnectionClose.mock.calls.length).toEqual(initialCallCount); - }); }); }); diff --git a/src/artemis/artemis.service.ts b/src/message/artemis/artemis.service.ts similarity index 86% rename from src/artemis/artemis.service.ts rename to src/message/artemis/artemis.service.ts index c8da350e..54f6d33c 100644 --- a/src/artemis/artemis.service.ts +++ b/src/message/artemis/artemis.service.ts @@ -1,4 +1,5 @@ -import { Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { validate } from 'class-validator'; import { AwaitableSender, @@ -6,7 +7,6 @@ import { Connection, ConnectionOptions, Container, - Delivery, EventContext, Receiver, ReceiverEvents, @@ -17,8 +17,9 @@ import { import { AppInternalError } from '~/common/errors'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; - -type EventHandler = (params: T) => Promise; +import { ArtemisConfig } from '~/message/artemis/types'; +import { MessageReceipt } from '~/message/common'; +import { EventHandler, MessageService } from '~/message/common/message.service'; interface AddressEntry { addressName: AddressName; @@ -28,17 +29,24 @@ interface AddressEntry { type AddressStore = Record; @Injectable() -export class ArtemisService implements OnApplicationShutdown { +export class ArtemisService extends MessageService implements OnApplicationShutdown { private receivers: Receiver[] = []; private addressStore: Partial = {}; private connectionPromise?: Promise; - constructor(private readonly logger: PolymeshLogger) { + constructor( + private readonly logger: PolymeshLogger, + @Inject('ARTEMIS_CONFIG') private readonly config: ArtemisConfig + ) { + super(); this.logger.setContext(ArtemisService.name); - } + this.logger.debug('Artemis service initialized'); - public isConfigured(): boolean { - return !!process.env.ARTEMIS_HOST; + if (!this.config.configured) { + throw new AppInternalError( + 'Artemis service was constructed, but Artemis config values were not set' + ); + } } public async onApplicationShutdown(signal?: string | undefined): Promise { @@ -46,10 +54,6 @@ export class ArtemisService implements OnApplicationShutdown { `artemis service received application shutdown request, sig: ${signal} - now closing connections` ); - if (!this.isConfigured()) { - return; - } - const closePromises = [ ...this.receivers.map(receiver => receiver.close()), ...this.addressEntries().map(entry => entry.sender.close()), @@ -88,16 +92,7 @@ export class ArtemisService implements OnApplicationShutdown { } private connectOptions(): ConnectionOptions { - const { ARTEMIS_HOST, ARTEMIS_USERNAME, ARTEMIS_PASSWORD, ARTEMIS_PORT } = process.env; - - return { - port: Number(ARTEMIS_PORT), - host: ARTEMIS_HOST, - username: ARTEMIS_USERNAME, - password: ARTEMIS_PASSWORD, - operationTimeoutInSeconds: 10, - transport: 'tcp', - }; + return this.config as ConnectionOptions; } private sendOptions(): AwaitableSendOptions { @@ -139,14 +134,19 @@ export class ArtemisService implements OnApplicationShutdown { }; } - public async sendMessage(publishOn: AddressName, body: unknown): Promise { + public async sendMessage(publishOn: AddressName, body: unknown): Promise { const { sender } = await this.getAddress(publishOn); const message = { body }; const sendOptions = this.sendOptions(); this.logger.debug(`sending message on: ${publishOn}`); - return sender.send(message, sendOptions); + const receipt = await sender.send(message, sendOptions); + + return { + id: new BigNumber(receipt.id), + topic: publishOn, + }; } /** @@ -206,6 +206,7 @@ export class ArtemisService implements OnApplicationShutdown { }); receiver.on(ReceiverEvents.receiverError, (context: EventContext) => { + /* istanbul ignore next */ const receiverError = context?.receiver?.error; this.logger.error(`an error occurred for receiver "${listenOn}": ${receiverError}`); }); diff --git a/src/message/artemis/types.ts b/src/message/artemis/types.ts new file mode 100644 index 00000000..906db5a2 --- /dev/null +++ b/src/message/artemis/types.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +export type ArtemisConfig = + | { + type: 'artemis'; + port: number; + host: string; + username: string; + password: string; + operationTimeoutInSeconds: number; + transport: string; + configured: true; + } + | { type: 'artemis'; configured: false }; diff --git a/src/message/artemis/utils.ts b/src/message/artemis/utils.ts new file mode 100644 index 00000000..3b5c7adf --- /dev/null +++ b/src/message/artemis/utils.ts @@ -0,0 +1,41 @@ +/* istanbul ignore file */ + +export type ArtemisConfig = + | { + type: 'artemis'; + port: number; + host: string; + username: string; + password: string; + operationTimeoutInSeconds: number; + transport: string; + configured: true; + } + | { + type: 'artemis'; + configured: false; + }; + +export const readArtemisFromEnv = (): ArtemisConfig => { + const { + ARTEMIS_HOST: host, + ARTEMIS_PORT: port, + ARTEMIS_USERNAME: username, + ARTEMIS_PASSWORD: password, + } = process.env; + + if (!host || !port || !username || !password) { + return { type: 'artemis', configured: false }; + } + + return { + type: 'artemis', + host, + username, + port: Number(port), + password, + operationTimeoutInSeconds: 10, + transport: 'tcp', + configured: true, + }; +}; diff --git a/src/message/common/index.ts b/src/message/common/index.ts new file mode 100644 index 00000000..808ab8e2 --- /dev/null +++ b/src/message/common/index.ts @@ -0,0 +1,3 @@ +/* eslint-disable no-restricted-imports */ +export * from './message.service'; +export * from './types'; diff --git a/src/message/common/message.service.suite.ts b/src/message/common/message.service.suite.ts new file mode 100644 index 00000000..6b888ee8 --- /dev/null +++ b/src/message/common/message.service.suite.ts @@ -0,0 +1,18 @@ +/* istanbul ignore file */ + +import { AddressName, QueueName } from '~/common/utils/amqp'; +import { MessageService } from '~/message/common/message.service'; +import { AnyModel } from '~/offline-recorder/model/any.model'; + +export const testMessageService = async (service: MessageService): Promise => { + describe('sending and receiving messages', () => { + it('should register a listener', async () => { + const mockListener = jest.fn(); + await service.registerListener(QueueName.Requests, mockListener, AnyModel); + + await service.sendMessage(AddressName.Requests, { someKey: 'someValue' }); + + expect(mockListener).toHaveBeenCalled(); + }); + }); +}; diff --git a/src/message/common/message.service.ts b/src/message/common/message.service.ts new file mode 100644 index 00000000..0726cdb2 --- /dev/null +++ b/src/message/common/message.service.ts @@ -0,0 +1,21 @@ +import { QueueName } from '~/common/utils/amqp'; +import { MessageReceipt } from '~/message/common'; +import { testMessageService } from '~/message/common/message.service.suite'; + +export type EventHandler = (params: T) => Promise; + +export abstract class MessageService { + public abstract sendMessage(topic: string, message: unknown): Promise; + public abstract registerListener( + listenOn: QueueName, + listener: EventHandler, + Model: new (params: T) => T + ): Promise; + + /** + * a set of tests implementations should pass + */ + public static async test(service: MessageService): Promise { + return testMessageService(service); + } +} diff --git a/src/message/common/types.ts b/src/message/common/types.ts new file mode 100644 index 00000000..bbac2ff4 --- /dev/null +++ b/src/message/common/types.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ + +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; + +export interface MessageReceipt { + id: BigNumber; + topic: string; +} diff --git a/src/message/local/address-to-queue.ts b/src/message/local/address-to-queue.ts new file mode 100644 index 00000000..adb065bf --- /dev/null +++ b/src/message/local/address-to-queue.ts @@ -0,0 +1,7 @@ +import { AddressName, QueueName } from '~/common/utils/amqp'; + +export const addressToQueue: Record = { + [AddressName.Requests]: QueueName.Requests, + [AddressName.Signatures]: QueueName.Signatures, + [AddressName.Finalizations]: QueueName.Finalizations, +}; diff --git a/src/message/local/local-message.module.ts b/src/message/local/local-message.module.ts new file mode 100644 index 00000000..b36a6183 --- /dev/null +++ b/src/message/local/local-message.module.ts @@ -0,0 +1,17 @@ +/* istanbul ignore file */ + +import { Module } from '@nestjs/common'; + +import { LoggerModule } from '~/logger/logger.module'; +import { MessageService } from '~/message/common/message.service'; +import { LocalMessageService } from '~/message/local/local-message.service'; + +/** + * provides a local message service + */ +@Module({ + imports: [LoggerModule], + providers: [{ provide: MessageService, useClass: LocalMessageService }], + exports: [MessageService], +}) +export class LocalMessageModule {} diff --git a/src/message/local/local-message.service.spec.ts b/src/message/local/local-message.service.spec.ts new file mode 100644 index 00000000..6b609ea3 --- /dev/null +++ b/src/message/local/local-message.service.spec.ts @@ -0,0 +1,48 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { AppInternalError } from '~/common/errors'; +import { AddressName, QueueName } from '~/common/utils/amqp'; +import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { MessageService } from '~/message/common/message.service'; +import { LocalMessageService } from '~/message/local/local-message.service'; +import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; + +describe('LocalMessageService generic test suite', () => { + const logger = createMock(); + const service = new LocalMessageService(logger); + + MessageService.test(service); +}); + +describe('LocalMessageService', () => { + let service: LocalMessageService; + let logger: DeepMocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LocalMessageService, mockPolymeshLoggerProvider], + }).compile(); + + service = module.get(LocalMessageService); + logger = module.get(PolymeshLogger); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should extend MessageService', () => { + expect(service).toBeInstanceOf(MessageService); + }); + + it('should validate messages', async () => { + const mockListener = jest.fn(); + await service.registerListener(QueueName.Requests, mockListener, OfflineRequestModel); + + await expect( + service.sendMessage(AddressName.Requests, { someKey: 'someValue' }) + ).rejects.toThrow(AppInternalError); + }); +}); diff --git a/src/message/local/local-message.service.ts b/src/message/local/local-message.service.ts new file mode 100644 index 00000000..135c5692 --- /dev/null +++ b/src/message/local/local-message.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { BigNumber } from '@polymeshassociation/polymesh-sdk'; +import { validate } from 'class-validator'; + +import { AppInternalError } from '~/common/errors'; +import { AddressName, QueueName } from '~/common/utils/amqp'; +import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { MessageReceipt } from '~/message/common'; +import { EventHandler, MessageService } from '~/message/common/message.service'; +import { addressToQueue } from '~/message/local/address-to-queue'; + +@Injectable() +export class LocalMessageService extends MessageService { + private listeners: Record[]>; + private _id = 1; + + constructor(private readonly logger: PolymeshLogger) { + super(); + this.logger.setContext(LocalMessageService.name); + + this.listeners = {} as Record[]>; + Object.values(QueueName).forEach(queue => (this.listeners[queue] = [])); + } + + async sendMessage(publishOn: AddressName, message: unknown): Promise { + this.logger.debug(`sending msg on ${publishOn}`); + const queue = addressToQueue[publishOn]; + const handlers = [...this.listeners[queue], ...this.listeners[QueueName.EventsLog]]; + + await Promise.all(handlers.map(handler => handler(message))); + + return { id: new BigNumber(this._id++), topic: publishOn }; + } + + public async registerListener( + listenOn: QueueName, + listener: EventHandler, + Model: new (params: T) => T + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handler = async (message: any): Promise => { + this.logger.debug(`received message for: ${listenOn}`); + const model = new Model(message); + const validationErrors = await validate(model); + + if (validationErrors.length) { + const errString = JSON.stringify(validationErrors); + this.logger.error(`validation errors for "${listenOn}": ${errString}`); + + throw new AppInternalError(`received invalid message on "${listenOn}": ${errString}`); + } + + listener(model); + }; + + this.listeners[listenOn].push(handler); + } +} diff --git a/src/message/message.module.ts b/src/message/message.module.ts new file mode 100644 index 00000000..b98f0488 --- /dev/null +++ b/src/message/message.module.ts @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { DynamicModule, Module } from '@nestjs/common'; + +import { ArtemisModule } from '~/message/artemis/artemis.module'; +import { readArtemisFromEnv } from '~/message/artemis/utils'; +import { LocalMessageModule } from '~/message/local/local-message.module'; + +/** + * responsible for selecting a module to manage messages + * + * @note defaults to LocalMessageModule + */ +@Module({}) +export class MessageModule { + public static registerAsync(): DynamicModule { + const artemisConfig = readArtemisFromEnv(); + + if (artemisConfig.configured) { + return { + providers: [{ provide: 'ARTEMIS_CONFIG', useValue: artemisConfig }], + module: ArtemisModule, + exports: [ArtemisModule], + }; + } else { + return { + module: LocalMessageModule, + exports: [LocalMessageModule], + }; + } + } +} diff --git a/src/offline-recorder/offline-recorder.module.ts b/src/offline-recorder/offline-recorder.module.ts index cb3e260b..27975df0 100644 --- a/src/offline-recorder/offline-recorder.module.ts +++ b/src/offline-recorder/offline-recorder.module.ts @@ -2,13 +2,13 @@ import { Module } from '@nestjs/common'; -import { ArtemisModule } from '~/artemis/artemis.module'; import { DatastoreModule } from '~/datastore/datastore.module'; import { LoggerModule } from '~/logger/logger.module'; +import { MessageModule } from '~/message/message.module'; import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; @Module({ - imports: [ArtemisModule, DatastoreModule.registerAsync(), LoggerModule], + imports: [LoggerModule, MessageModule.registerAsync(), DatastoreModule.registerAsync()], providers: [OfflineRecorderService], }) export class OfflineRecorderModule {} diff --git a/src/offline-recorder/offline-recorder.service.spec.ts b/src/offline-recorder/offline-recorder.service.spec.ts index fbc03cb1..7b4d1b54 100644 --- a/src/offline-recorder/offline-recorder.service.spec.ts +++ b/src/offline-recorder/offline-recorder.service.spec.ts @@ -1,30 +1,30 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ArtemisService } from '~/artemis/artemis.service'; import { QueueName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { MessageService } from '~/message/common/message.service'; import { OfflineRecorderService } from '~/offline-recorder/offline-recorder.service'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; -import { mockArtemisServiceProvider, mockOfflineRepoProvider } from '~/test-utils/service-mocks'; +import { mockMessageServiceProvider, mockOfflineRepoProvider } from '~/test-utils/service-mocks'; describe('OfflineRecorderService', () => { let service: OfflineRecorderService; let mockOfflineRepo: DeepMocked; - let mockArtemisService: DeepMocked; + let mockMessageService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ OfflineRecorderService, - mockArtemisServiceProvider, + mockMessageServiceProvider, mockOfflineRepoProvider, mockPolymeshLoggerProvider, ], }).compile(); mockOfflineRepo = module.get(OfflineEventRepo); - mockArtemisService = module.get(ArtemisService); + mockMessageService = module.get(MessageService); service = module.get(OfflineRecorderService); }); @@ -34,7 +34,7 @@ describe('OfflineRecorderService', () => { describe('constructor', () => { it('should have subscribed to the required topics', () => { - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + expect(mockMessageService.registerListener).toHaveBeenCalledWith( QueueName.EventsLog, expect.any(Function), expect.any(Function) diff --git a/src/offline-recorder/offline-recorder.service.ts b/src/offline-recorder/offline-recorder.service.ts index 35ccb15b..cb1647ec 100644 --- a/src/offline-recorder/offline-recorder.service.ts +++ b/src/offline-recorder/offline-recorder.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ArtemisService } from '~/artemis/artemis.service'; import { QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { MessageService } from '~/message/common/message.service'; import { AnyModel } from '~/offline-recorder/model/any.model'; import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; @@ -12,13 +12,13 @@ import { OfflineEventRepo } from '~/offline-recorder/repo/offline-event.repo'; @Injectable() export class OfflineRecorderService { constructor( - private readonly artemisService: ArtemisService, + private readonly messageService: MessageService, private readonly offlineRepo: OfflineEventRepo, private readonly logger: PolymeshLogger ) { this.logger.setContext(OfflineRecorderService.name); - this.artemisService.registerListener( + this.messageService.registerListener( QueueName.EventsLog, /* istanbul ignore next */ msg => this.recordEvent(msg), diff --git a/src/offline-signer/offline-signer.module.ts b/src/offline-signer/offline-signer.module.ts index d63aef9b..085c43cf 100644 --- a/src/offline-signer/offline-signer.module.ts +++ b/src/offline-signer/offline-signer.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; -import { ArtemisModule } from '~/artemis/artemis.module'; import { LoggerModule } from '~/logger/logger.module'; +import { MessageModule } from '~/message/message.module'; import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; import { SigningModule } from '~/signing/signing.module'; @Module({ - imports: [ArtemisModule, SigningModule, LoggerModule], + imports: [MessageModule.registerAsync(), SigningModule, LoggerModule], providers: [OfflineSignerService], }) export class OfflineSignerModule {} diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts index 7eac2cab..4cb8d69c 100644 --- a/src/offline-signer/offline-signer.service.spec.ts +++ b/src/offline-signer/offline-signer.service.spec.ts @@ -2,31 +2,31 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { MessageService } from '~/message/common/message.service'; import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; import { SigningService } from '~/signing/services'; import { mockSigningProvider } from '~/signing/signing.mock'; -import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; +import { mockMessageServiceProvider } from '~/test-utils/service-mocks'; describe('OfflineSignerService', () => { let service: OfflineSignerService; - let mockArtemisService: DeepMocked; + let mockMessageService: DeepMocked; let mockSigningService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ OfflineSignerService, - mockArtemisServiceProvider, + mockMessageServiceProvider, mockSigningProvider, mockPolymeshLoggerProvider, ], }).compile(); - mockArtemisService = module.get(ArtemisService); + mockMessageService = module.get(MessageService); mockSigningService = module.get(SigningService); service = module.get(OfflineSignerService); }); @@ -37,7 +37,7 @@ describe('OfflineSignerService', () => { describe('constructor', () => { it('should have subscribed to the required topics', () => { - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + expect(mockMessageService.registerListener).toHaveBeenCalledWith( AddressName.Requests, expect.any(Function), expect.any(Function) @@ -58,7 +58,7 @@ describe('OfflineSignerService', () => { await service.autoSign(model); - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith(AddressName.Signatures, { + expect(mockMessageService.sendMessage).toHaveBeenCalledWith(AddressName.Signatures, { id: 'someId', signature: mockSignature, payload: expect.any(Object), diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts index 70a2e256..bd855189 100644 --- a/src/offline-signer/offline-signer.service.ts +++ b/src/offline-signer/offline-signer.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { MessageService } from '~/message/common/message.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; import { SigningService } from '~/signing/services'; @@ -13,13 +13,13 @@ import { SigningService } from '~/signing/services'; @Injectable() export class OfflineSignerService { constructor( - private readonly artemisService: ArtemisService, + private readonly messageService: MessageService, private readonly signingService: SigningService, private readonly logger: PolymeshLogger ) { this.logger.setContext(OfflineSignerService.name); - this.artemisService.registerListener( + this.messageService.registerListener( QueueName.Requests, /* istanbul ignore next */ msg => this.autoSign(msg), @@ -38,6 +38,6 @@ export class OfflineSignerService { const model = new OfflineSignatureModel({ signature, id: body.id, payload }); this.logger.log(`signed transaction: ${transactionId}`); - await this.artemisService.sendMessage(AddressName.Signatures, model); + await this.messageService.sendMessage(AddressName.Signatures, model); } } diff --git a/src/offline-starter/offline-starter.module.ts b/src/offline-starter/offline-starter.module.ts index 1582b6b3..d2aad24d 100644 --- a/src/offline-starter/offline-starter.module.ts +++ b/src/offline-starter/offline-starter.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; -import { ArtemisModule } from '~/artemis/artemis.module'; import { LoggerModule } from '~/logger/logger.module'; +import { MessageModule } from '~/message/message.module'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; @Module({ - imports: [ArtemisModule, LoggerModule], + imports: [MessageModule.registerAsync(), LoggerModule], providers: [OfflineStarterService], exports: [OfflineStarterService], }) diff --git a/src/offline-starter/offline-starter.service.spec.ts b/src/offline-starter/offline-starter.service.spec.ts index 29eb7fce..ab01d29b 100644 --- a/src/offline-starter/offline-starter.service.spec.ts +++ b/src/offline-starter/offline-starter.service.spec.ts @@ -2,24 +2,23 @@ import { DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; -import { ArtemisService } from '~/artemis/artemis.service'; -import { AppConfigError } from '~/common/errors'; import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { MessageService } from '~/message/common/message.service'; import { OfflineStarterService } from '~/offline-starter/offline-starter.service'; import { MockPolymeshTransaction } from '~/test-utils/mocks'; -import { mockArtemisServiceProvider } from '~/test-utils/service-mocks'; +import { mockMessageServiceProvider } from '~/test-utils/service-mocks'; describe('OfflineStarterService', () => { let service: OfflineStarterService; - let mockArtemisService: DeepMocked; + let mockMessageService: DeepMocked; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [OfflineStarterService, mockArtemisServiceProvider, mockPolymeshLoggerProvider], + providers: [OfflineStarterService, mockMessageServiceProvider, mockPolymeshLoggerProvider], }).compile(); - mockArtemisService = module.get(ArtemisService); + mockMessageService = module.get(MessageService); service = module.get(OfflineStarterService); }); @@ -34,17 +33,10 @@ describe('OfflineStarterService', () => { it('should submit the transaction to the queue', async () => { await service.beginTransaction(tx, { clientId: 'someId' }); - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( + expect(mockMessageService.sendMessage).toHaveBeenCalledWith( AddressName.Requests, expect.objectContaining({ id: expect.any(String), payload: 'mockPayload' }) ); }); - - it('should throw a config error if artemis is not active', async () => { - mockArtemisService.isConfigured.mockReturnValue(false); - const expectedError = new AppConfigError('artemis', 'service is not configured'); - - expect(service.beginTransaction(tx, { clientId: 'someId' })).rejects.toThrow(expectedError); - }); }); }); diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts index b4bb791e..44593934 100644 --- a/src/offline-starter/offline-starter.service.ts +++ b/src/offline-starter/offline-starter.service.ts @@ -1,19 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { GenericPolymeshTransaction } from '@polymeshassociation/polymesh-sdk/types'; import { randomUUID } from 'crypto'; -import { ArtemisService } from '~/artemis/artemis.service'; -import { AppConfigError } from '~/common/errors'; import { AddressName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { MessageService } from '~/message/common/message.service'; import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; @Injectable() export class OfflineStarterService { constructor( - private readonly artemisService: ArtemisService, + private readonly messageService: MessageService, private readonly logger: PolymeshLogger ) {} @@ -24,10 +22,6 @@ export class OfflineStarterService { transaction: GenericPolymeshTransaction, metadata?: Record ): Promise { - if (!this.artemisService.isConfigured()) { - throw new AppConfigError('artemis', 'service is not configured'); - } - const internalTxId = this.generateTxId(); const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); @@ -38,12 +32,12 @@ export class OfflineStarterService { }); const topicName = AddressName.Requests; - this.logger.debug(`sending topic: ${topicName}`, topicName); - const delivery = await this.artemisService.sendMessage(topicName, request); + this.logger.debug(`sending topic: ${topicName}`); + const { id: deliveryId } = await this.messageService.sendMessage(topicName, request); const model = new OfflineReceiptModel({ id: internalTxId, - deliveryId: new BigNumber(delivery.id), + deliveryId, payload: payload.payload, metadata: payload.metadata, topicName, diff --git a/src/offline-submitter/offline-submitter.module.ts b/src/offline-submitter/offline-submitter.module.ts index d9f3fdf2..5a2d3f7c 100644 --- a/src/offline-submitter/offline-submitter.module.ts +++ b/src/offline-submitter/offline-submitter.module.ts @@ -1,13 +1,18 @@ import { Module } from '@nestjs/common'; -import { ArtemisModule } from '~/artemis/artemis.module'; import { DatastoreModule } from '~/datastore/datastore.module'; import { LoggerModule } from '~/logger/logger.module'; +import { MessageModule } from '~/message/message.module'; import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; import { PolymeshModule } from '~/polymesh/polymesh.module'; @Module({ - imports: [ArtemisModule, DatastoreModule.registerAsync(), PolymeshModule, LoggerModule], + imports: [ + MessageModule.registerAsync(), + DatastoreModule.registerAsync(), + PolymeshModule, + LoggerModule, + ], providers: [OfflineSubmitterService], }) export class OfflineSubmitterModule {} diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts index 53ae4e8e..e51adf37 100644 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -4,9 +4,9 @@ import { BigNumber } from '@polymeshassociation/polymesh-sdk'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; import { when } from 'jest-when'; -import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; +import { MessageService } from '~/message/common/message.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; import { OfflineSubmitterService } from '~/offline-submitter/offline-submitter.service'; @@ -14,14 +14,14 @@ import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; import { PolymeshService } from '~/polymesh/polymesh.service'; import { testValues } from '~/test-utils/consts'; import { mockPolymeshServiceProvider } from '~/test-utils/mocks'; -import { mockArtemisServiceProvider, mockOfflineTxRepoProvider } from '~/test-utils/service-mocks'; +import { mockMessageServiceProvider, mockOfflineTxRepoProvider } from '~/test-utils/service-mocks'; const { offlineTx } = testValues; describe('OfflineSubmitterService', () => { let service: OfflineSubmitterService; let mockRepo: DeepMocked; - let mockArtemisService: DeepMocked; + let mockMessageService: DeepMocked; let mockPolymeshService: DeepMocked; let offlineModel: OfflineTxModel; @@ -29,7 +29,7 @@ describe('OfflineSubmitterService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ OfflineSubmitterService, - mockArtemisServiceProvider, + mockMessageServiceProvider, mockOfflineTxRepoProvider, mockPolymeshServiceProvider, mockPolymeshLoggerProvider, @@ -37,7 +37,7 @@ describe('OfflineSubmitterService', () => { }).compile(); mockRepo = module.get(OfflineTxRepo); - mockArtemisService = module.get(ArtemisService); + mockMessageService = module.get(MessageService); mockPolymeshService = module.get(PolymeshService); service = module.get(OfflineSubmitterService); @@ -57,7 +57,7 @@ describe('OfflineSubmitterService', () => { describe('constructor', () => { it('should have subscribed to the required topics', () => { - expect(mockArtemisService.registerListener).toHaveBeenCalledWith( + expect(mockMessageService.registerListener).toHaveBeenCalledWith( AddressName.Signatures, expect.any(Function), expect.any(Function) @@ -95,7 +95,7 @@ describe('OfflineSubmitterService', () => { txIndex: '1', }) ); - expect(mockArtemisService.sendMessage).toHaveBeenCalledWith( + expect(mockMessageService.sendMessage).toHaveBeenCalledWith( AddressName.Finalizations, expect.objectContaining({ blockHash: '0x02', diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 94d8e5df..7e5329b7 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; +import { MessageService } from '~/message/common/message.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; import { OfflineTxRepo } from '~/offline-submitter/repos/offline-tx.repo'; @@ -14,13 +14,13 @@ import { PolymeshService } from '~/polymesh/polymesh.service'; @Injectable() export class OfflineSubmitterService { constructor( - private readonly artemisService: ArtemisService, + private readonly messageService: MessageService, private readonly polymeshService: PolymeshService, private readonly offlineTxRepo: OfflineTxRepo, private readonly logger: PolymeshLogger ) { this.logger.setContext(OfflineSubmitterService.name); - this.artemisService.registerListener( + this.messageService.registerListener( QueueName.Signatures, /* istanbul ignore next */ msg => this.submit(msg), @@ -62,7 +62,7 @@ export class OfflineSubmitterService { nonce, }; - await this.artemisService.sendMessage(AddressName.Finalizations, finalizationMsg); + await this.messageService.sendMessage(AddressName.Finalizations, finalizationMsg); transaction.blockHash = result.blockHash; transaction.txIndex = result.transactionIndex.toString(); diff --git a/src/test-utils/service-mocks.ts b/src/test-utils/service-mocks.ts index 63e375f5..3ef1337c 100644 --- a/src/test-utils/service-mocks.ts +++ b/src/test-utils/service-mocks.ts @@ -4,12 +4,12 @@ import { createMock } from '@golevelup/ts-jest'; import { ValueProvider } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { ArtemisService } from '~/artemis/artemis.service'; import { AuthService } from '~/auth/auth.service'; import { ClaimsService } from '~/claims/claims.service'; import { ComplianceRequirementsService } from '~/compliance/compliance-requirements.service'; import { TrustedClaimIssuersService } from '~/compliance/trusted-claim-issuers.service'; import { DeveloperTestingService } from '~/developer-testing/developer-testing.service'; +import { MessageService } from '~/message/common/message.service'; import { MetadataService } from '~/metadata/metadata.service'; import { NetworkService } from '~/network/network.service'; import { NftsService } from '~/nfts/nfts.service'; @@ -301,9 +301,9 @@ export const mockClaimsServiceProvider: ValueProvider = { useValue: createMock(), }; -export const mockArtemisServiceProvider: ValueProvider = { - provide: ArtemisService, - useValue: createMock(), +export const mockMessageServiceProvider: ValueProvider = { + provide: MessageService, + useValue: createMock(), }; export const mockOfflineRepoProvider: ValueProvider = {