diff --git a/package-lock.json b/package-lock.json index 998921b5..34bbebc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@digicatapult/sqnc-matchmaker-api", - "version": "3.0.136", + "version": "3.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@digicatapult/sqnc-matchmaker-api", - "version": "3.0.136", + "version": "3.1.0", "license": "Apache-2.0", "dependencies": { "@digicatapult/tsoa-oauth-express": "^0.1.69", "@polkadot/api": "^14.3.1", "@tsoa/runtime": "^6.5.1", + "async-mutex": "^0.5.0", "base-x": "^5.0.0", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -23,6 +24,7 @@ "multer": "^1.4.5-lts.1", "pg": "^8.13.1", "pino": "^9.5.0", + "reflect-metadata": "^0.2.2", "swagger-ui-express": "^5.0.1", "tsoa": "^6.5.1", "tsyringe": "^4.8.0", @@ -3107,6 +3109,15 @@ "node": ">=12" } }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10608,6 +10619,14 @@ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true }, + "async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "requires": { + "tslib": "^2.4.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/package.json b/package.json index 5cb25ee8..bcdb9bae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@digicatapult/sqnc-matchmaker-api", - "version": "3.0.136", + "version": "3.1.0", "description": "An OpenAPI Matchmaking API service for SQNC", "main": "src/index.ts", "type": "module", @@ -75,6 +75,7 @@ "@digicatapult/tsoa-oauth-express": "^0.1.69", "@polkadot/api": "^14.3.1", "@tsoa/runtime": "^6.5.1", + "async-mutex": "^0.5.0", "base-x": "^5.0.0", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -86,6 +87,7 @@ "multer": "^1.4.5-lts.1", "pg": "^8.13.1", "pino": "^9.5.0", + "reflect-metadata": "^0.2.2", "swagger-ui-express": "^5.0.1", "tsoa": "^6.5.1", "tsyringe": "^4.8.0", diff --git a/src/authentication.ts b/src/authentication.ts index ccd4d6d2..dc851e41 100644 --- a/src/authentication.ts +++ b/src/authentication.ts @@ -14,7 +14,7 @@ const exampleOptions: AuthOptions = { const scopes = ((decoded as jwt.JwtPayload).scopes as string) || '' return scopes.split(' ') }, - tryRefreshTokens: (_req: express.Request) => Promise.resolve(false), + tryRefreshTokens: () => Promise.resolve(false), } export const expressAuthentication = mkExpressAuthentication(exampleOptions) diff --git a/src/controllers/v1/_common/demand.ts b/src/controllers/v1/_common/demand.ts index 43b1e2ae..816ee8e5 100644 --- a/src/controllers/v1/_common/demand.ts +++ b/src/controllers/v1/_common/demand.ts @@ -17,32 +17,27 @@ import { BadRequest, NotFound } from '../../../lib/error-handler/index.js' import { TransactionResponse, TransactionType } from '../../../models/transaction.js' import { demandCommentCreate, demandCreate } from '../../../lib/payload.js' import ChainNode from '../../../lib/chainNode.js' -import env from '../../../env.js' import { parseDateParam } from '../../../lib/utils/queryParams.js' import Identity from '../../../lib/services/identity.js' import { getAuthorization } from '../../../lib/utils/shared.js' +let self: { address: string; alias: string } | null = null export class DemandController extends Controller { demandType: 'demandA' | 'demandB' dbDemandSubtype: 'demand_a' | 'demand_b' log: Logger db: Database - node: ChainNode - private identity: Identity - constructor(demandType: 'demandA' | 'demandB', identity: Identity) { + constructor( + demandType: 'demandA' | 'demandB', + private identity: Identity, + private node: ChainNode + ) { super() this.demandType = demandType this.dbDemandSubtype = demandType === 'demandA' ? 'demand_a' : 'demand_b' this.log = logger.child({ controller: `/${this.demandType}` }) this.db = new Database() - this.node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) - this.identity = identity } public async createDemand(req: express.Request, { parametersAttachmentId }: DemandRequest): Promise { @@ -52,7 +47,8 @@ export class DemandController extends Controller { throw new BadRequest('Attachment not found') } - const res = await this.identity.getMemberBySelf(getAuthorization(req)) + const res = self || (await this.identity.getMemberBySelf(getAuthorization(req))) + self = res const selfAddress = res.address const selfAlias = res.alias diff --git a/src/controllers/v1/attachment/index.ts b/src/controllers/v1/attachment/index.ts index 9dbf1671..282ccb71 100644 --- a/src/controllers/v1/attachment/index.ts +++ b/src/controllers/v1/attachment/index.ts @@ -161,7 +161,8 @@ export class attachment extends Controller { const json = JSON.parse(blobBuffer.toString()) return json } catch (err) { - this.log.warn(`Unable to parse json file for attachment ${id}`) + this.log.warn('Unable to parse json file for attachment %s', id) + this.log.debug('Parse error: %s', err instanceof Error ? err.message : 'unknown') return this.octetResponse(blobBuffer, filename) } } diff --git a/src/controllers/v1/demandA/index.ts b/src/controllers/v1/demandA/index.ts index 96ef4394..8658b28d 100644 --- a/src/controllers/v1/demandA/index.ts +++ b/src/controllers/v1/demandA/index.ts @@ -27,14 +27,15 @@ import { BadRequest, NotFound } from '../../../lib/error-handler/index.js' import { TransactionResponse } from '../../../models/transaction.js' import { DemandController } from '../_common/demand.js' import Identity from '../../../lib/services/identity.js' +import ChainNode from '../../../lib/chainNode.js' @Route('v1/demandA') @injectable() @Tags('demandA') @Security('oauth2') export class DemandAController extends DemandController { - constructor(identity: Identity) { - super('demandA', identity) + constructor(identity: Identity, node: ChainNode) { + super('demandA', identity, node) } /** diff --git a/src/controllers/v1/demandB/index.ts b/src/controllers/v1/demandB/index.ts index 5324f094..08e28c26 100644 --- a/src/controllers/v1/demandB/index.ts +++ b/src/controllers/v1/demandB/index.ts @@ -27,14 +27,15 @@ import { BadRequest, NotFound } from '../../../lib/error-handler/index.js' import { TransactionResponse } from '../../../models/transaction.js' import { DemandController } from '../_common/demand.js' import Identity from '../../../lib/services/identity.js' +import ChainNode from '../../../lib/chainNode.js' @Route('v1/demandB') @injectable() @Tags('demandB') @Security('oauth2') export class DemandBController extends DemandController { - constructor(identity: Identity) { - super('demandB', identity) + constructor(identity: Identity, node: ChainNode) { + super('demandB', identity, node) } /** diff --git a/src/controllers/v1/match2/index.ts b/src/controllers/v1/match2/index.ts index 97be0fde..d16d83b6 100644 --- a/src/controllers/v1/match2/index.ts +++ b/src/controllers/v1/match2/index.ts @@ -42,7 +42,6 @@ import { rematch2AcceptFinal, } from '../../../lib/payload.js' import ChainNode from '../../../lib/chainNode.js' -import env from '../../../env.js' import { parseDateParam } from '../../../lib/utils/queryParams.js' import { getAuthorization } from '../../../lib/utils/shared.js' @@ -53,19 +52,14 @@ import { getAuthorization } from '../../../lib/utils/shared.js' export class Match2Controller extends Controller { log: Logger db: Database - node: ChainNode - constructor(private identity: Identity) { + constructor( + private identity: Identity, + private node: ChainNode + ) { super() this.log = logger.child({ controller: '/match2' }) this.db = new Database() - this.node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) - this.identity = identity } /** diff --git a/src/env.ts b/src/env.ts index 86a02d25..55605142 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,5 +1,6 @@ import * as envalid from 'envalid' import dotenv from 'dotenv' +import { container } from 'tsyringe' if (process.env.NODE_ENV === 'test') { dotenv.config({ path: 'test/test.env' }) @@ -7,7 +8,7 @@ if (process.env.NODE_ENV === 'test') { dotenv.config() } -export default envalid.cleanEnv(process.env, { +const env = envalid.cleanEnv(process.env, { PORT: envalid.port({ default: 3000 }), LOG_LEVEL: envalid.str({ default: 'info', devDefault: 'debug' }), DB_HOST: envalid.str({ devDefault: 'localhost' }), @@ -42,3 +43,12 @@ export default envalid.cleanEnv(process.env, { default: '/certs', }), }) + +export default env + +export const EnvToken = Symbol('Env') +export type Env = typeof env + +container.register(EnvToken, { + useValue: env, +}) diff --git a/src/index.ts b/src/index.ts index 24987e09..0fc989e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,7 @@ +import 'reflect-metadata' + import { Express } from 'express' +import { container } from 'tsyringe' import Indexer from './lib/indexer/index.js' import ChainNode from './lib/chainNode.js' @@ -10,12 +13,7 @@ import { logger } from './lib/logger.js' const app: Express = await Server() if (env.ENABLE_INDEXER) { - const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) + const node = container.resolve(ChainNode) const indexer = new Indexer({ db: new Database(), logger, node }) await indexer.start() diff --git a/src/ioc.ts b/src/ioc.ts index 2eedd18f..19d6653d 100644 --- a/src/ioc.ts +++ b/src/ioc.ts @@ -1,8 +1,18 @@ import { IocContainer } from '@tsoa/runtime' import { container } from 'tsyringe' +import { Logger } from 'pino' + +import env, { type Env, EnvToken } from './env.js' +import { logger, LoggerToken } from './lib/logger.js' export const iocContainer: IocContainer = { get: (controller: { prototype: T }): T => { return container.resolve(controller as never) }, } + +export function resetContainer() { + container.reset() + container.register(EnvToken, { useValue: env }) + container.register(LoggerToken, { useValue: logger }) +} diff --git a/src/lib/chainNode.ts b/src/lib/chainNode.ts index 5771e7b6..f48d78f3 100644 --- a/src/lib/chainNode.ts +++ b/src/lib/chainNode.ts @@ -1,8 +1,10 @@ -import { Logger } from 'pino' +import { type Logger } from 'pino' import { ApiPromise, WsProvider, Keyring, SubmittableResult } from '@polkadot/api' import { blake2AsHex } from '@polkadot/util-crypto' import { SubmittableExtrinsic } from '@polkadot/api/types' import type { u128 } from '@polkadot/types' +import { inject, singleton } from 'tsyringe' +import { Mutex } from 'async-mutex' import { serviceState } from './service-watcher/statusPoll.js' import { TransactionState } from '../models/transaction.js' @@ -10,6 +12,8 @@ import type { Payload, Output, Metadata } from './payload.js' import { HEX } from '../models/strings.js' import { hexToBs58 } from '../utils/hex.js' import { trim0x } from './utils/shared.js' +import { LoggerToken } from './logger.js' +import { type Env, EnvToken } from '../env.js' const processRanTopic = blake2AsHex('utxoNFT.ProcessRan') @@ -48,6 +52,7 @@ type EventData = } | undefined +@singleton() export default class ChainNode { private provider: WsProvider private api: ApiPromise @@ -55,11 +60,12 @@ export default class ChainNode { private logger: Logger private userUri: string private lastSubmittedNonce: number + private mutex = new Mutex() - constructor({ host, port, logger, userUri }: NodeCtorConfig) { + constructor(@inject(LoggerToken) logger: Logger, @inject(EnvToken) env: Env) { this.logger = logger.child({ module: 'ChainNode' }) - this.provider = new WsProvider(`ws://${host}:${port}`) - this.userUri = userUri + this.provider = new WsProvider(`ws://${env.NODE_HOST}:${env.NODE_PORT}`) + this.userUri = env.USER_URI this.api = new ApiPromise({ provider: this.provider }) this.keyring = new Keyring({ type: 'sr25519' }) this.lastSubmittedNonce = -1 @@ -69,11 +75,11 @@ export default class ChainNode { }) this.api.on('disconnected', () => { - this.logger.warn(`Disconnected from substrate node at ${host}:${port}`) + this.logger.warn(`Disconnected from substrate node at ${env.NODE_HOST}:${env.NODE_PORT}`) }) this.api.on('connected', () => { - this.logger.info(`Connected to substrate node at ${host}:${port}`) + this.logger.info(`Connected to substrate node at ${env.NODE_HOST}:${env.NODE_PORT}`) }) this.api.on('error', (err) => { @@ -135,9 +141,14 @@ export default class ChainNode { await this.api.isReady const extrinsic = this.api.tx.utxoNFT.runProcess(process, inputs, outputsAsMaps) const account = this.keyring.addFromUri(this.userUri) - const nextTxPoolNonce = (await this.api.rpc.system.accountNextIndex(account.publicKey)).toNumber() - const nonce = Math.max(nextTxPoolNonce, this.lastSubmittedNonce + 1) - this.lastSubmittedNonce = nonce + + const nonce = await this.mutex.runExclusive(async () => { + const nextTxPoolNonce = (await this.api.rpc.system.accountNextIndex(account.publicKey)).toNumber() + const nonce = Math.max(nextTxPoolNonce, this.lastSubmittedNonce + 1) + this.lastSubmittedNonce = nonce + return nonce + }) + const signed = await extrinsic.signAsync(account, { nonce }) return signed } diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 03cffa89..d10959af 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -287,7 +287,7 @@ export default class Database { .returning(transactionColumns) } - getTransaction = async (id: UUID) => { + getTransaction = async (id: UUID): Promise<[Transaction] | []> => { return this.db().transaction().select(transactionColumns).where({ id }) } diff --git a/src/lib/logger.ts b/src/lib/logger.ts index e2d8a69c..64ff8efe 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -1,6 +1,7 @@ import { pino, Logger } from 'pino' import env from '../env.js' +import { container } from 'tsyringe' export const logger: Logger = pino( { @@ -10,3 +11,6 @@ export const logger: Logger = pino( }, process.stdout ) + +export const LoggerToken = Symbol('Logger') +container.register(LoggerToken, { useValue: logger }) diff --git a/src/lib/service-watcher/apiStatus.ts b/src/lib/service-watcher/apiStatus.ts index 1f437260..6f87d2f5 100644 --- a/src/lib/service-watcher/apiStatus.ts +++ b/src/lib/service-watcher/apiStatus.ts @@ -1,15 +1,10 @@ import { startStatusHandler } from './statusPoll.js' import env from '../../env.js' import ChainNode from '../chainNode.js' -import { logger } from '../logger.js' +import { container } from 'tsyringe' const { WATCHER_POLL_PERIOD_MS, WATCHER_TIMEOUT_MS } = env -const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, -}) +const node = container.resolve(ChainNode) const startApiStatus = () => startStatusHandler({ diff --git a/src/lib/service-watcher/statusPoll.ts b/src/lib/service-watcher/statusPoll.ts index 2a680a5e..c1c0a3f0 100644 --- a/src/lib/service-watcher/statusPoll.ts +++ b/src/lib/service-watcher/statusPoll.ts @@ -1,3 +1,5 @@ +import { logger } from '../logger.js' + export const serviceState = { UP: 'up', DOWN: 'down', @@ -51,6 +53,7 @@ const mkStatusGenerator = async function* ({ } throw new Error('Status is not a valid value') } catch (err) { + logger.debug('Status generator error: %s', err instanceof Error ? err.message : 'unknown') yield { status: serviceState.ERROR, detail: null, diff --git a/src/lib/services/identity.ts b/src/lib/services/identity.ts index 795da187..c503a6b7 100644 --- a/src/lib/services/identity.ts +++ b/src/lib/services/identity.ts @@ -4,6 +4,7 @@ import { z } from 'zod' import { NotFound, HttpResponse } from '../error-handler/index.js' import env from '../../env.js' import { Status, serviceState } from '../service-watcher/statusPoll.js' +import { logger } from '../logger.js' const identityResponseValidator = z.object({ address: z.string(), @@ -46,6 +47,7 @@ export default class Identity { } throw new Error() } catch (err) { + logger.debug('Identity service status error: %s', err instanceof Error ? err.message : 'unknown') return { status: serviceState.DOWN, detail: { diff --git a/test/helper/chainTest.ts b/test/helper/chainTest.ts index 434aa132..b8515412 100644 --- a/test/helper/chainTest.ts +++ b/test/helper/chainTest.ts @@ -7,26 +7,21 @@ import Indexer from '../../src/lib/indexer/index.js' import Database from '../../src/lib/db/index.js' import ChainNode from '../../src/lib/chainNode.js' import { logger } from '../../src/lib/logger.js' -import env from '../../src/env.js' +import { container } from 'tsyringe' const db = new Database() export const withAppAndIndexer = (context: { app: Express; indexer: Indexer }) => { before(async function () { context.app = await createHttpServer() - const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) + const node = container.resolve(ChainNode) const blockHash = await node.getLastFinalisedBlockHash() const blockHeader = await node.getHeader(blockHash) await db .insertProcessedBlock({ hash: blockHash, - height: blockHeader.height, + height: blockHeader.height.toString(10), parent: blockHash, }) .catch(() => { diff --git a/test/helper/poll.ts b/test/helper/poll.ts index d36592c0..9f47921b 100644 --- a/test/helper/poll.ts +++ b/test/helper/poll.ts @@ -1,36 +1,55 @@ import Database from '../../src/lib/db/index.js' -import { TransactionState, TransactionResponse } from '../../src/models/transaction.js' import { UUID } from '../../src/models/strings.js' -export const pollTransactionState = async ( - db: Database, - transactionId: UUID, - targetState: TransactionState, - delay = 1000, - maxRetry = 30 -): Promise => { - let retry = 0 - - const poll = async (): Promise => { - if (retry >= maxRetry) { - throw new Error( - `Maximum number of retries exceeded while waiting for transaction ${transactionId} to reach state ${targetState}` - ) - } +type Method = 'getTransaction' | 'getDemand' | 'getDemandCommentForTransaction' | 'getMatch2' extends keyof Database + ? 'getTransaction' | 'getDemand' | 'getDemandCommentForTransaction' | 'getMatch2' + : never - const [transaction] = await db.getTransaction(transactionId) - if (transaction.state === targetState) { - return transaction +const getRow = async (db: Database, method: Method, id: string): Promise<{ state: string } | undefined> => { + switch (method) { + case 'getTransaction': { + const [row] = await db.getTransaction(id) + return row + } + case 'getDemand': { + const [row] = await db.getDemand(id) + return row + } + case 'getDemandCommentForTransaction': { + const [row] = await db.getDemandCommentForTransaction(id) + return row } + case 'getMatch2': { + const [row] = await db.getMatch2(id) + return row + } + } +} - retry += 1 +const pollState = + (method: Method) => + async (db: Database, id: UUID | undefined, targetState: string, delay = 100, maxRetry = 100): Promise => { + let retry = 0 - return new Promise((resolve) => setTimeout(resolve, delay)).then(poll) + const poll = async (): Promise => { + if (retry >= maxRetry) { + throw new Error(`Maximum number of retries exceeded while waiting for ${id} to reach state ${targetState}`) + } + + const row = await getRow(db, method, id || '') + if (row && row.state === targetState) { + return + } + + retry += 1 + + return new Promise((resolve) => setTimeout(resolve, delay)).then(poll) + } + + await poll() } - return poll().then((value) => { - return new Promise((resolve) => { - setTimeout(() => resolve(value), delay) - }) - }) -} +export const pollTransactionState = pollState('getTransaction') +export const pollDemandState = pollState('getDemand') +export const pollDemandCommentState = pollState('getDemandCommentForTransaction') +export const pollMatch2State = pollState('getMatch2') diff --git a/test/init.ts b/test/init.ts new file mode 100644 index 00000000..3f425aaf --- /dev/null +++ b/test/init.ts @@ -0,0 +1 @@ +import 'reflect-metadata' diff --git a/test/integration/offchain/healthcheck.test.ts b/test/integration/offchain/healthcheck.test.ts index 8291fa4d..6575429e 100644 --- a/test/integration/offchain/healthcheck.test.ts +++ b/test/integration/offchain/healthcheck.test.ts @@ -8,6 +8,7 @@ import createHttpServer from '../../../src/server.js' import { get } from '../../helper/routeHelper.js' import { responses as healthResponses } from '../../helper/healthHelper.js' import { withOkMock, withIpfsMockError } from '../../helper/mockHealth.js' +import { resetContainer } from '../../../src/ioc.js' const getSpecVersion = (actualResult: any) => { return actualResult?._body?.details?.api?.detail?.runtime?.versions?.spec @@ -32,7 +33,7 @@ describe('health check', () => { after(async function () { const serviceWatcher = container.resolve(ServiceWatcher) await serviceWatcher.close() - container.reset() + resetContainer() }) it('health check', async function () { @@ -57,7 +58,7 @@ describe('health check', () => { after(async function () { const serviceWatcher = container.resolve(ServiceWatcher) await serviceWatcher.close() - container.reset() + resetContainer() }) withIpfsMockError() diff --git a/test/integration/onchain/chain.test.ts b/test/integration/onchain/chain.test.ts index dc0f8475..d550c7db 100644 --- a/test/integration/onchain/chain.test.ts +++ b/test/integration/onchain/chain.test.ts @@ -7,21 +7,15 @@ import { cleanup, seededDemandBId } from '../../seeds/onchainSeeds/transaction.s import { withIdentitySelfMock } from '../../helper/mock.js' import Database from '../../../src/lib/db/index.js' import ChainNode from '../../../src/lib/chainNode.js' -import { logger } from '../../../src/lib/logger.js' -import env from '../../../src/env.js' import { pollTransactionState } from '../../helper/poll.js' import { withAppAndIndexer } from '../../helper/chainTest.js' +import { container } from 'tsyringe' describe('on-chain', function () { this.timeout(60000) const db = new Database() - const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) + const node = container.resolve(ChainNode) const context: { app: Express; indexer: Indexer } = {} as { app: Express; indexer: Indexer } withAppAndIndexer(context) @@ -53,8 +47,9 @@ describe('on-chain', function () { // wait for dispatch error await node.sealBlock() - const failedTransaction = await pollTransactionState(db, transaction.id, 'failed') - expect(failedTransaction.state).to.equal('failed') + await pollTransactionState(db, transaction.id, 'failed') + const [failedTransaction] = await db.getTransaction(transaction.id) + expect(failedTransaction?.state).to.equal('failed') }) }) }) diff --git a/test/integration/onchain/demandA.test.ts b/test/integration/onchain/demandA.test.ts index 90f7ed20..86c77aa3 100644 --- a/test/integration/onchain/demandA.test.ts +++ b/test/integration/onchain/demandA.test.ts @@ -9,20 +9,14 @@ import { seed, cleanup, parametersAttachmentId, seededDemandAId } from '../../se import { selfAddress, withIdentitySelfMock } from '../../helper/mock.js' import Database, { DemandRow } from '../../../src/lib/db/index.js' import ChainNode from '../../../src/lib/chainNode.js' -import { logger } from '../../../src/lib/logger.js' -import env from '../../../src/env.js' -import { pollTransactionState } from '../../helper/poll.js' +import { pollTransactionState, pollDemandState, pollDemandCommentState } from '../../helper/poll.js' import { withAppAndIndexer } from '../../helper/chainTest.js' +import { container } from 'tsyringe' describe('on-chain', function () { this.timeout(60000) const db = new Database() - const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) + const node = container.resolve(ChainNode) const context: { app: Express; indexer: Indexer } = {} as { app: Express; indexer: Indexer } withAppAndIndexer(context) @@ -56,6 +50,7 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, transactionId, 'finalised') + await pollDemandState(db, demandAId, 'created') const [demandA] = await db.getDemand(demandAId) expect(demandA).to.contain({ @@ -68,6 +63,60 @@ describe('on-chain', function () { }) }) + it('creates many demandAs on chain in parallel', async function () { + const numberDemands = 500 + + const demandIds = await Promise.all( + Array(numberDemands) + .fill(null) + .map(async () => { + const { + status, + body: { id: demandAId }, + } = await post(context.app, '/v1/demandA', { parametersAttachmentId }) + expect(status).to.equal(201) + return demandAId as string + }) + ) + + const transactionIds = await Promise.all( + demandIds.map(async (demandAId) => { + const response = await post(context.app, `/v1/demandA/${demandAId}/creation`, {}) + expect(response.status).to.equal(201) + + const { id: transactionId, state } = response.body + expect(transactionId).to.match( + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89ABab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/ + ) + expect(state).to.equal('submitted') + + return transactionId as string + }) + ) + + await node.sealBlock() + + await Promise.all( + transactionIds.map(async (tx) => { + await pollTransactionState(db, tx, 'finalised') + }) + ) + + await Promise.all( + demandIds.map(async (demand) => { + await pollDemandState(db, demand, 'created', 500, 100) + + const [demandA] = await db.getDemand(demand) + expect(demandA).to.contain({ + id: demand, + state: 'created', + subtype: 'demand_a', + parametersAttachmentId, + }) + }) + ) + }) + it('should comment on a demandA on-chain', async () => { const lastTokenId = await node.getLastTokenId() @@ -77,6 +126,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, creationResponse.body.id, 'finalised') + await pollDemandState(db, seededDemandAId, 'created') // submit to chain const commentResponse = await post(context.app, `/v1/demandA/${seededDemandAId}/comment`, { @@ -86,6 +136,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, commentResponse.body.id, 'finalised') + await pollDemandCommentState(db, commentResponse.body.id, 'created') // check local demandA updates with token id const [maybeDemandB] = await db.getDemand(seededDemandAId) diff --git a/test/integration/onchain/demandB.test.ts b/test/integration/onchain/demandB.test.ts index af44a1e5..b53db4b7 100644 --- a/test/integration/onchain/demandB.test.ts +++ b/test/integration/onchain/demandB.test.ts @@ -8,20 +8,14 @@ import { seed, cleanup, seededDemandBId, parametersAttachmentId } from '../../se import { selfAddress, withIdentitySelfMock } from '../../helper/mock.js' import Database, { DemandRow } from '../../../src/lib/db/index.js' import ChainNode from '../../../src/lib/chainNode.js' -import { logger } from '../../../src/lib/logger.js' -import env from '../../../src/env.js' -import { pollTransactionState } from '../../helper/poll.js' +import { pollDemandCommentState, pollDemandState, pollTransactionState } from '../../helper/poll.js' import { withAppAndIndexer } from '../../helper/chainTest.js' +import { container } from 'tsyringe' describe('on-chain', function () { this.timeout(60000) const db = new Database() - const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) + const node = container.resolve(ChainNode) const context: { app: Express; indexer: Indexer } = {} as { app: Express; indexer: Indexer } withAppAndIndexer(context) @@ -53,6 +47,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, transactionId, 'finalised') + await pollDemandState(db, seededDemandBId, 'created') // check local demandB updates with token id const [maybeDemandB] = await db.getDemand(seededDemandBId) @@ -70,6 +65,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, creationResponse.body.id, 'finalised') + await pollDemandState(db, seededDemandBId, 'created') // submit to chain const commentResponse = await post(context.app, `/v1/demandB/${seededDemandBId}/comment`, { @@ -79,6 +75,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, commentResponse.body.id, 'finalised') + await pollDemandCommentState(db, commentResponse.body.id, 'created') // check local demandB updates with token id const [maybeDemandB] = await db.getDemand(seededDemandBId) diff --git a/test/integration/onchain/match2.test.ts b/test/integration/onchain/match2.test.ts index fbcc1c79..196ba361 100644 --- a/test/integration/onchain/match2.test.ts +++ b/test/integration/onchain/match2.test.ts @@ -8,21 +8,15 @@ import { seed, cleanup, parametersAttachmentId } from '../../seeds/onchainSeeds/ import { withIdentitySelfMock } from '../../helper/mock.js' import Database, { DemandRow, Match2Row, Transaction } from '../../../src/lib/db/index.js' import ChainNode from '../../../src/lib/chainNode.js' -import { logger } from '../../../src/lib/logger.js' -import env from '../../../src/env.js' -import { pollTransactionState } from '../../helper/poll.js' +import { pollDemandState, pollMatch2State, pollTransactionState } from '../../helper/poll.js' import { withAppAndIndexer } from '../../helper/chainTest.js' import { UUID } from '../../../src/models/strings.js' +import { container } from 'tsyringe' describe('on-chain', function () { this.timeout(180000) const db = new Database() - const node = new ChainNode({ - host: env.NODE_HOST, - port: env.NODE_PORT, - logger, - userUri: env.USER_URI, - }) + const node = container.resolve(ChainNode) const context: { app: Express; indexer: Indexer } = {} as { app: Express; indexer: Indexer } withAppAndIndexer(context) @@ -58,11 +52,13 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, demandATransactionId, 'finalised') + await pollDemandState(db, demandAId, 'created') const [demandA]: DemandRow[] = await db.getDemand(demandAId) await node.sealBlock() await pollTransactionState(db, demandBTransactionId, 'finalised') + await pollDemandState(db, demandBId, 'created') const [demandB]: DemandRow[] = await db.getDemand(demandBId) @@ -76,6 +72,7 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, newDemandBTransactionId, 'finalised') + await pollDemandState(db, newDemandBId, 'created') const { body: { id: match2Id }, @@ -107,6 +104,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, transactionId, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') // check local entities update with token id const [maybeDemandA] = await db.getDemand(ids.demandA) @@ -133,6 +131,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') // submit accept to chain const responseAcceptA = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) @@ -140,6 +139,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, responseAcceptA.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedA') // submit 2nd accept to chain const responseAcceptFinal = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) @@ -147,6 +147,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, responseAcceptFinal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedFinal') const lastTokenId = await node.getLastTokenId() @@ -171,6 +172,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, transactionId, 'finalised') + await pollMatch2State(db, ids.rematch2, 'proposed') // check local entities update with token id const [maybeDemandA] = await db.getDemand(ids.demandA) @@ -202,16 +204,19 @@ describe('on-chain', function () { const proposal = await post(context.app, `/v1/match2/${ids.match2}/proposal`, {}) await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') const resAcceptA = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) await node.sealBlock() await pollTransactionState(db, resAcceptA.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedA') const resAcceptFinal = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) await node.sealBlock() await pollTransactionState(db, resAcceptFinal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedFinal') const reMatch = await post(context.app, '/v1/match2', { demandA: ids.demandA, @@ -224,17 +229,20 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, resProposal.body.id, 'finalised') + await pollMatch2State(db, ids.rematch2, 'proposed') const resRematchAccept = await post(context.app, `/v1/match2/${ids.rematch2}/accept`, {}) await node.sealBlock() await pollTransactionState(db, resRematchAccept.body.id, 'finalised') + await pollMatch2State(db, ids.rematch2, 'acceptedA') const lastTokenId = await node.getLastTokenId() const resFinal = await post(context.app, `/v1/match2/${ids.rematch2}/accept`, {}) await node.sealBlock() await pollTransactionState(db, resFinal.body.id, 'finalised') + await pollMatch2State(db, ids.rematch2, 'acceptedFinal') // output const [match2]: Match2Row[] = await db.getMatch2(ids.match2) @@ -266,12 +274,14 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') const acceptA = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) await node.sealBlock() await pollTransactionState(db, acceptA.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedA') const { body: transactions } = await get(context.app, `/v1/match2/${ids.match2}/accept`) const failed = transactions.filter((el: Transaction) => el.state === 'failed') @@ -300,6 +310,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') const [maybeMatch2] = await db.getMatch2(ids.match2) const match2 = maybeMatch2 as Match2Row @@ -313,6 +324,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, responseAcceptA.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedA') // check local entities update with token id const [maybeMatch2AcceptA] = await db.getMatch2(ids.match2) @@ -328,6 +340,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, responseAcceptFinal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedFinal') // check local entities update with token id const [maybeDemandA] = await db.getDemand(ids.demandA) @@ -357,6 +370,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') const [maybeMatch2] = await db.getMatch2(ids.match2) const match2 = maybeMatch2 as Match2Row @@ -369,6 +383,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, rejection.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'rejected') // check local entities update with token id const [maybeMatch2Rejected] = await db.getMatch2(ids.match2) @@ -387,6 +402,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') // acceptA const acceptA = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) @@ -395,6 +411,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, acceptA.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedA') const [maybeMatch2] = await db.getMatch2(ids.match2) const match2 = maybeMatch2 as Match2Row @@ -407,6 +424,7 @@ describe('on-chain', function () { // wait for block to finalise await node.sealBlock() await pollTransactionState(db, rejection.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'rejected') // check local entities update with token id const [maybeMatch2Rejected] = await db.getMatch2(ids.match2) @@ -422,6 +440,7 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, proposal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'proposed') const { originalTokenId } = await db.getMatch2(ids.match2).then((el: Match2Row[]) => el[0]) @@ -429,11 +448,13 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, acceptA.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedA') const acceptFinal = await post(context.app, `/v1/match2/${ids.match2}/accept`, {}) await node.sealBlock() await pollTransactionState(db, acceptFinal.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'acceptedFinal') const lastTokenId = await node.getLastTokenId() @@ -443,6 +464,7 @@ describe('on-chain', function () { await node.sealBlock() await pollTransactionState(db, cancel.body.id, 'finalised') + await pollMatch2State(db, ids.match2, 'cancelled') const demandA: DemandRow = await db.getDemand(ids.demandA).then((rows: DemandRow[]) => rows[0]) const demandB: DemandRow = await db.getDemand(ids.demandB).then((rows: DemandRow[]) => rows[0]) diff --git a/test/mocharc.json b/test/mocharc.json index 4331df4f..3a9bedbb 100644 --- a/test/mocharc.json +++ b/test/mocharc.json @@ -2,7 +2,6 @@ "timeout": 5000, "exit": true, "extension": "ts", - "node-option": [ - "import=@digicatapult/tsimp/import" - ] + "file": ["test/init.ts"], + "node-option": ["import=@digicatapult/tsimp/import"] }