diff --git a/mocks/blocks-mocks.ts b/mocks/blocks-mocks.ts index 18d0a86..4d726c2 100644 --- a/mocks/blocks-mocks.ts +++ b/mocks/blocks-mocks.ts @@ -2,6 +2,7 @@ export const mockBlock = { hash: '0x03b26a67c6c7fda467f7b96d09b99d04ef9a8163043e72b5e5474358631afad2', parentHash: '0x9b0f818b9cac7d9451819de6172e308d67c4b8ff8c2f1f6773cdb20c40573858', number: 27, + timestamp: 1590000000, createdDate: '2022-08-25 22:49:21.843575', } diff --git a/mocks/transactions-mock.ts b/mocks/transactions-mock.ts index 7e25327..3184498 100644 --- a/mocks/transactions-mock.ts +++ b/mocks/transactions-mock.ts @@ -11,6 +11,7 @@ export const mockTransaction = { signer: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', nonce: 37, tip: 0, + timestamp: 1600000000000, createdDate: '2022-08-25 23:42:49.006343', } @@ -41,6 +42,8 @@ export const mockTransactions = [ }, ] +export const mockTimestamp = 1600000000000 + export const mockExtrinsics = [ { hash: stringToHex('0x01c780fccc47dc4e9652180876a8267dc9f9dd501ed249f077e32c1653a89f2a'), @@ -57,6 +60,7 @@ export const mockExtrinsics = [ method: { method: 'set', section: 'timestamp', + args: [mockTimestamp], }, }, { @@ -74,6 +78,7 @@ export const mockExtrinsics = [ method: { method: 'call', section: 'timestamp', + args: [mockTimestamp], }, }, ] diff --git a/src/blocks/blocks.service.spec.ts b/src/blocks/blocks.service.spec.ts index 4133200..cec3101 100644 --- a/src/blocks/blocks.service.spec.ts +++ b/src/blocks/blocks.service.spec.ts @@ -83,6 +83,7 @@ describe('BlocksService', () => { parentHash: mockBlock.parentHash, number, } as any) || {}, + mockBlock.timestamp, ), ).resolves.toBe(mockBlock) expect(repo.create).toBeCalledTimes(1) @@ -90,6 +91,7 @@ describe('BlocksService', () => { hash: mockBlock.hash, parentHash: mockBlock.parentHash, number: mockBlock.number, + timestamp: mockBlock.timestamp, }) // TODO: fix repo.save called // expect(repo.save).toBeCalledTimes(1) @@ -109,6 +111,7 @@ describe('BlocksService', () => { parentHash: mockBlock.parentHash, number, } as any) || {}, + mockBlock.timestamp, ), ).resolves.toBe(mockBlock) expect(repo.create).toBeCalledTimes(1) @@ -116,6 +119,7 @@ describe('BlocksService', () => { hash: mockBlock.hash, parentHash: mockBlock.parentHash, number: mockBlock.number, + timestamp: mockBlock.timestamp, }) expect(repo.save).toBeCalledTimes(0) }) diff --git a/src/blocks/blocks.service.ts b/src/blocks/blocks.service.ts index 2101832..378be40 100644 --- a/src/blocks/blocks.service.ts +++ b/src/blocks/blocks.service.ts @@ -29,13 +29,14 @@ export class BlocksService { return this.blockRepository.find(args) } - async createFromHeader(header: Header): Promise { + async createFromHeader(header: Header, timestamp: number): Promise { try { const { hash, parentHash, number } = header const block = this.blockRepository.create({ hash: hash.toString().toLowerCase(), parentHash: parentHash.toString().toLowerCase(), number: parseInt(number.toHex()), + timestamp, }) const persistedBlock = await retry( async () => { diff --git a/src/blocks/entity/block.entity.ts b/src/blocks/entity/block.entity.ts index 10c0356..75f0d88 100644 --- a/src/blocks/entity/block.entity.ts +++ b/src/blocks/entity/block.entity.ts @@ -1,4 +1,4 @@ -import { Field, Int, ObjectType } from '@nestjs/graphql' +import { Field, Float, Int, ObjectType } from '@nestjs/graphql' import { BaseEntity, Column, CreateDateColumn, Entity, Index, OneToMany, PrimaryColumn } from 'typeorm' import { Transaction } from '../../transactions/entity/transaction.entity' @@ -20,6 +20,10 @@ export class Block extends BaseEntity { @Field(/* istanbul ignore next */ () => Int) number!: number + @Column('bigint') + @Field(/* istanbul ignore next */ () => Float) + timestamp!: number + @OneToMany( /* istanbul ignore next */ () => Transaction, /* istanbul ignore next */ (transaction: Transaction) => transaction.block, diff --git a/src/events/entity/event.entity.ts b/src/events/entity/event.entity.ts index 584736b..f3dd3da 100644 --- a/src/events/entity/event.entity.ts +++ b/src/events/entity/event.entity.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql' +import { Field, Float, ObjectType } from '@nestjs/graphql' import { Codec } from '@polkadot/types-codec/types' import { IEventData } from '@polkadot/types/types' import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, CreateDateColumn } from 'typeorm' @@ -39,6 +39,10 @@ export class Event extends BaseEntity { @Field(/* istanbul ignore next */ () => String) topics!: string + @Column('bigint') + @Field(/* istanbul ignore next */ () => Float) + timestamp!: number + @ManyToOne( /* istanbul ignore next */ () => Transaction, /* istanbul ignore next */ (transaction: Transaction) => transaction.events, diff --git a/src/events/events.service.spec.ts b/src/events/events.service.spec.ts index 17bf4e4..9fa1869 100644 --- a/src/events/events.service.spec.ts +++ b/src/events/events.service.spec.ts @@ -114,7 +114,12 @@ describe('EventsService', () => { .mockResolvedValueOnce(mockEvents[0] as never) .mockResolvedValueOnce(mockEvents[1] as never) - const events = await service.createEventsFromRecords(mockRecords as any, 1, mockTransaction.hash) + const events = await service.createEventsFromRecords( + mockRecords as any, + 1, + mockTransaction.hash, + mockTransaction.timestamp, + ) expect(events).toStrictEqual(mockEvents) }) @@ -131,7 +136,12 @@ describe('EventsService', () => { .mockResolvedValueOnce(mockEvents[0] as never) .mockResolvedValueOnce(mockEvents[1] as never) - const events = await service.createEventsFromRecords(mockRecords as any, 1, mockTransaction.hash) + const events = await service.createEventsFromRecords( + mockRecords as any, + 1, + mockTransaction.hash, + mockTransaction.timestamp, + ) expect(events).toStrictEqual(mockEvents) }) diff --git a/src/events/events.service.ts b/src/events/events.service.ts index bfc6ae5..c49833f 100644 --- a/src/events/events.service.ts +++ b/src/events/events.service.ts @@ -33,6 +33,7 @@ export class EventsService { records: Vec, extrinsicIndex: number, transactionHash: string, + timestamp: number, ): Promise { const events = records.filter(({ phase }) => phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(extrinsicIndex)) const contractEmittedEvents = events.filter((record) => record?.event?.method === 'ContractEmitted') @@ -66,6 +67,7 @@ export class EventsService { topics: topics.toString(), data, transactionHash: transactionHash.toString().toLowerCase(), + timestamp, }) }) return Promise.all(eventsToSave.map((event) => this.eventRepository.save(event))) diff --git a/src/schema.graphql b/src/schema.graphql index fab45c3..dac54e0 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -6,6 +6,7 @@ type Block { hash: String! number: Int! parentHash: String! + timestamp: Float! transactions: [Transaction!]! } @@ -21,6 +22,7 @@ type Event { index: String! method: String! section: String! + timestamp: Float! topics: String! transactionHash: String } @@ -57,6 +59,7 @@ type Transaction { """Address of the signer""" signer: String + timestamp: Float! """Extra gas paid for the Tx as tip""" tip: Int diff --git a/src/subscriptions/subscriptions.service.spec.ts b/src/subscriptions/subscriptions.service.spec.ts index 5f660db..4d13370 100644 --- a/src/subscriptions/subscriptions.service.spec.ts +++ b/src/subscriptions/subscriptions.service.spec.ts @@ -4,7 +4,7 @@ import { apiMock } from '../../mocks/api-mock' import { mockBlock, mockBlocks } from '../../mocks/blocks-mocks' import { mockEvents } from '../../mocks/events-mocks' import { mockPinoService } from '../../mocks/pino-mocks' -import { mockExtrinsics, mockTransactions } from '../../mocks/transactions-mock' +import { mockExtrinsics, mockTimestamp, mockTransactions } from '../../mocks/transactions-mock' import { BlocksService } from '../blocks/blocks.service' import { EventsService } from '../events/events.service' import { TransactionsService } from '../transactions/transactions.service' @@ -133,6 +133,7 @@ describe('subscriptionsService', () => { }, extrinsics: mockExtrinsics, records: [], + timestamp: mockTimestamp, }), ) }) diff --git a/src/subscriptions/subscriptions.service.ts b/src/subscriptions/subscriptions.service.ts index 14eb6da..76ca707 100644 --- a/src/subscriptions/subscriptions.service.ts +++ b/src/subscriptions/subscriptions.service.ts @@ -84,7 +84,9 @@ export class SubscriptionsService implements OnModuleInit { async getBlockData(api: ApiPromise, hash: BlockHash) { const [block, records] = await Promise.all([api.rpc.chain.getBlock(hash), api.query.system.events.at(hash)]) const { header, extrinsics } = block.block || {} - return { header, extrinsics, records } + const timestampArgs = extrinsics.map((e) => e.method).find((m) => m.section === 'timestamp' && m.method === 'set') + const timestamp = Number(timestampArgs?.args[0].toString()) + return { header, extrinsics, records, timestamp } } getBlocksToLoad(from: number, to: number): number[] { @@ -116,11 +118,15 @@ export class SubscriptionsService implements OnModuleInit { } async registerBlockData(blockData: any) { - const { header, extrinsics, records } = blockData - const block = await this.blocksService.createFromHeader(header) - const transactions = await this.transactionsService.createTransactionsFromExtrinsics(extrinsics, block.hash) + const { header, extrinsics, records, timestamp } = blockData + const block = await this.blocksService.createFromHeader(header, timestamp) + const transactions = await this.transactionsService.createTransactionsFromExtrinsics( + extrinsics, + block.hash, + timestamp, + ) for (const [index, tx] of transactions.entries()) { - await this.eventsService.createEventsFromRecords(records, index, tx.hash) + await this.eventsService.createEventsFromRecords(records, index, tx.hash, timestamp) } return block } diff --git a/src/transactions/entity/transaction.entity.ts b/src/transactions/entity/transaction.entity.ts index f75e0be..dab2537 100644 --- a/src/transactions/entity/transaction.entity.ts +++ b/src/transactions/entity/transaction.entity.ts @@ -1,4 +1,4 @@ -import { Field, Int, ObjectType } from '@nestjs/graphql' +import { Field, Float, Int, ObjectType } from '@nestjs/graphql' import { BaseEntity, Column, @@ -35,6 +35,10 @@ export class Transaction extends BaseEntity { @Field(/* istanbul ignore next */ () => String) section!: string + @Column('bigint') + @Field(/* istanbul ignore next */ () => Float) + timestamp!: number + @ManyToOne(/* istanbul ignore next */ () => Block, /* istanbul ignore next */ (block: Block) => block.transactions, { onDelete: 'SET NULL', nullable: true, diff --git a/src/transactions/transactions.service.spec.ts b/src/transactions/transactions.service.spec.ts index a7394d2..b208ebe 100644 --- a/src/transactions/transactions.service.spec.ts +++ b/src/transactions/transactions.service.spec.ts @@ -3,7 +3,13 @@ import { Test, TestingModule } from '@nestjs/testing' import { getRepositoryToken } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { mockPinoService } from '../../mocks/pino-mocks' -import { mockExtrinsics, mockSavedTransactions, mockTransaction, mockTransactions } from '../../mocks/transactions-mock' +import { + mockExtrinsics, + mockSavedTransactions, + mockTimestamp, + mockTransaction, + mockTransactions, +} from '../../mocks/transactions-mock' import { Transaction } from './entity/transaction.entity' import { TransactionsService } from './transactions.service' @@ -66,7 +72,7 @@ describe('TransactionsService', () => { .mockResolvedValueOnce(mockSavedTransactions[0]) .mockResolvedValueOnce(mockSavedTransactions[1]) const blockHash = '0xffcfae3ecc9ab7b79fc0cd451dad35477a32219b219b29584b968826ac04c1a1' - const savedTxs = await service.createTransactionsFromExtrinsics(mockExtrinsics as any, blockHash) + const savedTxs = await service.createTransactionsFromExtrinsics(mockExtrinsics as any, blockHash, mockTimestamp) mockSavedTransactions.forEach((tx, i) => { expect(savedTxs[i].hash).toEqual(tx.hash) expect(savedTxs[i].blockHash).toEqual(tx.blockHash) @@ -87,7 +93,7 @@ describe('TransactionsService', () => { .mockResolvedValueOnce(mockSavedTransactions[0]) .mockResolvedValueOnce(mockSavedTransactions[1]) const blockHash = '0xffcfae3ecc9ab7b79fc0cd451dad35477a32219b219b29584b968826ac04c1a1' - const savedTxs = await service.createTransactionsFromExtrinsics(mockExtrinsics as any, blockHash) + const savedTxs = await service.createTransactionsFromExtrinsics(mockExtrinsics as any, blockHash, mockTimestamp) mockSavedTransactions.forEach((tx, i) => { expect(savedTxs[i].hash).toEqual(tx.hash) expect(savedTxs[i].blockHash).toEqual(tx.blockHash) @@ -107,7 +113,7 @@ describe('TransactionsService', () => { try { jest.spyOn(repo, 'findOneBy').mockResolvedValue(Promise.reject("Can't connect to database")) const blockHash = '0xffcfae3ecc9ab7b79fc0cd451dad35477a32219b219b29584b968826ac04c1a1' - await service.createTransactionsFromExtrinsics(mockExtrinsics as any, blockHash) + await service.createTransactionsFromExtrinsics(mockExtrinsics as any, blockHash, mockTimestamp) fail("Shouldn't reach this point") } catch (error) { expect(error).toEqual("Can't connect to database") diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts index 4672115..6452b00 100644 --- a/src/transactions/transactions.service.ts +++ b/src/transactions/transactions.service.ts @@ -35,6 +35,7 @@ export class TransactionsService { async createTransactionsFromExtrinsics( extrinsics: Vec>, blockHash: string, + timestamp: number, ): Promise { return Promise.all( extrinsics.map(async (extrinsic) => { @@ -50,6 +51,7 @@ export class TransactionsService { signer: signer.toString(), tip: tip.toNumber(), blockHash: blockHash.toLowerCase(), + timestamp, }) const transaction = await retry( async () => {