• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cartesi / rollups-explorer / 11943333351

20 Nov 2024 11:09PM UTC coverage: 86.535% (-0.07%) from 86.605%
11943333351

push

github

brunomenezes
chore(ci): Set the Preview endpoint when available in the CI settings.

1276 of 1529 branches covered (83.45%)

Branch coverage included in aggregate %.

9077 of 10435 relevant lines covered (86.99%)

44.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.81
/apps/web/src/components/inputs/inputDetailsView.tsx
1
"use client";
1✔
2
import { ContentType, InputDetails } from "@cartesi/rollups-explorer-ui";
1✔
3
import { Alert, Box, Group, Select, Stack, Text } from "@mantine/core";
1✔
4
import { find, omit, pathOr } from "ramda";
1✔
5
import { included, isNilOrEmpty, isNotNilOrEmpty } from "ramda-adjunct";
1✔
6
import { FC, useEffect, useMemo, useState } from "react";
1✔
7
import { TbExclamationCircle } from "react-icons/tb";
1✔
8
import { UseQueryResponse, useQuery } from "urql";
1✔
9
import { Address, Hex } from "viem";
10
import { InputItemFragment } from "../../graphql/explorer/operations";
11
import { RollupVersion } from "../../graphql/explorer/types";
1✔
12
import {
13
    InputDetailsDocument,
14
    InputDetailsQuery,
15
    InputDetailsQueryVariables,
16
} from "../../graphql/rollups/operations";
1✔
17
import { Voucher } from "../../graphql/rollups/types";
18
import {
19
    InputDetailsDocument as V2InputDetailsDocument,
20
    InputDetailsQuery as V2InputDetailsQuery,
21
    InputDetailsQueryVariables as V2InputDetailsQueryVariables,
22
} from "../../graphql/rollups/v2/operations";
1✔
23
import { Voucher as VoucherV2 } from "../../graphql/rollups/v2/types";
24
import getConfiguredChainId from "../../lib/getConfiguredChain";
1✔
25
import { useConnectionConfig } from "../../providers/connectionConfig/hooks";
1✔
26
import { theme } from "../../providers/theme";
1✔
27
import { useBlockExplorerData } from "../BlockExplorerLink";
1✔
28
import AddressEl from "../address";
1✔
29
import { NewSpecificationButton } from "../specification/components/NewSpecificationButton";
1✔
30
import { findSpecificationFor } from "../specification/conditionals";
1✔
31
import { Envelope, decodePayload } from "../specification/decoder";
1✔
32
import { useSpecification } from "../specification/hooks/useSpecification";
1✔
33
import { useSystemSpecifications } from "../specification/hooks/useSystemSpecifications";
1✔
34
import useVoucherDecoder from "../specification/hooks/useVoucherDecoder";
1✔
35
import { Specification } from "../specification/types";
36
import { stringifyContent } from "../specification/utils";
1✔
37
import VoucherExecution from "./voucherExecution";
1✔
38
import VoucherExecutionV2 from "./voucherExecutionV2";
1✔
39

40
interface ApplicationInputDataProps {
41
    input: InputItemFragment;
42
}
43

44
type OptionalInputDetailsVariables = Omit<
45
    InputDetailsQueryVariables & V2InputDetailsQueryVariables,
46
    "inputIdx" | "inputId"
47
>;
48

49
type InputTypes = "vouchers" | "reports" | "notices";
50

51
const destinationOrString = pathOr("", ["edges", "0", "node", "destination"]);
1✔
52
const payloadOrString = pathOr("", ["edges", 0, "node", "payload"]);
1✔
53

54
const updateForNextPage = (
1✔
55
    name: InputTypes,
4✔
56
    obj: OptionalInputDetailsVariables,
4✔
57
) => {
4✔
58
    switch (name) {
4✔
59
        case "vouchers":
4✔
60
            return omit(["lastVouchers", "vouchersPrevPage"], obj);
2✔
61
        case "notices":
4✔
62
            return omit(["lastNotices", "noticesPrevPage"], obj);
1✔
63
        case "reports":
4✔
64
            return omit(["lastReports", "reportsPrevPage"], obj);
1✔
65
        default:
4!
66
            throw new Error(`${name} not supported.`);
×
67
    }
4✔
68
};
4✔
69

