diff --git a/db/migrations/1700532563386-Data.js b/db/migrations/1700532563386-Data.js new file mode 100644 index 0000000..5f6a070 --- /dev/null +++ b/db/migrations/1700532563386-Data.js @@ -0,0 +1,11 @@ +module.exports = class Data1700532563386 { + name = 'Data1700532563386' + + async up(db) { + await db.query(`ALTER TABLE "application" ADD "timestamp" numeric NOT NULL`) + } + + async down(db) { + await db.query(`ALTER TABLE "application" DROP COLUMN "timestamp"`) + } +} diff --git a/schema.graphql b/schema.graphql index 67cbdb1..0ae447b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -6,6 +6,7 @@ type ApplicationFactory @entity @cardinality(value: 5) { type Application @entity @cardinality(value: 100) { id: ID! owner: String + timestamp: BigInt! factory: ApplicationFactory inputs: [Input!] @derivedFrom(field: "application") } diff --git a/src/handlers/ApplicationCreated.ts b/src/handlers/ApplicationCreated.ts index 19846a1..2038fea 100644 --- a/src/handlers/ApplicationCreated.ts +++ b/src/handlers/ApplicationCreated.ts @@ -33,7 +33,9 @@ export default class ApplicationCreated implements Handler { id, factory, owner: dappOwner.toLowerCase(), + timestamp: timestamp / 1000n, }); + this.applicationStorage.set(id, app); ctx.log.info(`${id} (Application) stored`); } diff --git a/src/handlers/InputAdded.ts b/src/handlers/InputAdded.ts index d9f6397..d8c6b0d 100644 --- a/src/handlers/InputAdded.ts +++ b/src/handlers/InputAdded.ts @@ -55,13 +55,17 @@ export default class InputAdded implements Handler { const timestamp = BigInt(log.block.timestamp); const event = events.InputAdded.decode(log); const dappId = event.dapp.toLowerCase(); + const timestampInSeconds = timestamp / 1000n; let application = this.applicationStorage.get(dappId) ?? (await ctx.store.get(Application, dappId)); if (!application) { ctx.log.warn(`${dappId} (Application) not found`); - application = new Application({ id: dappId }); + application = new Application({ + id: dappId, + timestamp: timestampInSeconds, + }); this.applicationStorage.set(dappId, application); ctx.log.info(`${dappId} (Application) stored`); } @@ -73,7 +77,7 @@ export default class InputAdded implements Handler { index: Number(event.inboxInputIndex), msgSender: event.sender.toLowerCase(), payload: event.input, - timestamp: timestamp / 1000n, + timestamp: timestampInSeconds, blockNumber: BigInt(log.block.height), blockHash: log.block.hash, transactionHash: log.transaction?.hash, diff --git a/tests/handlers/ApplicationCreated.test.ts b/tests/handlers/ApplicationCreated.test.ts index d0ba808..8d02b4e 100644 --- a/tests/handlers/ApplicationCreated.test.ts +++ b/tests/handlers/ApplicationCreated.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import ApplicationCreated from '../../src/handlers/ApplicationCreated'; +import { Application } from '../../src/model'; import { block, ctx, logs } from '../stubs/params'; vi.mock('../../src/model/', async (importOriginal) => { @@ -13,10 +14,13 @@ vi.mock('../../src/model/', async (importOriginal) => { }; }); +const ApplicationMock = vi.mocked(Application); + describe('ApplicationCreated', () => { let applicationCreated: ApplicationCreated; const mockFactoryStorage = new Map(); const mockApplicationStorage = new Map(); + beforeEach(() => { applicationCreated = new ApplicationCreated( mockFactoryStorage, @@ -26,6 +30,7 @@ describe('ApplicationCreated', () => { mockApplicationStorage.clear(); vi.clearAllMocks(); }); + describe('handle', async () => { test('call with correct params', async () => { vi.spyOn(applicationCreated, 'handle'); @@ -49,5 +54,26 @@ describe('ApplicationCreated', () => { expect(mockFactoryStorage.has(logs[1].address)).toBe(true); expect(mockApplicationStorage.has(applicationId)).toBe(true); }); + + test('set the timestamp in seconds from the block timestamp', async () => { + const expectedParams = vi.fn(); + + ApplicationMock.mockImplementationOnce((args) => { + expectedParams(args); + return new Application(args); + }); + + await applicationCreated.handle(logs[1], block, ctx); + const applicationId = '0x0be010fa7e70d74fa8b6729fe1ae268787298f54'; + const timestampInSeconds = BigInt(logs[1].block.timestamp) / 1000n; + + expect(expectedParams).toHaveBeenCalledOnce(); + expect(expectedParams).toHaveBeenCalledWith({ + factory: expect.any(Object), + id: applicationId, + owner: '0x74d093f6911ac080897c3145441103dabb869307', + timestamp: timestampInSeconds, + }); + }); }); }); diff --git a/tests/handlers/InputAdded.test.ts b/tests/handlers/InputAdded.test.ts index 4b4ce2b..d9a1a9e 100644 --- a/tests/handlers/InputAdded.test.ts +++ b/tests/handlers/InputAdded.test.ts @@ -2,7 +2,7 @@ import { dataSlice, getUint } from 'ethers'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { Contract } from '../../src/abi/ERC20'; import InputAdded from '../../src/handlers/InputAdded'; -import { Erc20Deposit, Token } from '../../src/model'; +import { Application, Erc20Deposit, Token } from '../../src/model'; import { block, ctx, input, logs } from '../stubs/params'; vi.mock('../../src/abi/ERC20', async (importOriginal) => { @@ -30,9 +30,13 @@ vi.mock('../../src/model/', async (importOriginal) => { Input, }; }); + +const ApplicationMock = vi.mocked(Application); + const tokenAddress = dataSlice(input.payload, 1, 21).toLowerCase(); // 20 bytes for address const from = dataSlice(input.payload, 21, 41).toLowerCase(); // 20 bytes for address const amount = getUint(dataSlice(input.payload, 41, 73)); // 32 bytes for uint256 + describe('InputAdded', () => { let inputAdded: InputAdded; let erc20; @@ -152,5 +156,23 @@ describe('InputAdded', () => { await inputAdded.handle(logs[0], block, ctx); expect(mockDepositStorage.size).toBe(1); }); + + test('when creating a non-existing app it should also set the timestamp in seconds', async () => { + const expectedParams = vi.fn(); + + ApplicationMock.mockImplementationOnce((args) => { + expectedParams(args); + return new Application(args); + }); + + await inputAdded.handle(logs[0], block, ctx); + + const timestamp = BigInt(logs[0].block.timestamp) / 1000n; + + expect(expectedParams).toHaveBeenCalledWith({ + id: '0x0be010fa7e70d74fa8b6729fe1ae268787298f54', + timestamp, + }); + }); }); }); diff --git a/tests/stubs/params.ts b/tests/stubs/params.ts index b2fe834..4be3f29 100644 --- a/tests/stubs/params.ts +++ b/tests/stubs/params.ts @@ -6,6 +6,7 @@ import { CartesiDAppFactoryAddress, ERC20PortalAddress, } from '../../src/config'; +import { Input } from '../../src/model'; vi.mock('@subsquid/logger', async (importOriginal) => { const actualMods = await importOriginal; const Logger = vi.fn(); @@ -31,6 +32,7 @@ export const input = { id: '0x60a7048c3136293071605a4eaffef49923e981cc-0', application: { id: '0x60a7048c3136293071605a4eaffef49923e981cc', + timestamp: 1696281168n, owner: null, factory: null, inputs: [], @@ -38,14 +40,14 @@ export const input = { index: 1, msgSender: ERC20PortalAddress, payload: payload, - timestamp: 1691384268 as unknown as bigint, - blockNumber: 4040941 as unknown as bigint, + timestamp: 1691384268n, + blockNumber: 4040941n, blockHash: '0xce6a0d404b4201b3bd4fb8309df0b6a64f6a5d7b71fa89bf2737d4574c58b32f', erc20Deposit: null, transactionHash: '0x6a3d76983453c0f74188bd89e01576c35f9d9b02daecdd49f7171aeb2bd3dc78', -}; +} satisfies Input; export const logs = [ { diff --git a/vitest.config.ts b/vitest.config.ts index d29c8d8..b45e693 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,11 @@ -import { defineConfig } from 'vitest/config' import { UserConfig } from 'vitest'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { coverage: { reporter: ['text', 'lcov'], + exclude: ['src/abi', 'src/model', 'tests/'], }, } as UserConfig, -}) \ No newline at end of file +});