diff --git a/package.json b/package.json index 877b1e95..72427f17 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "postgres:dev:reset": "yarn postgres:dev down postgres --volumes && yarn postgres:dev:start", "postgres:dev:migration:generate": "source ./postgres.dev.config && yarn postgres migration:generate", "postgres:dev:migration:run": "source ./postgres.dev.config && yarn postgres migration:run", + "postgres:dev:migration:revert": "source ./postgres.dev.config && yarn postgres migration:revert", "postgres:migration:run": "yarn postgres migration:run" }, "dependencies": { diff --git a/src/artemis/artemis.service.ts b/src/artemis/artemis.service.ts index 4dfbf67f..0932bc4b 100644 --- a/src/artemis/artemis.service.ts +++ b/src/artemis/artemis.service.ts @@ -164,7 +164,9 @@ export class ArtemisService implements OnApplicationShutdown { const model = new Model(context.message.body); const validationErrors = await validate(model); if (validationErrors.length) { - this.logger.error(`Validation errors: ${JSON.stringify(validationErrors)}`); + this.logger.error( + `Validation errors for "${listenOn}": ${JSON.stringify(validationErrors)}` + ); } try { diff --git a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap index 5d8a5bb5..3528c70c 100644 --- a/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap +++ b/src/datastore/local-store/repos/__snapshots__/offline-tx.repo.spec.ts.snap @@ -2,7 +2,9 @@ exports[`LocalOfflineTxRepo OfflineEvent test suite method: createTx should record the transaction request 1`] = ` { + "address": "someAddress", "id": "1", + "nonce": 1, "payload": { "metadata": { "memo": "test utils payload", @@ -28,6 +30,7 @@ exports[`LocalOfflineTxRepo OfflineEvent test suite method: createTx should reco "type": "bytes", }, }, - "status": "Requested", + "signature": "0x01", + "status": "Signed", } `; diff --git a/src/datastore/postgres/entities/offline-tx.entity.ts b/src/datastore/postgres/entities/offline-tx.entity.ts index 8dd3d989..1b4570be 100644 --- a/src/datastore/postgres/entities/offline-tx.entity.ts +++ b/src/datastore/postgres/entities/offline-tx.entity.ts @@ -13,6 +13,12 @@ export class OfflineTx extends BaseEntity { @Column({ type: 'text', nullable: true }) signature: string; + @Column({ type: 'text' }) + address: string; + + @Column({ type: 'integer' }) + nonce: number; + @Column({ type: 'json' }) payload: TransactionPayload; diff --git a/src/datastore/postgres/migrations/1705074621853-offline.ts b/src/datastore/postgres/migrations/1705074621853-offline.ts new file mode 100644 index 00000000..12f365fb --- /dev/null +++ b/src/datastore/postgres/migrations/1705074621853-offline.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Offline1705074621853 implements MigrationInterface { + name = 'Offline1705074621853'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "offline_tx" ADD "address" text'); + await queryRunner.query('UPDATE "offline_tx" SET "address" = \'\' WHERE "address" IS NULL'); + await queryRunner.query('ALTER TABLE "offline_tx" ALTER COLUMN "address" SET NOT NULL'); + await queryRunner.query('ALTER TABLE "offline_tx" ADD "nonce" integer'); + await queryRunner.query('UPDATE "offline_tx" SET "nonce" = -1 WHERE "nonce" IS NULL'); + await queryRunner.query('ALTER TABLE "offline_tx" ALTER COLUMN "nonce" SET NOT NULL'); + + await queryRunner.query('CREATE INDEX idx_address_nonce ON offline_tx (address, nonce)'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX idx_address_nonce'); + + await queryRunner.query('ALTER TABLE "offline_tx" DROP COLUMN "nonce"'); + await queryRunner.query('ALTER TABLE "offline_tx" DROP COLUMN "address"'); + } +} diff --git a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap index 9a8d3e05..27c495d6 100644 --- a/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap +++ b/src/datastore/postgres/repos/__snapshots__/offline-tx.repo.spec.ts.snap @@ -2,7 +2,9 @@ exports[`PostgresOfflineTxRepo OfflineTxRepo test suite method: createTx should record the transaction request 1`] = ` OfflineTxModel { + "address": "someAddress", "id": "someTestSuiteId", + "nonce": 1, "payload": { "metadata": { "memo": "test utils payload", @@ -28,6 +30,7 @@ OfflineTxModel { "type": "bytes", }, }, - "status": "Requested", + "signature": "0x01", + "status": "Signed", } `; diff --git a/src/offline-signer/offline-signer.service.spec.ts b/src/offline-signer/offline-signer.service.spec.ts index 07343990..7eac2cab 100644 --- a/src/offline-signer/offline-signer.service.spec.ts +++ b/src/offline-signer/offline-signer.service.spec.ts @@ -6,7 +6,7 @@ import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName } from '~/common/utils/amqp'; import { mockPolymeshLoggerProvider } from '~/logger/mock-polymesh-logger'; import { OfflineSignerService } from '~/offline-signer/offline-signer.service'; -import { OfflineTxModel, OfflineTxStatus } from '~/offline-submitter/models/offline-tx.model'; +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'; @@ -47,10 +47,9 @@ describe('OfflineSignerService', () => { describe('method: autoSign', () => { it('should sign and publish the signature', async () => { - const model = new OfflineTxModel({ + const model = new OfflineRequestModel({ id: 'someId', payload: {} as TransactionPayload, - status: OfflineTxStatus.Requested, }); const mockSignature = '0x01'; diff --git a/src/offline-signer/offline-signer.service.ts b/src/offline-signer/offline-signer.service.ts index 43174262..70a2e256 100644 --- a/src/offline-signer/offline-signer.service.ts +++ b/src/offline-signer/offline-signer.service.ts @@ -4,7 +4,7 @@ import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName, QueueName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineSignatureModel } from '~/offline-signer/models/offline-signature.model'; -import { OfflineTxModel } from '~/offline-submitter/models/offline-tx.model'; +import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; import { SigningService } from '~/signing/services'; /** @@ -23,11 +23,11 @@ export class OfflineSignerService { QueueName.Requests, /* istanbul ignore next */ msg => this.autoSign(msg), - OfflineTxModel + OfflineRequestModel ); } - public async autoSign(body: OfflineTxModel): Promise { + public async autoSign(body: OfflineRequestModel): Promise { const { id: transactionId } = body; this.logger.debug(`received request for signature: ${transactionId}`); diff --git a/src/offline-starter/models/offline-request.model.ts b/src/offline-starter/models/offline-request.model.ts new file mode 100644 index 00000000..eed92b8c --- /dev/null +++ b/src/offline-starter/models/offline-request.model.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ + +import { ApiProperty } from '@nestjs/swagger'; +import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; +import { IsString } from 'class-validator'; + +export class OfflineRequestModel { + @ApiProperty({ + description: 'The internal ID', + }) + @IsString() + id: string; + + @ApiProperty({ + description: 'The transaction payload to be signed', + }) + payload: TransactionPayload; + + constructor(model: OfflineRequestModel) { + Object.assign(this, model); + } +} diff --git a/src/offline-starter/offline-starter.service.ts b/src/offline-starter/offline-starter.service.ts index 962621a1..2a8aa858 100644 --- a/src/offline-starter/offline-starter.service.ts +++ b/src/offline-starter/offline-starter.service.ts @@ -7,6 +7,7 @@ import { ArtemisService } from '~/artemis/artemis.service'; import { AddressName } from '~/common/utils/amqp'; import { PolymeshLogger } from '~/logger/polymesh-logger.service'; import { OfflineReceiptModel } from '~/offline-starter/models/offline-receipt.model'; +import { OfflineRequestModel } from '~/offline-starter/models/offline-request.model'; @Injectable() export class OfflineStarterService { @@ -25,13 +26,15 @@ export class OfflineStarterService { const internalTxId = this.generateTxId(); const payload = await transaction.toSignablePayload({ ...metadata, internalTxId }); - const topicName = AddressName.Requests; - this.logger.debug(`sending topic: ${topicName}`, topicName); - const delivery = await this.artemisService.sendMessage(topicName, { + const request = new OfflineRequestModel({ id: internalTxId, payload, }); + const topicName = AddressName.Requests; + + this.logger.debug(`sending topic: ${topicName}`, topicName); + const delivery = await this.artemisService.sendMessage(topicName, request); const model = new OfflineReceiptModel({ deliveryId: new BigNumber(delivery.id), diff --git a/src/offline-submitter/models/offline-tx.model.ts b/src/offline-submitter/models/offline-tx.model.ts index e7ad2040..3ecbbcdc 100644 --- a/src/offline-submitter/models/offline-tx.model.ts +++ b/src/offline-submitter/models/offline-tx.model.ts @@ -2,10 +2,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { TransactionPayload } from '@polymeshassociation/polymesh-sdk/types'; -import { IsEnum, IsOptional, IsString } from 'class-validator'; +import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; export enum OfflineTxStatus { - Requested = 'Requested', Signed = 'Signed', Finalized = 'Finalized', } @@ -27,14 +26,26 @@ export class OfflineTxModel { }) @IsOptional() @IsString() - signature?: string; + signature: string; @ApiProperty({ description: 'The status of the transaction', enum: OfflineTxStatus, }) @IsEnum(OfflineTxStatus) - status: OfflineTxStatus = OfflineTxStatus.Requested; + status: OfflineTxStatus = OfflineTxStatus.Signed; + + @ApiProperty({ + description: 'The account signing the transaction', + }) + @IsString() + readonly address: string; + + @ApiProperty({ + description: 'The nonce of the transaction', + }) + @IsNumber() + readonly nonce: number; @ApiProperty({ description: 'The block hash the transaction was included in', diff --git a/src/offline-submitter/offline-submitter.service.spec.ts b/src/offline-submitter/offline-submitter.service.spec.ts index 0db2b7f9..9b00616a 100644 --- a/src/offline-submitter/offline-submitter.service.spec.ts +++ b/src/offline-submitter/offline-submitter.service.spec.ts @@ -43,7 +43,10 @@ describe('OfflineSubmitterService', () => { offlineModel = new OfflineTxModel({ id: 'someId', payload: {} as TransactionPayload, - status: OfflineTxStatus.Requested, + status: OfflineTxStatus.Signed, + signature: '0x01', + nonce: 1, + address: 'someAddress', }); }); diff --git a/src/offline-submitter/offline-submitter.service.ts b/src/offline-submitter/offline-submitter.service.ts index 1945f10d..465e3a27 100644 --- a/src/offline-submitter/offline-submitter.service.ts +++ b/src/offline-submitter/offline-submitter.service.ts @@ -33,6 +33,8 @@ export class OfflineSubmitterService { */ public async submit(body: OfflineSignatureModel): Promise { const { id, signature, payload } = body; + const { address, nonce: rawNonce } = payload.payload; + const nonce = parseInt(rawNonce, 16); this.logger.debug(`received signature for: ${id}`); const transaction = await this.offlineTxRepo.createTx({ @@ -40,6 +42,8 @@ export class OfflineSubmitterService { payload, status: OfflineTxStatus.Signed, signature, + address, + nonce, }); this.logger.log(`submitting transaction: ${id}`); @@ -50,6 +54,7 @@ export class OfflineSubmitterService { this.logger.log(`transaction finalized: ${id}`); const msg = JSON.parse(JSON.stringify(result)); // make sure its serializes properly + await this.artemisService.sendMessage(AddressName.Finalizations, msg); transaction.blockHash = result.blockHash as string; diff --git a/src/offline-submitter/repos/offline-tx.suite.ts b/src/offline-submitter/repos/offline-tx.suite.ts index ff02533d..e704316e 100644 --- a/src/offline-submitter/repos/offline-tx.suite.ts +++ b/src/offline-submitter/repos/offline-tx.suite.ts @@ -11,9 +11,8 @@ export const testOfflineTxRepo = async (offlineTxRepo: OfflineTxRepo): Promise { it('should record the transaction request', async () => { const txParams = new OfflineTxModel({ + ...offlineTx, id: 'someTestSuiteId', - payload: offlineTx.payload, - status: OfflineTxStatus.Requested, }); model = await offlineTxRepo.createTx({ ...txParams }); expect(model).toMatchSnapshot(); diff --git a/src/test-utils/consts.ts b/src/test-utils/consts.ts index 4fceb0f9..7d7d74ec 100644 --- a/src/test-utils/consts.ts +++ b/src/test-utils/consts.ts @@ -47,7 +47,10 @@ const offlineTx = new OfflineTxModel({ rawPayload: { address: 'address', data: '0x01', type: 'bytes' }, metadata: { memo: 'test utils payload' }, }, - status: OfflineTxStatus.Requested, + status: OfflineTxStatus.Signed, + signature: '0x01', + address: 'someAddress', + nonce: 1, }); export const testAccount = createMock({ address: 'address' });