Skip to content

Commit

Permalink
Merge pull request #2285 from trilitech/wc_verify
Browse files Browse the repository at this point in the history
feat: WalletConnect integration, part 8, verify
  • Loading branch information
dianasavvatina authored Jan 3, 2025
2 parents 9085d74 + 6127b2d commit 188d44e
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { type CalculatedSignProps, type SdkSignPageProps } from "../utils";
export const useSignWithBeacon = ({
operation,
headerProps,
requestId,
}: SdkSignPageProps): CalculatedSignProps => {
const { isLoading: isSigning, handleAsyncAction } = useAsyncActionHandler();
const { openWith } = useDynamicModalContext();
Expand All @@ -28,7 +27,7 @@ export const useSignWithBeacon = ({

const response: OperationResponseInput = {
type: BeaconMessageType.OperationResponse,
id: requestId.id.toString(),
id: headerProps.requestId.id.toString(),
transactionHash: opHash,
};
await WalletClient.respond(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { type CalculatedSignProps, type SdkSignPageProps } from "../utils";
export const useSignWithWalletConnect = ({
operation,
headerProps,
requestId,
}: SdkSignPageProps): CalculatedSignProps => {
const { isLoading: isSigning, handleAsyncAction } = useAsyncActionHandler();
const { openWith } = useDynamicModalContext();

const form = useForm({ defaultValues: { executeParams: operation.estimates } });
const requestId = headerProps.requestId;

if (requestId.sdkType !== "walletconnect") {
return {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/SendFlow/common/BatchSignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export const BatchSignPage = (
const beaconCalculatedProps = useSignWithBeacon({ ...signProps });
const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps });
const calculatedProps =
signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps;
signProps.headerProps.requestId.sdkType === "beacon"
? beaconCalculatedProps
: walletConnectCalculatedProps;

const { isSigning, onSign, network, fee } = calculatedProps;
const { signer, operations } = signProps.operation;
Expand Down
63 changes: 36 additions & 27 deletions apps/web/src/components/SendFlow/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,49 @@ import { capitalize } from "lodash";

import { CodeSandboxIcon } from "../../../assets/icons";
import { useColor } from "../../../styles/useColor";
import { VerifyInfobox } from "../../WalletConnect/VerifyInfobox";
import { SignPageHeader } from "../SignPageHeader";
import { type SignHeaderProps } from "../utils";

export const Header = ({ headerProps }: { headerProps: SignHeaderProps }) => {
const color = useColor();

return (
<SignPageHeader>
<Flex alignItems="center" justifyContent="center" marginTop="10px">
<Heading marginRight="4px" color={color("700")} size="sm">
Network:
</Heading>
<Text color={color("700")} fontWeight="400" size="sm">
{capitalize(headerProps.network.name)}
</Text>
</Flex>
<>
<SignPageHeader>
<Flex alignItems="center" justifyContent="center" marginTop="10px">
<Heading marginRight="4px" color={color("700")} size="sm">
Network:
</Heading>
<Text color={color("700")} fontWeight="400" size="sm">
{capitalize(headerProps.network.name)}
</Text>
</Flex>

<Flex
alignItems="center"
marginTop="16px"
padding="15px"
borderRadius="4px"
backgroundColor={color("100")}
>
<AspectRatio width="30px" marginRight="12px" ratio={1}>
<Image
borderRadius="4px"
objectFit="cover"
fallback={<CodeSandboxIcon width="36px" height="36px" />}
src={headerProps.appIcon}
/>
</AspectRatio>
<Heading size="sm">{headerProps.appName}</Heading>
</Flex>
</SignPageHeader>
<Flex
alignItems="center"
marginTop="16px"
padding="15px"
borderRadius="4px"
backgroundColor={color("100")}
>
<AspectRatio width="30px" marginRight="12px" ratio={1}>
<Image
borderRadius="4px"
objectFit="cover"
fallback={<CodeSandboxIcon width="36px" height="36px" />}
src={headerProps.appIcon}
/>
</AspectRatio>
<Heading size="sm">{headerProps.appName}</Heading>
</Flex>
</SignPageHeader>
{headerProps.requestId.sdkType === "walletconnect" ? (
<VerifyInfobox
isScam={headerProps.isScam ?? false}
validationStatus={headerProps.validationStatus ?? "UNKNOWN"}
/>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ const headerProps: SignHeaderProps = {
network: GHOSTNET,
appName: message.appMetadata.name,
appIcon: message.appMetadata.icon,
requestId: { sdkType: "beacon", id: message.id },
};
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: operation,
requestId: { sdkType: "beacon", id: message.id },
};

jest.mock("@umami/core", () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe("<SingleSignPage />", () => {
network: GHOSTNET,
appName: message.appMetadata.name,
appIcon: message.appMetadata.icon,
requestId: { sdkType: "beacon", id: message.id },
};
store.dispatch(networksActions.setCurrent(GHOSTNET));

Expand All @@ -89,7 +90,6 @@ describe("<SingleSignPage />", () => {
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: operation,
requestId: { sdkType: "beacon", id: message.id },
};

jest.mocked(useGetSecretKey).mockImplementation(() => () => Promise.resolve("secretKey"));
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/SendFlow/common/SingleSignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const SingleSignPage = (signProps: SdkSignPageProps) => {
const beaconCalculatedProps = useSignWithBeacon({ ...signProps });
const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps });
const calculatedProps =
signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps;
signProps.headerProps.requestId.sdkType === "beacon"
? beaconCalculatedProps
: walletConnectCalculatedProps;

switch (operationType) {
case "tez": {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/SendFlow/common/TezSignPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ describe("<TezSignPage />", () => {
network: GHOSTNET,
appName: message.appMetadata.name,
appIcon: message.appMetadata.icon,
requestId: { sdkType: "beacon", id: message.id },
};
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: operation,
requestId: { sdkType: "beacon", id: message.id },
};

store.dispatch(networksActions.setCurrent(GHOSTNET));
Expand All @@ -73,6 +73,8 @@ describe("<TezSignPage />", () => {
expect(screen.getByText("Ghostnet")).toBeInTheDocument();
expect(screen.queryByText("Mainnet")).not.toBeInTheDocument();

expect(screen.queryByText("verifyinfobox")).not.toBeInTheDocument();

const signButton = screen.getByRole("button", {
name: "Confirm Transaction",
});
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/components/SendFlow/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ export type SignHeaderProps = {
network: Network;
appName: string;
appIcon?: string;
isScam?: boolean;
validationStatus?: "VALID" | "INVALID" | "UNKNOWN";
requestId: SignRequestId;
};

export type SdkSignPageProps = {
requestId: SignRequestId;
operation: EstimatedAccountOperations;
headerProps: SignHeaderProps;
};
Expand All @@ -89,6 +91,8 @@ export type SignPayloadProps = {
appName: string;
appIcon?: string;
payload: string;
isScam?: boolean;
validationStatus?: "VALID" | "INVALID" | "UNKNOWN";
signer: ImplicitAccount;
signingType: SigningType;
};
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/components/WalletConnect/ProjectInfoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Avatar, Box, Card, Flex, Icon, Link, Text } from "@chakra-ui/react";
import { Avatar, Box, Card, Link, Text } from "@chakra-ui/react";
import { type SignClientTypes } from "@walletconnect/types";

import { PencilIcon } from "../../assets/icons";

type Props = {
metadata: SignClientTypes.Metadata;
intention?: string;
Expand Down Expand Up @@ -38,10 +36,6 @@ export const ProjectInfoCard = ({ metadata, intention }: Props) => {
{url}
</Link>
</Box>
<Flex alignItems="center" justifyContent="center" marginTop="16px">
<Icon as={PencilIcon} verticalAlign="bottom" />
<Card marginLeft="8px">Cannot Verify: to be implemented</Card>
</Flex>
</Box>
);
};
11 changes: 8 additions & 3 deletions apps/web/src/components/WalletConnect/SessionProposalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
useToggleWcPeerListUpdated,
walletKit,
} from "@umami/state";
import { type SessionTypes } from "@walletconnect/types";
import { type SessionTypes, type Verify } from "@walletconnect/types";
import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils";
import { FormProvider, useForm } from "react-hook-form";

Expand All @@ -46,6 +46,10 @@ export const SessionProposalModal = ({
const { onClose } = useDynamicModalContext();
const { isLoading, handleAsyncAction } = useAsyncActionHandler();

const verifyContext: Verify.Context = proposal.verifyContext;
const isScam = verifyContext.verified.isScam;
const validationStatus = verifyContext.verified.validation;

const form = useForm<{ address: string }>({
mode: "onBlur",
});
Expand Down Expand Up @@ -85,6 +89,7 @@ export const SessionProposalModal = ({
handleAsyncAction(async () => {
// close immediately assuming that the user wants to get rid of the modal
onClose();
console.log("WC session rejected");
await walletKit.rejectSession({
id: proposal.id,
reason: getSdkError("USER_REJECTED_METHODS"),
Expand All @@ -97,6 +102,7 @@ export const SessionProposalModal = ({
<ModalBody>
<Card>
<ProjectInfoCard metadata={proposal.params.proposer.metadata} />
<VerifyInfobox isScam={isScam} validationStatus={validationStatus} />
<Divider />
<Box marginBottom="16px" fontSize="xl" fontWeight="semibold">
Requested permissions
Expand Down Expand Up @@ -132,7 +138,6 @@ export const SessionProposalModal = ({
<Text marginLeft="8px">{network}</Text>
</Box>
<Divider />
<VerifyInfobox />
</Card>
</ModalBody>
<ModalFooter>
Expand All @@ -141,7 +146,7 @@ export const SessionProposalModal = ({
</Button>
<Button
width="100%"
isDisabled={!isValid}
isDisabled={!isValid || isScam}
isLoading={isLoading}
loadingText="Approving..."
onClick={onApprove}
Expand Down
66 changes: 55 additions & 11 deletions apps/web/src/components/WalletConnect/VerifyInfobox.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,61 @@
import { Box, Card, HStack, Icon, VStack } from "@chakra-ui/react";

import { AlertCircleIcon } from "../../assets/icons";
import { AlertCircleIcon, AlertTriangleIcon, VerifiedIcon } from "../../assets/icons";

export const VerifyInfobox = () => (
<Box textAlign="center">
<VStack spacing="16px">
<HStack margin="auto">
<Icon as={AlertCircleIcon} verticalAlign="bottom" />
<Card marginLeft="8px">Unknown domain</Card>
</HStack>
<Box margin="auto">
<Card>This domain was not verified. To be implemented.</Card>
</Box>
export type VerificationAlert = {
color: string;
icon: typeof AlertTriangleIcon;
text: string;
};
export type ValidationStatus = "SCAM" | "UNKNOWN" | "INVALID" | "VALID";

const getVerificationAlert = (validationStatus: ValidationStatus) => {
const statusOptions: Record<ValidationStatus, VerificationAlert> = {
SCAM: {
color: "red.500",
icon: AlertTriangleIcon,
text: "This domain is suspected to be a SCAM. Potential threat detected.",
},
UNKNOWN: {
color: "yellow.500",
icon: AlertCircleIcon,
text: "This domain is unknown. Cannot verify it.",
},
INVALID: {
color: "yellow.500",
icon: AlertTriangleIcon,
text: "This domain is invalid.",
},
VALID: {
color: "green.500",
icon: VerifiedIcon,
text: "This domain is verified.",
},
};
return (
<HStack
margin="auto"
padding="8px"
border="1px solid"
borderColor={statusOptions[validationStatus].color}
borderRadius="md"
>
<Icon as={statusOptions[validationStatus].icon} verticalAlign="bottom" />
<Card marginLeft="8px">{statusOptions[validationStatus].text}</Card>
</HStack>
);
};

export const VerifyInfobox = ({
isScam,
validationStatus,
}: {
isScam?: boolean;
validationStatus: "UNKNOWN" | "INVALID" | "VALID";
}) => (
<Box textAlign="left" data-testid="verifyinfobox">
<VStack margin="auto" marginTop="16px" marginBottom="16px" spacing="16px">
{isScam ? getVerificationAlert("SCAM") : getVerificationAlert(validationStatus)}
</VStack>
</Box>
);
6 changes: 5 additions & 1 deletion apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const useHandleWcRequest = () => {
appName: session.peer.metadata.name,
appIcon: session.peer.metadata.icons[0],
payload: request.params.payload,
isScam: event.verifyContext.verified.isScam,
validationStatus: event.verifyContext.verified.validation,
signer: signer,
signingType: SigningType.RAW,
requestId: { sdkType: "walletconnect", id: id, topic },
Expand Down Expand Up @@ -126,11 +128,13 @@ export const useHandleWcRequest = () => {
network,
appName: session.peer.metadata.name,
appIcon: session.peer.metadata.icons[0],
isScam: event.verifyContext.verified.isScam,
validationStatus: event.verifyContext.verified.validation,
requestId: { sdkType: "walletconnect", id: id, topic },
};
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: estimatedOperations,
requestId: { sdkType: "walletconnect", id: id, topic },
};

if (operation.operations.length === 1) {
Expand Down
Loading

1 comment on commit 188d44e

@github-actions
Copy link

Choose a reason for hiding this comment

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

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
apps/web Coverage: 83%
83.81% (1786/2131) 79.58% (850/1068) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.47% (207/251) 72.72% (88/121) 81.35% (48/59)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 97%
95.27% (141/148) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 85%
84.79% (820/967) 81.03% (188/232) 78.59% (301/383)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

Please sign in to comment.