diff --git a/src/explorer/Transaction.vue b/src/explorer/Transaction.vue
index cb50c88..b54dbcb 100644
--- a/src/explorer/Transaction.vue
+++ b/src/explorer/Transaction.vue
@@ -9,6 +9,7 @@ import { useRoute, useRouter } from "vue-router";
import type { TransactionInfo } from "@/state/transactions";
import type { ProofInfo } from "@/state/proofs";
import { decodeBlobData } from "@/explorer/utils/blobDecoder";
+import { TxEventProcessor } from "@/services/eventProcessor";
const route = useRoute();
const router = useRouter();
@@ -316,22 +317,22 @@ watch(
-
+
- Event #{{ index + 1 }}: {{ event.name }}
+ {{ event.name }}
- Block: {{ event.block_hash }}
+ Block {{ event.block_height }} -- {{ event.block_hash }}
{{
- JSON.stringify(event.metadata, null, 2)
+ TxEventProcessor.processEvent(event)
}}
-
+
diff --git a/src/services/eventProcessor.ts b/src/services/eventProcessor.ts
new file mode 100644
index 0000000..523f31c
--- /dev/null
+++ b/src/services/eventProcessor.ts
@@ -0,0 +1,356 @@
+/**
+ * Event processor for handling Rust TxEvent variants
+ * Maps complex Rust event structures to readable JavaScript objects
+ */
+
+import { EventInfo } from "@/state/transactions";
+
+export interface ProcessedEvent {
+ type?: string;
+ txHash?: string;
+ laneId?: string;
+ sequenceNumber?: number;
+ contractName?: string;
+ programId?: string;
+ error?: string;
+ blobIndex?: number;
+ success?: boolean;
+ reason?: string;
+ additionalData?: Record
;
+}
+
+export class TxEventProcessor {
+ /**
+ * Process a raw event metadata object based on the event name
+ */
+ static processEvent(event: EventInfo): ProcessedEvent {
+ const { name: name, metadata } = event;
+ if (!metadata) {
+ return { type: name };
+ }
+ switch (name) {
+ case 'RejectedBlobTransaction':
+ return this.processRejectedBlobTransaction(metadata);
+
+ case 'DuplicateBlobTransaction':
+ return this.processDuplicateBlobTransaction(metadata);
+
+ case 'SequencedBlobTransaction':
+ return this.processSequencedBlobTransaction(metadata);
+
+ case 'SequencedProofTransaction':
+ return this.processSequencedProofTransaction(metadata);
+
+ case 'Settled':
+ return this.processSettled(metadata);
+
+ case 'SettledAsFailed':
+ return this.processSettledAsFailed(metadata);
+
+ case 'TimedOut':
+ return this.processTimedOut(metadata);
+
+ case 'TxError':
+ return this.processTxError(metadata);
+
+ case 'NewProof':
+ return this.processNewProof(metadata);
+
+ case 'BlobSettled':
+ return this.processBlobSettled(metadata);
+
+ case 'ContractDeleted':
+ return this.processContractDeleted(metadata);
+
+ case 'ContractRegistered':
+ return this.processContractRegistered(metadata);
+
+ case 'ContractStateUpdated':
+ return this.processContractStateUpdated(metadata);
+
+ case 'ContractProgramIdUpdated':
+ return this.processContractProgramIdUpdated(metadata);
+
+ case 'ContractTimeoutWindowUpdated':
+ return this.processContractTimeoutWindowUpdated(metadata);
+
+ default:
+ return {
+ type: name,
+ additionalData: metadata
+ };
+ }
+ }
+
+ private static processRejectedBlobTransaction(metadata: Record): ProcessedEvent {
+ // RejectedBlobTransaction(&'a TxHash, &'a LaneId, u32, &'a BlobTransaction, &'a Arc)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ laneId: this.extractLaneId(metadata, 1),
+ reason: 'Transaction rejected during blob processing',
+ additionalData: {
+ context: this.extractTxContext(metadata, 4)
+ }
+ };
+ }
+
+ private static processDuplicateBlobTransaction(metadata: Record): ProcessedEvent {
+ // DuplicateBlobTransaction(&'a TxHash)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ reason: 'Duplicate blob transaction detected'
+ };
+ }
+
+ private static processSequencedBlobTransaction(metadata: Record): ProcessedEvent {
+ // SequencedBlobTransaction(&'a TxHash, &'a LaneId, u32, &'a BlobTransaction, &'a Arc)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ laneId: this.extractLaneId(metadata, 1),
+ additionalData: {
+ context: this.extractTxContext(metadata, 4)
+ }
+ };
+ }
+
+ private static processSequencedProofTransaction(metadata: Record): ProcessedEvent {
+ // SequencedProofTransaction(&'a TxHash, &'a LaneId, u32, &'a VerifiedProofTransaction)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ laneId: this.extractLaneId(metadata, 1),
+ additionalData: {
+ }
+ };
+ }
+
+ private static processSettled(metadata: Record): ProcessedEvent {
+ // Settled(&'a TxHash, &'a UnsettledBlobTransaction)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ additionalData: {
+ }
+ };
+ }
+
+ private static processSettledAsFailed(metadata: Record): ProcessedEvent {
+ // SettledAsFailed(&'a TxHash, &'a UnsettledBlobTransaction, &'a str)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ error: this.extractString(metadata, 2),
+ additionalData: {
+ }
+ };
+ }
+
+ private static processTimedOut(metadata: Record): ProcessedEvent {
+ // TimedOut(&'a TxHash, &'a UnsettledBlobTransaction)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ reason: 'Transaction timed out',
+ additionalData: {
+ unsettledBlobTransaction: this.extractUnsettledBlobTransaction(metadata, 1)
+ }
+ };
+ }
+
+ private static processTxError(metadata: Record): ProcessedEvent {
+ // TxError(&'a TxHash, &'a str)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ error: this.extractString(metadata, 1)
+ };
+ }
+
+ private static processNewProof(metadata: Record): ProcessedEvent {
+ // NewProof(&'a TxHash, &'a Blob, BlobIndex, &'a (ProgramId, Verifier, TxHash, HyliOutput), usize)
+ const hyliOutputTuple = this.extractTuple(metadata, 3);
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ blobIndex: this.extractBlobIndex(metadata, 2),
+ additionalData: {
+ blob: this.extractBlob(metadata, 1),
+ verifier: hyliOutputTuple?.[1],
+ hyliOutput: hyliOutputTuple?.[3],
+ proofIndex: this.extractUsize(metadata, 4)
+ }
+ };
+ }
+
+ private static processBlobSettled(metadata: Record): ProcessedEvent {
+ // BlobSettled(&'a TxHash, &'a UnsettledBlobTransaction, &'a Blob, BlobIndex, Option<&'a (ProgramId, Verifier, TxHash, HyliOutput)>, usize)
+ const hyliOutputTuple = this.extractOptionalTuple(metadata, 4);
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ blobIndex: this.extractBlobIndex(metadata, 3),
+ additionalData: {
+ blob: this.extractBlob(metadata, 2),
+ verifier: hyliOutputTuple?.[1],
+ proofIndex: this.extractUsize(metadata, 5)
+ }
+ };
+ }
+
+ private static processContractDeleted(metadata: Record): ProcessedEvent {
+ // ContractDeleted(&'a TxHash, &'a ContractName)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ contractName: this.extractContractName(metadata, 1),
+ };
+ }
+
+ private static processContractRegistered(metadata: Record): ProcessedEvent {
+ // ContractRegistered(&'a TxHash, &'a ContractName, &'a Contract, &'a Option>)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ contractName: this.extractContractName(metadata, 1),
+ additionalData: {
+ contract: this.extractContract(metadata, 2),
+ initData: this.extractOptionalBytes(metadata, 3)
+ }
+ };
+ }
+
+ private static processContractStateUpdated(metadata: Record): ProcessedEvent {
+ // ContractStateUpdated(&'a TxHash, &'a ContractName, &'a Contract, &'a StateCommitment)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ contractName: this.extractContractName(metadata, 1),
+ additionalData: {
+ contract: this.extractContract(metadata, 2),
+ stateCommitment: this.extractStateCommitment(metadata, 3)
+ }
+ };
+ }
+
+ private static processContractProgramIdUpdated(metadata: Record): ProcessedEvent {
+ // ContractProgramIdUpdated(&'a TxHash, &'a ContractName, &'a Contract, &'a ProgramId)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ contractName: this.extractContractName(metadata, 1),
+ programId: this.extractProgramId(metadata, 3),
+ additionalData: {
+ contract: this.extractContract(metadata, 2)
+ }
+ };
+ }
+
+ private static processContractTimeoutWindowUpdated(metadata: Record): ProcessedEvent {
+ // ContractTimeoutWindowUpdated(&'a TxHash, &'a ContractName, &'a Contract, &'a TimeoutWindow)
+ return {
+ txHash: this.extractTxHash(metadata, 0),
+ contractName: this.extractContractName(metadata, 1),
+ additionalData: {
+ contract: this.extractContract(metadata, 2),
+ timeoutWindow: this.extractTimeoutWindow(metadata, 3)
+ }
+ };
+ }
+
+ // Helper methods for extracting typed data from metadata
+ private static extractTxHash(metadata: Record, index: number): string | undefined {
+ return metadata[index][1]?.toString();
+ }
+
+ private static extractLaneId(metadata: Record, index: number): string | undefined {
+ return metadata[index]?.toString();
+ }
+
+ private static extractUsize(metadata: Record, index: number): number | undefined {
+ const value = metadata[index];
+ return typeof value === 'number' ? value : undefined;
+ }
+
+ private static extractString(metadata: Record, index: number): string | undefined {
+ return metadata[index]?.toString();
+ }
+
+ private static extractContractName(metadata: Record, index: number): string | undefined {
+ return metadata[index]?.toString();
+ }
+
+ private static extractProgramId(metadata: Record, index: number): string | undefined {
+ return metadata[index]?.toString();
+ }
+
+ private static extractBlobIndex(metadata: Record, index: number): number | undefined {
+ const value = metadata[index];
+ return typeof value === 'number' ? value : undefined;
+ }
+
+ private static extractTxContext(metadata: Record, index: number): any {
+ return metadata[index];
+ }
+
+ private static extractUnsettledBlobTransaction(metadata: Record, index: number): any {
+ return metadata[index];
+ }
+
+ private static extractBlob(metadata: Record, index: number): any {
+ let blob = metadata[index];
+ console.log(blob);
+ if (blob && blob.data) {
+ blob.data = Array.isArray(blob.data)
+ ? blob.data.map((byte: { toString: (arg0: number) => string; }) => byte.toString(16).padStart(2, '0')).join('')
+ : blob.data;
+ }
+ return blob;
+ }
+
+ private static extractContract(metadata: Record, index: number): any {
+ return metadata[index];
+ }
+
+ private static extractStateCommitment(metadata: Record, index: number): any {
+ return metadata[index];
+ }
+
+ private static extractTimeoutWindow(metadata: Record, index: number): any {
+ return metadata[index];
+ }
+
+ private static extractTuple(metadata: Record, index: number): any[] | undefined {
+ const value = metadata[index];
+ return Array.isArray(value) ? value : undefined;
+ }
+
+ private static extractOptionalTuple(metadata: Record, index: number): any[] | undefined {
+ const value = metadata[index];
+ return value && Array.isArray(value) ? value : undefined;
+ }
+
+ private static extractOptionalBytes(metadata: Record, index: number): number[] | undefined {
+ const value = metadata[index];
+ return value && Array.isArray(value) ? value : undefined;
+ }
+
+ /**
+ * Get the status/severity level of the event
+ */
+ static getEventStatus(processedEvent: ProcessedEvent): 'success' | 'error' | 'warning' | 'info' {
+ if (processedEvent.success === false || processedEvent.error) {
+ return 'error';
+ }
+
+ switch (processedEvent.type) {
+ case 'RejectedBlobTransaction':
+ case 'DuplicateBlobTransaction':
+ case 'TxError':
+ return 'warning';
+
+ case 'SettledAsFailed':
+ case 'TimedOut':
+ return 'error';
+
+ case 'Settled':
+ case 'SequencedBlobTransaction':
+ case 'SequencedProofTransaction':
+ case 'NewProof':
+ case 'BlobSettled':
+ case 'ContractRegistered':
+ return 'success';
+
+ default:
+ return 'info';
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/state/transactions.ts b/src/state/transactions.ts
index 89e7717..4193fc0 100644
--- a/src/state/transactions.ts
+++ b/src/state/transactions.ts
@@ -1,4 +1,4 @@
-import { getNetworkIndexerApiUrl, network } from "@/state/network";
+import { getNetworkIndexerApiUrl } from "@/state/network";
export type HyliOutput = {
blobs: number[];
@@ -23,7 +23,9 @@ export type BlobInfo = {
export type EventInfo = {
name: string;
block_hash: string;
- metadata?: Record;
+ block_height: number;
+ index: number;
+ metadata?: any;
};
export type TransactionInfo = {
@@ -129,24 +131,27 @@ export class TransactionStore {
`${getNetworkIndexerApiUrl(this.network)}/v1/indexer/transaction/hash/${tx.tx_hash}/events?no_cache=${Date.now()}`,
);
const eventsData = await eventsResponse.json();
- // Preserve block_hash and other metadata when flattening events
- // Retrocompatibility
- if (network.value === "first-testnet")
- tx.events = eventsData.flatMap((eventEntry: { block_hash: string; events: Omit[] }) =>
- (eventEntry.events || []).map((event) => ({
- ...event,
- block_hash: eventEntry.block_hash,
- })),
- );
- else {
- tx.events = eventsData.flatMap((eventEntry: { block_hash: string; events: Omit[] }) =>
- (eventEntry.events || []).map((event) => ({
- name: Object.keys(event)[0],
- metadata: (event as any)[Object.keys(event)[0]][1], // Common format -> get the actual meat of the event
+
+ const events = eventsData.flatMap((eventEntry: { block_hash: string; block_height: number; events: any[] }) =>
+ (eventEntry.events || []).map((event) => {
+ const eventName = Object.keys(event).find(key => key !== 'index') || '';
+ console.log(event);
+ return {
+ name: eventName,
block_hash: eventEntry.block_hash,
- })),
- );
- }
+ block_height: eventEntry.block_height,
+ index: event.index || 0,
+ metadata: event[eventName],
+ };
+ }),
+ );
+ tx.events = events.sort((a: EventInfo, b: EventInfo) => {
+ const heightDiff = (a.block_height ?? 0) - (b.block_height ?? 0);
+ if (heightDiff !== 0) {
+ return heightDiff;
+ }
+ return (a.index ?? 0) - (b.index ?? 0);
+ });
}
}