Skip to content

Commit

Permalink
Feat: Search by slot index or commitmentId (#1320)
Browse files Browse the repository at this point in the history
* fix: Remove double network in path from Slot table links

* fix: Slot page infinite loader

* feat: Fix search by slot index

* feat: Support slot search by commitment Id. Show commitmentId on slot page

* fix: Add network in link paths of useGenerateSlotsTable

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
  • Loading branch information
msarcev and begonaalvarezd authored Mar 27, 2024
1 parent 3278cf1 commit 376813f
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 25 deletions.
7 changes: 6 additions & 1 deletion api/src/models/api/nova/ISearchResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Block, OutputWithMetadataResponse } from "@iota/sdk-nova";
import { IAddressDetails } from "./IAddressDetails";
import { IResponse } from "./IResponse";
import { ITaggedOutputsResponse } from "./ITaggedOutputsResponse";
import { IResponse } from "../IResponse";

export interface ISearchResponse extends IResponse {
/**
Expand Down Expand Up @@ -46,6 +46,11 @@ export interface ISearchResponse extends IResponse {
*/
transactionBlock?: Block;

/**
* A slot index.
*/
slotIndex?: string;

/**
* Basic and/or Nft tagged output ids.
*/
Expand Down
15 changes: 15 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,21 @@ export class NovaApiService {
}
}

/**
* Get the slot commitment by commitment id.
* @param slotCommitmentId The slot commitment id to get the commitment for.
* @returns The slot commitment.
*/
public async getCommitment(slotCommitmentId: string): Promise<ISlotResponse> {
try {
const slot = await this.client.getCommitment(slotCommitmentId);

return { slot };
} catch (e) {
logger.error(`Failed fetching slot with commitment id ${slotCommitmentId}. Cause: ${e}`);
}
}

/**
* Get the epoch committee.
* @param epochIndex The epoch index to get the committee for.
Expand Down
30 changes: 29 additions & 1 deletion api/src/utils/nova/searchExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ export class SearchExecutor {
);
}

if (searchQuery.slotIndex) {
promises.push(
this.executeQuery(
this.apiService.getSlotCommitment(searchQuery.slotIndex),
(_) => {
promisesResult = {
slotIndex: String(searchQuery.slotIndex),
};
},
"Slot commitment fetch failed",
),
);
}

if (searchQuery.slotCommitmentId) {
promises.push(
this.executeQuery(
this.apiService.getCommitment(searchQuery.slotCommitmentId),
(result) => {
promisesResult = {
slotIndex: String(result.slot.slot),
};
},
"Slot commitment fetch failed",
),
);
}

if (searchQuery.tag) {
promises.push(
this.executeQuery(
Expand Down Expand Up @@ -170,7 +198,7 @@ export class SearchExecutor {
};
}

return {};
return { message: "Nothing found" };
}

private async executeQuery<T>(query: Promise<T>, successHandler: (result: T) => void, failureMessage: string): Promise<void> {
Expand Down
7 changes: 7 additions & 0 deletions api/src/utils/nova/searchQueryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface SearchQuery {
* The tag of an output.
*/
tag?: HexEncodedString;
/**
* A slot commitment id.
*/
slotCommitmentId?: string;
}

/**
Expand Down Expand Up @@ -102,6 +106,7 @@ export class SearchQueryBuilder {
let anchorId: string;
let delegationId: string;
let tag: string;
let slotCommitmentId: string;

const queryDetails = AddressHelper.buildAddress(this.networkBechHrp, this.queryLower);
const slotIndex = /^\d+$/.test(this.query) ? Number.parseInt(this.query, 10) : undefined;
Expand All @@ -114,6 +119,7 @@ export class SearchQueryBuilder {
if (queryDetails?.hex && queryDetails.hex.length === 74) {
blockId = queryDetails.hex;
transactionId = queryDetails.hex;
slotCommitmentId = queryDetails.hex;
} else if (queryDetails?.hex && queryDetails.hex.length === 66) {
// if the hex has 66 characters it might be a accoount id or a nft id
accountId = queryDetails.hex;
Expand Down Expand Up @@ -157,6 +163,7 @@ export class SearchQueryBuilder {
anchorId,
delegationId,
tag,
slotCommitmentId,
};
}
}
10 changes: 6 additions & 4 deletions client/src/app/components/nova/slot/blocks/SlotBlocksSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import SlotBlocksSectionRow from "./SlotBlocksSectionRow";
import Spinner from "~/app/components/Spinner";
import Pagination from "~/app/components/Pagination";
import { ISlotBlock } from "~/models/api/nova/ISlotBlocksResponse";
import useSlotBlocks from "~/helpers/nova/hooks/useSlotBlocks";
import "./SlotBlocksSection.scss";

interface SlotBlocksSectionProps {
readonly blocks: ISlotBlock[] | null;
readonly slotIndex: string | null;
}

const PAGE_SIZE: number = 10;

const SlotBlocksSection: React.FC<SlotBlocksSectionProps> = ({ blocks }) => {
const SlotBlocksSection: React.FC<SlotBlocksSectionProps> = ({ slotIndex }) => {
const { blocks, isLoading } = useSlotBlocks(slotIndex);
const [currentPage, setCurrentPage] = useState<ISlotBlock[]>([]);
const [pageNumber, setPageNumber] = useState(1);

Expand Down Expand Up @@ -50,7 +52,7 @@ const SlotBlocksSection: React.FC<SlotBlocksSectionProps> = ({ blocks }) => {
</div>
</React.Fragment>
) : (
<Spinner />
isLoading && <Spinner />
)}
{showPagination && (
<Pagination
Expand All @@ -66,7 +68,7 @@ const SlotBlocksSection: React.FC<SlotBlocksSectionProps> = ({ blocks }) => {
};

SlotBlocksSection.defaultProps = {
blocks: undefined,
slotIndex: null,
};

export default SlotBlocksSection;
25 changes: 16 additions & 9 deletions client/src/app/routes/nova/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,15 @@ const Search: React.FC<RouteComponentProps<SearchRouteProps>> = (props) => {
network,
query: queryTerm,
});
if (response && Object.keys(response).length > 0) {
if (!response || response?.error || response?.message) {
setState((prevState) => ({
...prevState,
completion: response?.error ? "invalid" : "notFound",
invalidError: response?.error ?? response?.message ?? "",
status: "",
statusBusy: false,
}));
} else if (Object.keys(response).length > 0) {
const routeSearch = new Map<string, string>();
let route = "";
let routeParam = query;
Expand Down Expand Up @@ -102,6 +110,9 @@ const Search: React.FC<RouteComponentProps<SearchRouteProps>> = (props) => {
} else if (response.foundryId) {
route = "foundry";
routeParam = response.foundryId;
} else if (response.slotIndex) {
route = "slot";
routeParam = response.slotIndex.toString();
} else if (response.taggedOutputs) {
route = "outputs";
redirectState = {
Expand Down Expand Up @@ -131,14 +142,6 @@ const Search: React.FC<RouteComponentProps<SearchRouteProps>> = (props) => {
search: getEncodedSearch(),
redirectState,
}));
} else {
setState((prevState) => ({
...prevState,
completion: response?.error ? "invalid" : "notFound",
invalidError: response?.error ?? "",
status: "",
statusBusy: false,
}));
}
}, 0);
} else {
Expand Down Expand Up @@ -240,6 +243,10 @@ const Search: React.FC<RouteComponentProps<SearchRouteProps>> = (props) => {
<span>NFT Id</span>
<span>66 Hex characters</span>
</li>
<li>
<span>Slot</span>
<span>Index or commitmentId of finalized slot</span>
</li>
</ul>
<br />
<p>Please perform another search with a valid hash.</p>
Expand Down
10 changes: 6 additions & 4 deletions client/src/app/routes/nova/SlotPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { RouteComponentProps } from "react-router-dom";
import StatusPill from "~/app/components/nova/StatusPill";
import { getSlotStatusFromLatestSlotCommitments, parseSlotIndexFromParams } from "~/app/lib/utils/slot.utils";
import { SLOT_STATUS_TO_PILL_STATUS } from "~/app/lib/constants/slot.constants";
import useSlotBlocks from "~/helpers/nova/hooks/useSlotBlocks";
import SlotBlocksSection from "~/app/components/nova/slot/blocks/SlotBlocksSection";
import { useSlotManaBurned } from "~/helpers/nova/hooks/useSlotManaBurned";
import "./SlotPage.scss";
Expand All @@ -23,19 +22,22 @@ export default function SlotPage({
slotIndex: string;
}>): React.JSX.Element {
const { latestSlotCommitments = [] } = useSlotsFeed();
const { slotCommitment: slotCommitmentDetails } = useSlotDetails(network, slotIndex);
const { slotCommitment: slotCommitmentDetails, slotCommitmentId } = useSlotDetails(network, slotIndex);
const { slotManaBurned } = useSlotManaBurned(slotIndex);

const parsedSlotIndex = parseSlotIndexFromParams(slotIndex);
const slotStatus = getSlotStatusFromLatestSlotCommitments(parsedSlotIndex, latestSlotCommitments);
const slotFromSlotCommitments = latestSlotCommitments.find((slot) => slot.slotCommitment.slot === parsedSlotIndex);
const { blocks } = useSlotBlocks(network, slotIndex);

const dataRows: IPageDataRow[] = [
{
label: "Slot Index",
value: parsedSlotIndex ?? "-",
},
{
label: "Commitment Id",
value: slotCommitmentId ?? "-",
},
{
label: "RMC",
value:
Expand Down Expand Up @@ -74,7 +76,7 @@ export default function SlotPage({
<NotFound query={slotIndex} searchTarget="slot" />
)}

<SlotBlocksSection blocks={blocks} />
<SlotBlocksSection slotIndex={slotIndex} />
</div>
</div>
</section>
Expand Down
4 changes: 2 additions & 2 deletions client/src/helpers/nova/hooks/useGenerateSlotsTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { SlotTableCellType, type TSlotTableData } from "~/app/components/nova/la
import { SlotStatus } from "~app/lib/enums";
import { SlotTableHeadings } from "~/app/lib/ui/enums";
import { Utils } from "@iota/sdk-wasm-nova/web";
import { useNetworkInfoNova } from "../networkInfo";
import useSlotsFeed from "./useSlotsFeed";
import { useNovaTimeConvert } from "./useNovaTimeConvert";
import { useNetworkInfoNova } from "../networkInfo";
import moment from "moment";

type SlotTimeRange = {
Expand Down Expand Up @@ -168,9 +168,9 @@ function getSlotCommitmentTableRow(
}

export function useGenerateSlotsTable(): ITableRow<TSlotTableData>[] {
const { name: network } = useNetworkInfoNova((s) => s.networkInfo);
const { slotIndexToUnixTimeRange } = useNovaTimeConvert();
const { currentSlotIndex, currentSlotTimeRange, latestSlotCommitments, latestSlotIndexes } = useSlotsFeed();
const { name: network } = useNetworkInfoNova((s) => s.networkInfo);

const rows: ITableRow<TSlotTableData>[] = [];

Expand Down
4 changes: 3 additions & 1 deletion client/src/helpers/nova/hooks/useSlotBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { useIsMounted } from "~/helpers/hooks/useIsMounted";
import { ISlotBlock } from "~/models/api/nova/ISlotBlocksResponse";
import { NOVA } from "~/models/config/protocolVersion";
import { NovaApiClient } from "~/services/nova/novaApiClient";
import { useNetworkInfoNova } from "../networkInfo";

interface IUseSlotBlocks {
blocks: ISlotBlock[] | null;
isLoading: boolean;
error: string | undefined;
}

export default function useSlotBlocks(network: string, slotIndex: string): IUseSlotBlocks {
export default function useSlotBlocks(slotIndex: string | null): IUseSlotBlocks {
const { name: network } = useNetworkInfoNova((s) => s.networkInfo);
const isMounted = useIsMounted();
const [apiClient] = useState(ServiceFactory.get<NovaApiClient>(`api-client-${NOVA}`));
const [slotBlocks, setSlotBlocks] = useState<ISlotBlock[] | null>(null);
Expand Down
12 changes: 9 additions & 3 deletions client/src/helpers/nova/hooks/useSlotDetails.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SlotCommitment } from "@iota/sdk-wasm-nova/web";
import { SlotCommitment, Utils } from "@iota/sdk-wasm-nova/web";
import { plainToInstance } from "class-transformer";
import { useEffect, useState } from "react";
import { ServiceFactory } from "~/factories/serviceFactory";
Expand All @@ -8,6 +8,7 @@ import { NovaApiClient } from "~/services/nova/novaApiClient";

interface IUseSlotDetails {
slotCommitment: SlotCommitment | null;
slotCommitmentId: string | null;
error: string | undefined;
isLoading: boolean;
}
Expand All @@ -16,12 +17,14 @@ export default function useSlotDetails(network: string, slotIndex: string): IUse
const isMounted = useIsMounted();
const [apiClient] = useState(ServiceFactory.get<NovaApiClient>(`api-client-${NOVA}`));
const [slotCommitment, setSlotCommitment] = useState<SlotCommitment | null>(null);
const [slotCommitmentId, setSlotCommitmentId] = useState<string | null>(null);
const [error, setError] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(true);

useEffect(() => {
setIsLoading(true);
setSlotCommitment(null);
setSlotCommitmentId(null);
if (!slotCommitment) {
// eslint-disable-next-line no-void
void (async () => {
Expand All @@ -32,8 +35,10 @@ export default function useSlotDetails(network: string, slotIndex: string): IUse
})
.then((response) => {
if (isMounted) {
const slot = plainToInstance(SlotCommitment, response.slot) as unknown as SlotCommitment;
setSlotCommitment(slot);
const slotCommitment = plainToInstance(SlotCommitment, response.slot) as unknown as SlotCommitment;
const slotCommitmentId = Utils.computeSlotCommitmentId(slotCommitment);
setSlotCommitment(slotCommitment);
setSlotCommitmentId(slotCommitmentId);
setError(response.error);
}
})
Expand All @@ -48,6 +53,7 @@ export default function useSlotDetails(network: string, slotIndex: string): IUse

return {
slotCommitment,
slotCommitmentId,
error,
isLoading,
};
Expand Down
5 changes: 5 additions & 0 deletions client/src/models/api/nova/ISearchResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export interface ISearchResponse extends IResponse {
*/
transactionBlock?: Block;

/**
* A slot index.
*/
slotIndex?: string;

/**
* Basic and/or Nft tagged output ids.
*/
Expand Down

0 comments on commit 376813f

Please sign in to comment.