70
const updateForPrevPage = (
1✔
71
    name: InputTypes,
4✔
72
    obj: OptionalInputDetailsVariables,
4✔
73
) => {
4✔
74
    switch (name) {
4✔
75
        case "vouchers":
4✔
76
            return omit(["firstVouchers", "vouchersNextPage"], obj);
2✔
77
        case "notices":
4✔
78
            return omit(["firstNotices", "noticesNextPage"], obj);
1✔
79
        case "reports":
4✔
80
            return omit(["firstReports", "reportsNextPage"], obj);
1✔
81
        default:
4!
82
            throw new Error(`${name} not supported.`);
×
83
    }
4✔
84
};
4✔
85

86
const getStringifiedDecodedValueOrPayload = (
1✔
87
    envelope: Envelope | undefined,
43✔
88
    originalPayload: string,
43✔
89
) => {
43✔
90
    if (envelope?.error) {
43✔
91
        console.error(envelope.error);
41✔
92
        return originalPayload;
41✔
93
    }
41✔
94

95
    if (!envelope || isNilOrEmpty(envelope?.result)) {
43!
96
        return originalPayload;
×
97
    }
✔
98

99
    return stringifyContent(envelope.result);
2✔
100
};
2✔
101

102
type UseDecodingOnInputResult = [
103
    string,
104
    {
105
        specApplied: Specification | null;
106
        userSpecifications: Specification[];
107
        systemSpecifications: Specification[];
108
        error?: Error;
109
        wasSpecManuallySelected: boolean;
110
    },
111
];
112

113
/**
114
 * Receive the input from a graphQL call and
115
 * It may find a specification that matches a defined condition, therefore decoding the content.
116
 * If an specification is not found it returns the original payload.
117
 * @param input Input data returned by graphQL.
118
 * @param specName Specification id to apply instead of check for matching conditions. (optional)
119
 * @returns { UseDecodingOnInputResult }
120
 */
121
const useDecodingOnInput = (
1✔
122
    input: InputItemFragment,
43✔
123
    specId?: string,
43✔
124
): UseDecodingOnInputResult => {
43✔
125
    const { listSpecifications } = useSpecification();
43✔
126
    const { systemSpecificationAsList } = useSystemSpecifications();
43✔
127

128
    const userSpecifications = listSpecifications() ?? [];
43✔
129
    const specifications = [
43✔
130
        ...systemSpecificationAsList,
43✔
131
        ...userSpecifications,
43✔
132
    ];
43✔
133

134
    const specification = isNilOrEmpty(specId)
43✔
135
        ? findSpecificationFor(input, specifications)
43!
136
        : find((spec) => spec.id === specId, specifications) ?? null;
×
137

138
    const envelope = specification
43✔
139
        ? decodePayload(specification, input.payload as Hex)
43!
140
        : undefined;
×
141

142
    const result = getStringifiedDecodedValueOrPayload(envelope, input.payload);
43✔
143

144
    return [
43✔
145
        result,
43✔
146
        {
43✔
147
            specApplied: specification,
43✔
148
            systemSpecifications: systemSpecificationAsList,
43✔
149
            userSpecifications,
43✔
150
            error: envelope?.error,
43✔
151
            wasSpecManuallySelected: isNotNilOrEmpty(specId),
43✔
152
        },
43✔
153
    ];
43✔
154
};
43✔
155

