diff --git a/packages/explorer/src/app/blocks/[hash]/page.tsx b/packages/explorer/src/app/blocks/[hash]/page.tsx index 0e33056bf..85d3e0b9f 100644 --- a/packages/explorer/src/app/blocks/[hash]/page.tsx +++ b/packages/explorer/src/app/blocks/[hash]/page.tsx @@ -18,7 +18,9 @@ export interface GetBlockQueryResponse { | { hash: string; height: string; - fromStateRoot: string; + result: { + stateRoot: string; + }; transactions: { tx: { hash: string; @@ -64,7 +66,9 @@ export default function BlockDetail() { block (where: {hash: "${params.hash}"}) { height hash - fromStateRoot + result { + stateRoot + } transactions { tx { hash, @@ -109,7 +113,7 @@ export default function BlockDetail() { }, { label: "StateRoot", - value: data?.block?.fromStateRoot ?? "—", + value: data?.block?.result.stateRoot ?? "—", }, ]; diff --git a/packages/explorer/src/app/transactions/[hash]/page.tsx b/packages/explorer/src/app/transactions/[hash]/page.tsx index 3cc8222f6..7adc1f8f6 100644 --- a/packages/explorer/src/app/transactions/[hash]/page.tsx +++ b/packages/explorer/src/app/transactions/[hash]/page.tsx @@ -8,22 +8,27 @@ import Truncate from "react-truncate-inside/es"; import { DetailsLayout } from "@/components/details/layout"; import config from "@/config"; +interface Transaction { + hash: string; + sender: string; + methodId: string; + nonce: string; + executionResult: { + status: boolean; + statusMessage?: string; + block: { + batch: { + proof: string | null; + settlementTransactionHash: string | null; + }; + }; + }; + status: boolean; + statusMessage?: string; +} export interface GetTransactionQueryResponse { data: { - transaction: - | { - hash: string; - sender: string; - methodId: string; - nonce: string; - executionResult: { - status: boolean; - statusMessage?: string; - }; - status: boolean; - statusMessage?: string; - } - | undefined; + transaction: Transaction | undefined; }; } @@ -49,6 +54,12 @@ export default function BlockDetail() { executionResult { status statusMessage + block { + batch { + proof + settlementTransactionHash + } + } } } }`, @@ -71,13 +82,32 @@ export default function BlockDetail() { void query(); }, []); + const getStatus = (tx: Transaction | undefined) => { + const batch = tx?.executionResult?.block?.batch; + + if (tx == null) return "Pending"; + if (batch == null) + return tx?.executionResult?.block != null ? "Included" : "Pending"; + if (batch.settlementTransactionHash != null) return "Settled"; + if (batch.proof != null) return "Proven"; + + return "Included"; + }; const details = [ { label: "Nonce", value: data?.transaction?.nonce ?? "—", }, { - label: "Status", + label: "Finality status", + value: ( +
+ {getStatus(data?.transaction)} +
+ ), + }, + { + label: "Execution status", value: (
{data?.transaction?.executionResult?.status != null ? ( diff --git a/packages/explorer/src/components/blocks/BlocksPageClient.tsx b/packages/explorer/src/components/blocks/BlocksPageClient.tsx index 53ebd4737..f62064b5f 100644 --- a/packages/explorer/src/components/blocks/BlocksPageClient.tsx +++ b/packages/explorer/src/components/blocks/BlocksPageClient.tsx @@ -23,7 +23,9 @@ export interface GetBlocksQueryResponse { blocks: { height: string; hash: string; - fromStateRoot: string; + result: { + stateRoot: string; + }; _count: { transactions: number; }; @@ -131,7 +133,9 @@ export default function BlocksPageClient() { }){ height hash - fromStateRoot + result { + stateRoot + } _count { transactions } @@ -155,7 +159,7 @@ export default function BlocksPageClient() { height: item.height, hash: item.hash, transactions: item._count?.transactions?.toString(), - stateRoot: item.fromStateRoot, + stateRoot: item.result.stateRoot, })), }); setLoading(false); diff --git a/packages/explorer/src/components/pagination.tsx b/packages/explorer/src/components/pagination.tsx index 645d8d1a4..95eb5901e 100644 --- a/packages/explorer/src/components/pagination.tsx +++ b/packages/explorer/src/components/pagination.tsx @@ -22,8 +22,7 @@ export default function Pagination({ page, totalCount }: PaginationProps) { const nextPage = page + 1; const hasPreviousPage = page > 1; - const hasNextPage = - ((totalCount ?? 0) - page * showPerPage) / showPerPage > 1; + const hasNextPage = page * showPerPage < (totalCount ?? 0); const navigate = (to: number) => { const params = new URLSearchParams(window.location.search); diff --git a/packages/indexer/src/IndexerNotifier.ts b/packages/indexer/src/IndexerNotifier.ts index e29b6497a..3f1ac0256 100644 --- a/packages/indexer/src/IndexerNotifier.ts +++ b/packages/indexer/src/IndexerNotifier.ts @@ -6,14 +6,17 @@ import { TaskPayload, TaskQueue, SequencerIdProvider, + PrivateMempool, } from "@proto-kit/sequencer"; import { log } from "@proto-kit/common"; import { inject } from "tsyringe"; import { IndexBlockTask } from "./tasks/IndexBlockTask"; +import { IndexPendingTxTask } from "./tasks/IndexPendingTxTask"; export type NotifierMandatorySequencerModules = { BlockTrigger: typeof BlockTriggerBase; + Mempool: typeof PrivateMempool; }; @sequencerModule() @@ -24,6 +27,7 @@ export class IndexerNotifier extends SequencerModule> { @inject("TaskQueue") public taskQueue: TaskQueue, public indexBlockTask: IndexBlockTask, + public indexPendingTxTask: IndexPendingTxTask, private readonly sequencerIdProvider: SequencerIdProvider ) { super(); @@ -32,6 +36,7 @@ export class IndexerNotifier extends SequencerModule> { public async propagateEventsAsTasks() { const queue = await this.taskQueue.getQueue(this.indexBlockTask.name); const inputSerializer = this.indexBlockTask.inputSerializer(); + const txInputSerializer = this.indexPendingTxTask.inputSerializer(); this.sequencer.events.on("block-metadata-produced", async (block) => { log.debug( @@ -50,6 +55,26 @@ export class IndexerNotifier extends SequencerModule> { await queue.addTask(task); }); + this.sequencer.events.on("mempool-transaction-added", async (tx) => { + try { + const txQueue = await this.taskQueue.getQueue( + this.indexPendingTxTask.name + ); + const payload = await txInputSerializer.toJSON(tx); + const sequencerId = this.sequencerIdProvider.getSequencerId(); + + const task: TaskPayload = { + name: this.indexPendingTxTask.name, + payload, + flowId: "", + sequencerId, + }; + + await txQueue.addTask(task); + } catch (err) { + console.error("Failed to add pending-tx task", err); + } + }); } public async start(): Promise { diff --git a/packages/indexer/src/index.ts b/packages/indexer/src/index.ts index b8e3049fc..80d0d1bd3 100644 --- a/packages/indexer/src/index.ts +++ b/packages/indexer/src/index.ts @@ -4,3 +4,4 @@ export * from "./IndexerNotifier"; export * from "./api/GeneratedResolverFactoryGraphqlModule"; export * from "./tasks/IndexBlockTask"; export * from "./tasks/IndexBlockTaskParameters"; +export * from "./tasks/IndexPendingTxTask"; diff --git a/packages/indexer/src/tasks/IndexPendingTxTask.ts b/packages/indexer/src/tasks/IndexPendingTxTask.ts new file mode 100644 index 000000000..db468660d --- /dev/null +++ b/packages/indexer/src/tasks/IndexPendingTxTask.ts @@ -0,0 +1,51 @@ +import { + PendingTransaction, + Task, + TaskSerializer, + TaskWorkerModule, + TransactionStorage, +} from "@proto-kit/sequencer"; +import { log } from "@proto-kit/common"; +import { inject, injectable } from "tsyringe"; + +import { IndexPendingTxTaskParametersSerializer } from "./IndexPendingTxTaskParameters"; + +@injectable() +export class IndexPendingTxTask + extends TaskWorkerModule + implements Task +{ + public name = "index-pending-tx"; + + public constructor( + public taskSerializer: IndexPendingTxTaskParametersSerializer, + @inject("TransactionStorage") + public transactionStorage: TransactionStorage + ) { + super(); + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + public async prepare(): Promise {} + + public async compute(input: PendingTransaction): Promise { + try { + await this.transactionStorage.pushUserTransaction(input); + return ""; + } catch (err) { + log.error("Failed to process pending tx task", err); + return undefined; + } + } + + public inputSerializer(): TaskSerializer { + return this.taskSerializer; + } + + public resultSerializer(): TaskSerializer { + return { + fromJSON: async () => {}, + toJSON: async () => "", + }; + } +} diff --git a/packages/indexer/src/tasks/IndexPendingTxTaskParameters.ts b/packages/indexer/src/tasks/IndexPendingTxTaskParameters.ts new file mode 100644 index 000000000..5c60be517 --- /dev/null +++ b/packages/indexer/src/tasks/IndexPendingTxTaskParameters.ts @@ -0,0 +1,22 @@ +import { PendingTransaction } from "@proto-kit/sequencer"; +import { TransactionMapper } from "@proto-kit/persistance"; +import { injectable } from "tsyringe"; + +@injectable() +export class IndexPendingTxTaskParametersSerializer { + public constructor(public transactionMapper: TransactionMapper) {} + + public toJSON(parameters: PendingTransaction): string { + return JSON.stringify({ + tx: this.transactionMapper.mapOut(parameters), + }); + } + + public fromJSON(json: string): PendingTransaction { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const parsed = JSON.parse(json) as { + tx: ReturnType; + }; + return this.transactionMapper.mapIn(parsed.tx); + } +} diff --git a/packages/persistance/src/services/prisma/PrismaBlockStorage.ts b/packages/persistance/src/services/prisma/PrismaBlockStorage.ts index 1030ee269..f3146642e 100644 --- a/packages/persistance/src/services/prisma/PrismaBlockStorage.ts +++ b/packages/persistance/src/services/prisma/PrismaBlockStorage.ts @@ -97,12 +97,16 @@ export class PrismaBlockStorage implements BlockQueue, BlockStorage { const { prismaClient } = this.connection; - await prismaClient.transaction.createMany({ - data: block.transactions.map((txr) => - this.transactionMapper.mapOut(txr.tx) - ), - skipDuplicates: true, - }); + // Note: We can assume all transactions are already in the DB here, because the + // mempool shares the same table as this one. But that could change in the future, + // then transaction have to be inserted-if-missing + // await prismaClient.transaction.createMany({ + // data: block.transactions.map((txr) => + // this.transactionMapper.mapOut(txr.tx) + // ), + // skipDuplicates: true, + // }); + await prismaClient.block.create({ data: { ...encodedBlock,