Skip to content

Commit

Permalink
feat: Voucher payload auto decoding (#246)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunomenezes authored Oct 21, 2024
1 parent ca1518d commit 175df83
Show file tree
Hide file tree
Showing 25 changed files with 1,546 additions and 94 deletions.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@mantine/notifications": "^7.12.0",
"@rainbow-me/rainbowkit": "2",
"@react-spring/web": "^9.7.3",
"@shazow/whatsabi": "^0.14.1",
"@tanstack/react-query": "^5.27.5",
"@vercel/analytics": "^1.2.2",
"@vercel/speed-insights": "^1.0.10",
Expand Down
37 changes: 25 additions & 12 deletions apps/web/src/components/BlockExplorerLink.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Anchor, Group, Text, rem } from "@mantine/core";
import { anyPass, equals } from "ramda";
import { isNilOrEmpty } from "ramda-adjunct";
import { FC } from "react";
import { TbExternalLink } from "react-icons/tb";
import { useConfig } from "wagmi";
Expand All @@ -11,6 +12,27 @@ interface BlockExplorerLinkProps {

const isTxOrAddress = anyPass([equals("tx"), equals("address")]);

export const useBlockExplorerData = (
type: BlockExplorerLinkProps["type"],
value: string,
) => {
const config = useConfig();
const explorerUrl = config.chains[0].blockExplorers?.default.url;

if (isNilOrEmpty(explorerUrl) || isNilOrEmpty(value))
return { ok: false } as const;

const shouldShorten = isTxOrAddress(type);

const text = shouldShorten
? `${value.slice(0, 8)}...${value.slice(-6)}`
: value;

const url = `${explorerUrl}/${type}/${value}`;

return { ok: true, url, text } as const;
};

/**
*
* Works in conjuction with Wagmi. It requires a Wagmi-Provider to work as expected.
Expand All @@ -21,21 +43,12 @@ export const BlockExplorerLink: FC<BlockExplorerLinkProps> = ({
value,
type,
}) => {
const config = useConfig();
const explorerUrl = config.chains[0].blockExplorers?.default.url;

if (!explorerUrl) return;

const shouldShorten = isTxOrAddress(type);

const text = shouldShorten
? `${value.slice(0, 8)}...${value.slice(-6)}`
: value;
const { ok, text, url } = useBlockExplorerData(type, value);

const href = `${explorerUrl}/${type}/${value}`;
if (!ok) return;

return (
<Anchor href={href} target="_blank">
<Anchor href={url} target="_blank">
<Group gap="xs">
<Text>{text}</Text>
<TbExternalLink style={{ width: rem(21), height: rem(21) }} />
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
export type AddressProps = {
value: AddressType;
href?: string;
hrefTarget?: "_self" | "_blank" | "_top" | "_parent";
icon?: boolean;
iconSize?: number;
shorten?: boolean;
Expand All @@ -52,6 +53,7 @@ const Address: FC<AddressProps> = ({
icon,
iconSize,
shorten,
hrefTarget = "_self",
}) => {
value = getAddress(value);
const name = resolveName(value);
Expand All @@ -78,7 +80,7 @@ const Address: FC<AddressProps> = ({
)}

{href ? (
<Anchor href={href} component={Link}>
<Anchor href={href} component={Link} target={hrefTarget}>
{label}
</Anchor>
) : (
Expand Down
50 changes: 47 additions & 3 deletions apps/web/src/components/inputs/inputDetailsView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { InputDetails } from "@cartesi/rollups-explorer-ui";
import { ContentType, InputDetails } from "@cartesi/rollups-explorer-ui";
import { Alert, Box, Group, Select, Stack, Text } from "@mantine/core";
import { find, omit, pathOr } from "ramda";
import { included, isNilOrEmpty, isNotNilOrEmpty } from "ramda-adjunct";
Expand All @@ -14,13 +14,17 @@ import {
InputDetailsQueryVariables,
} from "../../graphql/rollups/operations";
import { Voucher } from "../../graphql/rollups/types";
import getConfiguredChainId from "../../lib/getConfiguredChain";
import { useConnectionConfig } from "../../providers/connectionConfig/hooks";
import { theme } from "../../providers/theme";
import { useBlockExplorerData } from "../BlockExplorerLink";
import AddressEl from "../address";
import { NewSpecificationButton } from "../specification/components/NewSpecificationButton";
import { findSpecificationFor } from "../specification/conditionals";
import { Envelope, decodePayload } from "../specification/decoder";
import { useSpecification } from "../specification/hooks/useSpecification";
import { useSystemSpecifications } from "../specification/hooks/useSystemSpecifications";
import useVoucherDecoder from "../specification/hooks/useVoucherDecoder";
import { Specification } from "../specification/types";
import { stringifyContent } from "../specification/utils";
import VoucherExecution from "./voucherExecution";
Expand All @@ -31,6 +35,7 @@ interface ApplicationInputDataProps {

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

const destinationOrString = pathOr("", ["edges", "0", "node", "destination"]);
const payloadOrString = pathOr("", ["edges", 0, "node", "payload"]);

const updateForNextPage = (
Expand Down Expand Up @@ -164,6 +169,8 @@ const buildSelectData = (
return groups;
};

const chainId = Number.parseInt(getConfiguredChainId());

/**
* InputDetailsView should be lazy rendered.
* to avoid multiple eager network calls.
Expand Down Expand Up @@ -220,6 +227,18 @@ const InputDetailsView: FC<ApplicationInputDataProps> = ({ input }) => {
},
] = useDecodingOnInput(input, selectedSpec);

const voucherDestination = destinationOrString(vouchers) as Hex;
const voucherBlockExplorer = useBlockExplorerData(
"address",
voucherDestination,
);

const voucherDecoderRes = useVoucherDecoder({
payload: payloadOrString(vouchers) as Hex,
destination: voucherDestination,
chainId: chainId,
});

const selectData = buildSelectData(
userSpecifications,
systemSpecifications,
Expand All @@ -232,6 +251,14 @@ const InputDetailsView: FC<ApplicationInputDataProps> = ({ input }) => {
const isSystemSpecAppliedManually =
wasSpecManuallySelected && included(systemSpecifications, specApplied);

const [voucherContentType, setVoucherContentType] =
useState<ContentType>("raw");

const voucherContent =
voucherContentType === "raw" || voucherDecoderRes.data === null
? payloadOrString(vouchers)
: voucherDecoderRes.data;

return (
<Box py="md">
<InputDetails>
Expand Down Expand Up @@ -381,8 +408,9 @@ const InputDetailsView: FC<ApplicationInputDataProps> = ({ input }) => {

{showVouchers && (
<InputDetails.VoucherContent
content={payloadOrString(vouchers)}
contentType="raw"
content={voucherContent}
contentType={voucherContentType}
onContentTypeChange={setVoucherContentType}
onConnect={() => showConnectionModal(appId)}
isLoading={result.fetching}
isConnected={hasConnection(appId)}
Expand Down Expand Up @@ -419,6 +447,22 @@ const InputDetailsView: FC<ApplicationInputDataProps> = ({ input }) => {
});
},
}}
middlePosition={
isNotNilOrEmpty(voucherDestination) && (
<Group
gap="xs"
data-testid="voucher-destination-block-explorer-link"
>
<Text fw="bold">Destination Address:</Text>
<AddressEl
value={voucherDestination}
href={voucherBlockExplorer.url}
hrefTarget="_blank"
shorten
/>
</Group>
)
}
>
{showVoucherForExecution ? (
<VoucherExecution
Expand Down
18 changes: 14 additions & 4 deletions apps/web/src/components/inputs/voucherExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FC, useEffect } from "react";
import type { Address } from "viem";
import { useAccount, useWaitForTransactionReceipt } from "wagmi";
import { Voucher } from "../../graphql/rollups/types";
import getConfiguredChainId from "../../lib/getConfiguredChain";

const typeCastProof = (voucher: Partial<Voucher>) => ({
context: voucher.proof?.context as Address,
Expand Down Expand Up @@ -50,16 +51,25 @@ const VoucherExecution: FC<VoucherExecutionType> = (props) => {
BigInt(voucher.input?.index as number),
BigInt(voucher.index as number),
],
chainId: parseInt(getConfiguredChainId()),
address: appId,
});

const prepare = useSimulateCartesiDAppExecuteVoucher({
args: [
voucher.destination as Address,
voucher.payload as Address,
typeCastProof(voucher),
],
address: appId,
query: {
enabled:
hasVoucherProof &&
!wasExecuted.isFetching &&
wasExecuted.data === false,
},
});

const execute = useWriteCartesiDAppExecuteVoucher();
const wait = useWaitForTransactionReceipt({
hash: execute.data,
Expand All @@ -72,7 +82,7 @@ const VoucherExecution: FC<VoucherExecutionType> = (props) => {
isExecuted ||
!hasVoucherProof ||
!isConnected ||
prepare.isPending ||
prepare.isLoading ||
prepare.isError;

useEffect(() => {
Expand Down Expand Up @@ -125,10 +135,10 @@ const VoucherExecution: FC<VoucherExecutionType> = (props) => {
execute.writeContract(prepare.data!.request)
}
>
{prepare.isPending
? "Preparing voucher..."
: isExecuted
{isExecuted
? "Executed"
: prepare.isLoading
? "Preparing voucher..."
: "Execute"}
</Button>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ type FilterByMode = (value: {
list: Specification[];
}) => Specification[];

const filterByMode: FilterByMode = cond([
const filterByMode = cond<Parameters<FilterByMode>, ReturnType<FilterByMode>>([
[({ filterBy }) => filterBy === "all", ({ list }) => list],
[T, ({ filterBy, list }) => filter(propEq(filterBy, "mode"), list)],
]);
Expand Down
20 changes: 19 additions & 1 deletion apps/web/src/components/specification/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
pathOr,
pipe,
} from "ramda";
import { isNotNilOrEmpty } from "ramda-adjunct";
import {
AbiDecodingDataSizeTooSmallError,
AbiFunction,
AbiFunctionSignatureNotFoundError,
AbiParameter,
Hex,
InvalidAbiParametersError,
decodeAbiParameters,
Expand Down Expand Up @@ -150,6 +152,22 @@ const prepareResultFromPieces = (e: Envelope): Envelope => {
return e;
};

type AbiParameterInfo = {
param: AbiParameter;
index: number;
};

/**
* Check the AbiParameter in the following precedence order [name, type]
* and returns the first available.
* Fallback to `arg-{index}` based on the parameter position passed to the abi function.
*/
const getAbiParamIdentifier = cond<[info: AbiParameterInfo], string>([
[(info) => isNotNilOrEmpty(info.param.name), pathOr("", ["param", "name"])],
[(info) => isNotNilOrEmpty(info.param.type), pathOr("", ["param", "type"])],
[T, (info) => `arg-${info.index}`],
]);

const prepareResultForJSONABI = (e: Envelope): Envelope => {
if (e.spec.mode === "json_abi") {
try {
Expand All @@ -168,7 +186,7 @@ const prepareResultForJSONABI = (e: Envelope): Envelope => {

// respecting order of arguments but including abi names
abiItem.inputs.forEach((param, index) => {
const name = param.name ?? `param${0}`;
const name = getAbiParamIdentifier({ param, index });
orderedNamedArgs.push([name, args[index]]);
});
}
Expand Down
Loading

0 comments on commit 175df83

Please sign in to comment.