156
const buildSelectData = (
1✔
157
    userSpecifications: Specification[],
43✔
158
    systemSpecifications: Specification[],
43✔
159
) => {
43✔
160
    const groups = [];
43✔
161

162
    if (userSpecifications.length) {
43!
163
        groups.push({
×
164
            group: "Your Specifications",
×
165
            items: userSpecifications.map((spec) => ({
×
166
                label: spec.name,
×
167
                value: spec.id!,
×
168
            })),
×
169
        });
×
170
    }
×
171

172
    if (systemSpecifications.length) {
43✔
173
        groups.push({
43✔
174
            group: "System Specifications",
43✔
175
            items: systemSpecifications.map((spec) => ({
43✔
176
                label: spec.name,
473✔
177
                value: spec.id!,
473✔
178
            })),
43✔
179
        });
43✔
180
    }
43✔
181

182
    return groups;
43✔
183
};
43✔
184

185
const chainId = Number.parseInt(getConfiguredChainId());
1✔
186

187
const getInputDetailsQueryFactoryByVersion = {
1✔
188
    [RollupVersion.V1]:
1✔
189
        (inputIdx: string) => (optionals: OptionalInputDetailsVariables) => {
1✔
190
            return useQuery<InputDetailsQuery, InputDetailsQueryVariables>({
39✔
191
                query: InputDetailsDocument,
39✔
192
                pause: true,
39✔
193
                variables: {
39✔
194
                    inputIdx: parseInt(inputIdx),
39✔
195
                    ...optionals,
39✔
196
                },
39✔
197
            });
39✔
198
        },
39✔
199
    [RollupVersion.V2]:
1✔
200
        (inputId: string) => (optionals: OptionalInputDetailsVariables) => {
1✔
201
            return useQuery<V2InputDetailsQuery, V2InputDetailsQueryVariables>({
4✔
202
                query: V2InputDetailsDocument,
4✔
203
                pause: true,
4✔
204
                variables: {
4✔
205
                    inputId,
4✔
206
                    ...optionals,
4✔
207
                },
4✔
208
            });
4✔
209
        },
4✔
210
} as const;
1✔
211

212
/**
213
 * InputDetailsView should be lazy rendered.
214
 * to avoid multiple eager network calls.
215
 */
