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(

Events

-
+
- 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); + }); } }