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,