216
const InputDetailsView: FC<ApplicationInputDataProps> = ({ input }) => {
1✔
217
    const { getConnection, hasConnection, showConnectionModal } =
44✔
218
        useConnectionConfig();
44✔
219
    const appId = input.application.address as Address;
44✔
220
    const appVersion = input.application.rollupVersion;
44✔
221
    const requiredValue = input.index.toString();
44✔
222

223
    const inputDetailsQuery = useMemo(() => {
44✔
224
        const factory = getInputDetailsQueryFactoryByVersion[appVersion];
16✔
225

226
        if (!factory) {
16!
227
            const text = isNilOrEmpty(appVersion)
×
228
                ? `Missing appVersion (${appVersion})`
×
229
                : `appVersion: ${appVersion} not supported.`;
×
230
            console.warn(`${text}. GraphQL queries will be skipped.`);
×
231

232
            return () => [{}, () => {}] as UseQueryResponse;
×
233
        }
×
234

235
        return factory(requiredValue);
16✔
236
    }, [appVersion, requiredValue]);
44✔
237

238
    const connection = getConnection(appId);
44✔
239
    const [selectedSpec, setSelectedSpec] = useState<string>("");
44✔
240
    const [variables, updateQueryVars] =
44✔
241
        useState<OptionalInputDetailsVariables>({
44✔
242
            firstNotices: 1,
44✔
243
            firstReports: 1,
44✔
244
            firstVouchers: 1,
44✔
245
        });
44✔
246

247
    const [result, execQuery] = inputDetailsQuery(variables);
44✔
248

249
    const reports = result.data?.input.reports;
44✔
250
    const notices = result.data?.input.notices;
44✔
251
    const vouchers = result.data?.input.vouchers;
44✔
252

253
    const showNotices =
44✔
254
        !connection ||
44✔
255
        (connection && notices && payloadOrString(notices) !== "");
27✔
256
    const showReports =
44✔
257
        !connection ||
44✔
258
        (connection && reports && payloadOrString(reports) !== "");
27✔
259
    const showVouchers =
44✔
260
        !connection ||
44✔
261
        (connection && vouchers && payloadOrString(vouchers) !== "");
27✔
262
    const vouchersForExecution = (vouchers?.edges?.map((e: any) => e.node) ??
44✔
263
        []) as Partial<Voucher>[] | VoucherV2[];
16✔
264
    const showVoucherForExecution =
44✔
265
        showVouchers && vouchersForExecution.length > 0;
44✔
266

267
    const [
44✔
268
        content,
44✔
269
        {
44✔
270
            specApplied,
44✔
271
            error,
44✔
272
            systemSpecifications,
44✔
273
            userSpecifications,
44✔
274
            wasSpecManuallySelected,
44✔
275
        },
44✔
276
    ] = useDecodingOnInput(input, selectedSpec);
44✔
277

278
    const voucherDestination = destinationOrString(vouchers) as Hex;
44✔
279
    const voucherBlockExplorer = useBlockExplorerData(
44✔
280
        "address",
44✔
281
        voucherDestination,
44✔
282
    );
44✔
283

284
    const voucherDecoderRes = useVoucherDecoder({
44✔
285
        payload: payloadOrString(vouchers) as Hex,
44✔
286
        destination: voucherDestination,
44✔
287
        chainId,
44✔
288
        appVersion,
44✔
289
    });
44✔
290

291
    const selectData = buildSelectData(
44✔
292
        userSpecifications,
44✔
293
        systemSpecifications,
44✔
294
    );
44✔
295

296
    useEffect(() => {
44✔
297
        if (connection) execQuery({ url: connection.url });
28✔
298
    }, [connection, execQuery, variables]);
44✔
299

300
    const isSystemSpecAppliedManually =
44✔
301
        wasSpecManuallySelected && included(systemSpecifications, specApplied);
44!
302

303
    const [voucherContentType, setVoucherContentType] =
44✔
304
        useState<ContentType>("raw");
44✔
305
    const [inputContentType, setInputContentType] =
44✔
306
        useState<ContentType>("raw");
44✔
307

308
    const voucherContent =
44✔
309
        voucherContentType === "raw" || voucherDecoderRes.data === null
44!
310
            ? payloadOrString(vouchers)
43!
311
            : voucherDecoderRes.data;
×
312

313
    const inputContent =
44✔
314
        inputContentType === "raw" || error ? input.payload : content;
44!
315

316
    return (
44✔
317
        <Box py="md">
44✔
318
            <InputDetails>
44✔
319
                <InputDetails.InputContent
44✔
320
                    content={inputContent}
44✔
321
                    contentType={inputContentType}
44✔
322
                    onContentTypeChange={setInputContentType}
44✔
323
                    additionalControls={!error ? ["decoded"] : []}
44✔
324
                >
325
                    <Stack gap="sm">
44✔
326
                        <Group>
44✔
327
                            <Select
44✔
328
                                label="Decode Specification"
44✔
329
                                description="When a specification condition(s) match(es), it will be auto-selected."
44✔
330
                                placeholder="Decode content with..."
44✔
331
                                value={specApplied?.id ?? selectedSpec}
44!
332
                                size="md"
44✔
333
                                checkIconPosition="right"
44✔
334
                                data={selectData}
44✔
335
                                onChange={(value) => {
44✔
336
                                    setSelectedSpec(value ?? "");
×
337
                                }}
×
338
                                error={
44✔
339
                                    error
44✔
340
                                        ? `We're not able to decode using ${specApplied?.name}`
41✔
341
                                        : null
2✔
342
                                }
343
                            />
44✔
344
                        </Group>
44✔
345
                        {isSystemSpecAppliedManually && (
44!
346
                            <Group>
×
347
                                <Alert
×
348
                                    data-testid="system-spec-applied-warning"
×
349
                                    icon={
×
350
                                        <TbExclamationCircle
×
351
                                            size={theme.other.iconSize}
×
352
                                        />
×
353
                                    }
354
                                    color="orange"
×
355
                                    title="System Specifications"
×
356
                                >
×
357
                                    Be careful when manually selecting system
358
                                    specifications.
359
                                    <br /> It may show readable information by
×
360
                                    sheer luck of byte length.
361
                                    <br /> They are always auto-selected.
×
362
                                </Alert>
×
363
                            </Group>
×
364
                        )}
365

366
                        {!specApplied && (
44!
367
                            <Group gap={3}>
×
368
                                <Text c="dimmed">
×
369
                                    {`Is this Application ABI encoding it's inputs?`}
×
370
                                </Text>
×
371
                                <NewSpecificationButton
×
372
                                    p={0}
×
373
                                    variant="transparent"
×
374
                                    btnText="Add a Spec!"
×
375
                                />
×
376
                            </Group>
×
377
                        )}
378
                    </Stack>
44✔
379
                </InputDetails.InputContent>
44✔
380

381
                {showReports && (
44✔
382
                    <InputDetails.ReportContent
43✔
383
                        content={payloadOrString(reports)}
43✔
384
                        contentType="raw"
43✔
385
                        onConnect={() => showConnectionModal(appId)}
43✔
386
                        isLoading={result.fetching}
43✔
387
                        isConnected={hasConnection(appId)}
43✔
388
                        paging={{
43✔
389
                            total: reports?.totalCount ?? 0,
43✔
390
                            onNextPage: () => {
43✔
391
                                updateQueryVars((vars) => {
1✔
392
                                    const newVars = {
1✔
393
                                        ...vars,
1✔
394
                                        firstReports: 1,
1✔
395
                                        reportsNextPage:
1✔
396
                                            reports?.pageInfo.endCursor,
1✔
397
                                    };
1✔
398

399
                                    return updateForNextPage(
1✔
400
                                        "reports",
1✔
401
                                        newVars,
1✔
402
                                    );
1✔
403
                                });
1✔
404
                            },
1✔
405
                            onPreviousPage: () => {
43✔
406
                                updateQueryVars((vars) => {
1✔
407
                                    const newVars = {
1✔
408
                                        ...vars,
1✔
409
                                        lastReports: 1,
1✔
410
                                        reportsPrevPage:
1✔
411
                                            reports?.pageInfo.startCursor,
1✔
412
                                    };
1✔
413
                                    return updateForPrevPage(
1✔
414
                                        "reports",
1✔
415
                                        newVars,
1✔
416
                                    );
1✔
417
                                });
1✔
418
                            },
1✔
419
                        }}
43✔
420
                    />
43✔
421
                )}
422

423
                {showNotices && (
44✔
424
                    <InputDetails.NoticeContent
34✔
425
                        content={payloadOrString(notices)}
34✔
426
                        contentType="raw"
34✔
427
                        onConnect={() => showConnectionModal(appId)}
34✔
428
                        isLoading={result.fetching}
34✔
429
                        isConnected={hasConnection(appId)}
34✔
430
                        paging={{
34✔
431
                            total: notices?.totalCount ?? 0,
34✔
432
                            onNextPage: () => {
34✔
433
                                updateQueryVars((vars) => {
1✔
434
                                    const newVars = {
1✔
435
                                        ...vars,
1✔
436
                                        firstNotices: 1,
1✔
437
                                        noticesNextPage:
1✔
438
                                            notices?.pageInfo.endCursor,
1✔
439
                                    };
1✔
440
                                    return updateForNextPage(
1✔
441
                                        "notices",
1✔
442
                                        newVars,
1✔
443
                                    );
1✔
444
                                });
1✔
445
                            },
1✔
446
                            onPreviousPage: () => {
34✔
447
                                updateQueryVars((vars) => {
1✔
448
                                    const newVars = {
1✔
449
                                        ...vars,
1✔
450
                                        lastNotices: 1,
1✔
451
                                        noticesPrevPage:
1✔
452
                                            notices?.pageInfo.startCursor,
1✔
453
                                    };
1✔
454

455
                                    return updateForPrevPage(
1✔
456
                                        "notices",
1✔
457
                                        newVars,
1✔
458
                                    );
1✔
459
                                });
1✔
460
                            },
1✔
461
                        }}
34✔
462
                    />
34✔
463
                )}
464

465
                {showVouchers && (
44✔
466
                    <InputDetails.VoucherContent
34✔
467
                        content={voucherContent}
34✔
468
                        contentType={voucherContentType}
34✔
469
                        additionalControls={
34✔
470
                            voucherDecoderRes.data ? ["decoded"] : []
34!
471
                        }
472
                        onContentTypeChange={setVoucherContentType}
34✔
473
                        onConnect={() => showConnectionModal(appId)}
34✔
474
                        isLoading={result.fetching}
34✔
475
                        isConnected={hasConnection(appId)}
34✔
476
                        paging={{
34✔
477
                            total: vouchers?.totalCount ?? 0,
34✔
478
                            onNextPage: () => {
34✔
479
                                updateQueryVars((vars) => {
2✔
480
                                    const newVars = {
2✔
481
                                        ...vars,
2✔
482
                                        firstVouchers: 1,
2✔
483
                                        vouchersNextPage:
2✔
484
                                            vouchers?.pageInfo.endCursor,
2✔
485
                                    };
2✔
486

487
                                    return updateForNextPage(
2✔
488
                                        "vouchers",
2✔
489
                                        newVars,
2✔
490
                                    );
2✔
491
                                });
2✔
492
                            },
2✔
493
                            onPreviousPage: () => {
34✔
494
                                updateQueryVars((vars) => {
2✔
495
                                    const newVars = {
2✔
496
                                        ...vars,
2✔
497
                                        lastVouchers: 1,
2✔
498
                                        vouchersPrevPage:
2✔
499
                                            vouchers?.pageInfo.startCursor,
2✔
500
                                    };
2✔
501

502
                                    return updateForPrevPage(
2✔
503
                                        "vouchers",
2✔
504
                                        newVars,
2✔
505
                                    );
2✔
506
                                });
2✔
507
                            },
2✔
508
                        }}
34✔
509
                        middlePosition={
34✔
510
                            isNotNilOrEmpty(voucherDestination) && (
34✔
511
                                <Group
4✔
512
                                    gap="xs"
4✔
513
                                    data-testid="voucher-destination-block-explorer-link"
4✔
514
                                >
515
                                    <Text fw="bold">Destination Address:</Text>
4✔
516
                                    <AddressEl
4✔
517
                                        value={voucherDestination}
4✔
518
                                        href={voucherBlockExplorer.url}
4✔
519
                                        hrefTarget="_blank"
4✔
520
                                        shorten
4✔
521
                                    />
4✔
522
                                </Group>
4✔
523
                            )
524
                        }
525
                    >
526
                        {showVoucherForExecution ? (
34✔
527
                            appVersion === RollupVersion.V1 ? (
18✔
528
                                <VoucherExecution
14✔
529
                                    appId={appId}
14✔
530
                                    voucher={
14✔
531
                                        vouchersForExecution[0] as Partial<Voucher>
14✔
532
                                    }
533
                                />
14✔
534
                            ) : appVersion === RollupVersion.V2 ? (
4✔
535
                                <VoucherExecutionV2
4✔
536
                                    input={input}
4✔
537
                                    voucher={
4✔
538
                                        vouchersForExecution[0] as VoucherV2
4✔
539
                                    }
540
                                />
4!
541
                            ) : null
✔
542
                        ) : null}
16✔
543
                    </InputDetails.VoucherContent>
34✔
544
                )}
545
            </InputDetails>
44✔
546
        </Box>
44✔
547
    );
548
};
44✔
549

550
export default InputDetailsView;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc