Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions apps/explorer/src/hooks/useGetEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useQuery } from '@tanstack/react-query';
import { type IotaEvent, type EventId } from '@iota/iota-sdk/client';
import { useIotaClient } from '@iota/dapp-kit';

type UseGetEventsProps = {
eventType: string;
objectId: string; // Optional objectId for more specific queries
limit?: number | null;
order?: 'ascending' | 'descending';
};

const QUERY_MAX_RESULT_LIMIT = 50;

/**
* A generic hook to query for Move events from the IOTA network.
*/
export function useGetEvents({ eventType, objectId, limit, order }: UseGetEventsProps) {
const client = useIotaClient();

// The query key will include all parameters to ensure uniqueness
const queryKey = ['events', eventType, objectId, limit, order];

return useQuery<IotaEvent[], Error>({
queryKey,
queryFn: async () => {
if (!limit) {
// Do some validation at the runtime level for some extra type-safety
// https://tkdodo.eu/blog/react-query-and-type-script#type-safety-with-the-enabled-option

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

throw new Error(
`Limit needs to always be defined and non-zero! Received ${limit} instead.`,
);
}

if (!eventType) {
// Return empty array if eventType is not provided
return [];
}

// The full event type name might need to be constructed based on the package, module, and event name.
// For now, we'll assume eventType is the full name.
// Example for audit trail: '0xabcde...::audit_trail::CapabilityIssued'
// const fullEventType = eventType;

const results: IotaEvent[] = [];
let currCursor: EventId | null | undefined;
let hasNextPage = true;

// Handle pagination similar to useGetValidatorsEvents
while (hasNextPage && results.length < limit) {
const response = await client.queryEvents({
query: {
// Event Filter not supported
MoveEventField: {
path: '/parsedJson',
value: null,
},
},
// query: {
// MoveEventType: fullEventType,
// },
// query: {
// And: [
// { MoveEventType: fullEventType },
// {
// MoveEventField: {
// path: 'parsedJson.target_key',
// value: objectId,
// },
// },
// ],
// },
cursor: currCursor,
limit: Math.min(limit, QUERY_MAX_RESULT_LIMIT),
order,
});

results.push(...(response.data as IotaEvent[]));
hasNextPage = response.hasNextPage;
currCursor = response.nextCursor;
}

return results.slice(0, limit);
},
enabled: !!client && !!eventType, // The query will not run until the client and eventType are available
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { TransactionsView } from '../common/TransactionsView';
import { AuditTrailSummaryView } from './views/AuditTrailSummaryView';
import { MetadataView } from './views/MetadataView';
import { LockLifecycleView } from './views/lock-lifecycle/LockLifecycleView';
import { CapabilitiesView } from './views/CapabilitiesView';
import { RolesView } from './views/roles/RolesView';
import { TagsView } from './views/TagsView';
import { RecordsView } from './views/RecordsView';
Expand Down Expand Up @@ -126,6 +127,7 @@ export function AuditTrailContent({ objectId }: AuditTrailContentProps) {
secondPanel={<MetadataView auditTrail={auditTrailObject} />}
/>
<RecordsView objectId={objectId} auditTrail={auditTrailHandle} />
<CapabilitiesView objectId={objectId} />
<SideBySidePanels
ratio="66-34"
firstPanel={<RolesView roles={auditTrailObject.roles.roles} />}
Expand Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be removed.

Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

// apps/explorer/src/pages/trust-framework/audit-trail-result/mockCapabilities.ts

export interface Capability {
holderAddress: string;
role: string;
status: 'active' | 'revoked';
validFrom: Date | null;
validUntil: Date | null;
}

export const mockCapabilities: Capability[] = [

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer Comment: // TODO: [Insert Issue #] Replace this mock data with real capability resolution once the SDK supports it.

{
holderAddress: '0x1234567890123456789012345678901234567890',
role: 'Admin',
status: 'active',
validFrom: new Date('2023-01-01'),
validUntil: new Date('2025-01-01'),
},
{
holderAddress: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
role: 'Auditor',
status: 'active',
validFrom: new Date(),
validUntil: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
},
{
holderAddress: '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
role: 'Viewer',
status: 'revoked',
validFrom: null,
validUntil: null,
},
{
holderAddress: '0x1111111111111111111111111111111111111111',
role: 'Contributor',
status: 'active',
validFrom: new Date('2022-01-01'),
validUntil: new Date('2023-01-01'),
},
{
holderAddress: '0x2222222222222222222222222222222222222222',
role: 'Editor',
status: 'active',
validFrom: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
validUntil: new Date(new Date().setDate(new Date().getDate() + 60)), // 60 days from now
},
{
holderAddress: '0x3333333333333333333333333333333333333333',
role: 'Admin',
status: 'active',
validFrom: new Date('2023-01-01'),
validUntil: new Date('2025-01-01'),
},
{
holderAddress: '0x4444444444444444444444444444444444444444',
role: 'Auditor',
status: 'revoked',
validFrom: new Date(),
validUntil: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
},
{
holderAddress: '0x5555555555555555555555555555555555555555',
role: 'Viewer',
status: 'active',
validFrom: null,
validUntil: null,
},
{
holderAddress: '0x6666666666666666666666666666666666666666',
role: 'Contributor',
status: 'active',
validFrom: new Date('2022-01-01'),
validUntil: new Date('2023-01-01'),
},
{
holderAddress: '0x7777777777777777777777777777777777777777',
role: 'Editor',
status: 'revoked',
validFrom: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
validUntil: new Date(new Date().setDate(new Date().getDate() + 60)), // 60 days from now
},
{
holderAddress: '0x8888888888888888888888888888888888888888',
role: 'Admin',
status: 'active',
validFrom: new Date('2023-01-01'),
validUntil: new Date('2025-01-01'),
},
{
holderAddress: '0x9999999999999999999999999999999999999999',
role: 'Auditor',
status: 'active',
validFrom: new Date(),
validUntil: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
},
{
holderAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
role: 'Viewer',
status: 'revoked',
validFrom: null,
validUntil: null,
},
{
holderAddress: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
role: 'Contributor',
status: 'active',
validFrom: new Date('2022-01-01'),
validUntil: new Date('2023-01-01'),
},
{
holderAddress: '0xcccccccccccccccccccccccccccccccccccccccc',
role: 'Editor',
status: 'active',
validFrom: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
validUntil: new Date(new Date().setDate(new Date().getDate() + 60)), // 60 days from now
},
{
holderAddress: '0xdddddddddddddddddddddddddddddddddddddddd',
role: 'Admin',
status: 'revoked',
validFrom: new Date('2023-01-01'),
validUntil: new Date('2025-01-01'),
},
{
holderAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
role: 'Auditor',
status: 'active',
validFrom: new Date(),
validUntil: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
},
{
holderAddress: '0xffffffffffffffffffffffffffffffffffffffff',
role: 'Viewer',
status: 'active',
validFrom: null,
validUntil: null,
},
{
holderAddress: '0x0000000000000000000000000000000000000001',
role: 'Contributor',
status: 'revoked',
validFrom: new Date('2022-01-01'),
validUntil: new Date('2023-01-01'),
},
{
holderAddress: '0x0000000000000000000000000000000000000002',
role: 'Editor',
status: 'active',
validFrom: new Date(new Date().setDate(new Date().getDate() + 30)), // 30 days from now
validUntil: new Date(new Date().setDate(new Date().getDate() + 60)), // 60 days from now
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';

import { mockCapabilities } from './mockCapabilities';

export const DEFAULT_CAPABILITIES_LIMIT = 10;

export enum CapabilityFilterValue {
Issued = 'Issued',
Revoked = 'Revoked',
}

export function useCapabilities(filter: CapabilityFilterValue, limit = DEFAULT_CAPABILITIES_LIMIT) {
return useInfiniteQuery({
queryKey: ['get-capabilities', filter, limit],
queryFn: async ({ pageParam = 0 }) => {
const allCapabilities = mockCapabilities.filter((c) => {
if (filter === CapabilityFilterValue.Issued) {
return c.status !== 'revoked';
}
return c.status === 'revoked';
});

const start = pageParam * limit;
const end = start + limit;
const page = allCapabilities.slice(start, end);

// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));

return {
data: page,
nextCursor: allCapabilities.length > end ? pageParam + 1 : null,
hasNextPage: allCapabilities.length > end,
};
},
initialPageParam: 0,
getNextPageParam: ({ hasNextPage, nextCursor }) => (hasNextPage ? nextCursor : null),
staleTime: 10 * 1000,
retry: false,
placeholderData: keepPreviousData,
});
}
Loading
Loading