Skip to content

Commit

Permalink
feat: WalletConnect integration, part 7, sign
Browse files Browse the repository at this point in the history
  • Loading branch information
dianasavvatina committed Dec 18, 2024
1 parent cb2e26c commit 83b7286
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("<SignPayloadRequestModal />", () => {
render(<SignPayloadRequestModal request={request} />, { store });

await waitFor(() =>
expect(screen.getByText("mockDappName/dApp Pairing Request")).toBeVisible()
expect(screen.getByText("Sign Payload Request from mockDappName")).toBeVisible()
);
});

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/utils/beacon/useHandleBeaconMessage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe("<useHandleBeaconMessage />", () => {

act(() => handleMessage(message));

await screen.findByText("mockDappName/dApp Pairing Request");
await screen.findByText("Sign Payload Request from mockDappName");
});

it("sends an error response to the dapp on close", async () => {
Expand All @@ -128,7 +128,7 @@ describe("<useHandleBeaconMessage />", () => {

act(() => handleMessage(message));

await screen.findByText("mockDappName/dApp Pairing Request");
await screen.findByText("Sign Payload Request from mockDappName");

act(() => screen.getByRole("button", { name: "Close" }).click());

Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/components/SendFlow/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { type SigningType } from "@airgap/beacon-wallet";
import { Button, type ButtonProps } from "@chakra-ui/react";
import { type TezosToolkit } from "@taquito/taquito";
import { useDynamicModalContext } from "@umami/components";
import {
type Account,
type AccountOperations,
type EstimatedAccountOperations,
type ImplicitAccount,
type Operation,
estimate,
executeOperations,
Expand Down Expand Up @@ -82,6 +84,15 @@ export type SdkSignPageProps = {
headerProps: SignHeaderProps;
};

export type SignPayloadProps = {
requestId: SignRequestId;
appName: string;
appIcon?: string;
payload: string;
signer: ImplicitAccount;
signingType: SigningType;
};

export const FormSubmitButton = ({ title = "Preview", ...props }: ButtonProps) => {
const {
formState: { isValid },
Expand Down
47 changes: 40 additions & 7 deletions apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { SigningType } from "@airgap/beacon-wallet";
import { useDynamicModalContext } from "@umami/components";
import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core";
import { useAsyncActionHandler, useFindNetwork, useGetOwnedAccountSafe } from "@umami/state";
import {
useAsyncActionHandler,
useFindNetwork,
useGetImplicitAccount,
useGetOwnedAccountSafe,
} from "@umami/state";
import { WalletConnectError } from "@umami/utils";
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";

import { SignPayloadRequestModal } from "../common/SignPayloadRequestModal";
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
import { SingleSignPage } from "../SendFlow/common/SingleSignPage";
import { type SdkSignPageProps, type SignHeaderProps } from "../SendFlow/utils";
import {
type SdkSignPageProps,
type SignHeaderProps,
type SignPayloadProps,
} from "../SendFlow/utils";

/**
* @returns a function that handles a beacon message and opens a modal with the appropriate content
Expand All @@ -18,6 +29,7 @@ export const useHandleWcRequest = () => {
const { openWith } = useDynamicModalContext();
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
const getAccount = useGetOwnedAccountSafe();
const getImplicitAccount = useGetImplicitAccount();
const findNetwork = useFindNetwork();

return async (
Expand Down Expand Up @@ -50,11 +62,32 @@ export const useHandleWcRequest = () => {
}

case "tezos_sign": {
throw new WalletConnectError(
"Sign is not supported yet",
"WC_METHOD_UNSUPPORTED",
session
);
if (!request.params.account) {
throw new WalletConnectError("Missing account in request", "INVALID_EVENT", session);
}
const signer = getImplicitAccount(request.params.account);
const network = findNetwork(chainId.split(":")[1]);
if (!network) {
throw new WalletConnectError(
`Unsupported network ${chainId}`,
"UNSUPPORTED_CHAINS",
session
);
}
const signPayloadProps: SignPayloadProps = {
appName: session.peer.metadata.name,
appIcon: session.peer.metadata.icons[0],
payload: request.params.payload,
signer: signer,
signingType: SigningType.RAW,
requestId: { sdkType: "walletconnect", id: id, topic },
};

modal = <SignPayloadRequestModal opts={signPayloadProps} />;
onClose = () => {
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
};
return openWith(modal, { onClose });
}

case "tezos_send": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe("<useHandleBeaconMessage />", () => {

act(() => handleMessage(message));

await screen.findByText("mockDappName/dApp Pairing Request");
await screen.findByText("Sign Payload Request from mockDappName");
});

it("sends an error response to the dapp on close", async () => {
Expand All @@ -134,7 +134,7 @@ describe("<useHandleBeaconMessage />", () => {

act(() => handleMessage(message));

await screen.findByText("mockDappName/dApp Pairing Request");
await screen.findByText("Sign Payload Request from mockDappName");

act(() => screen.getByRole("button", { name: "Close" }).click());

Expand Down
19 changes: 17 additions & 2 deletions apps/web/src/components/beacon/useHandleBeaconMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
WalletClient,
useAsyncActionHandler,
useFindNetwork,
useGetImplicitAccount,
useGetOwnedAccountSafe,
useRemoveBeaconPeerBySenderId,
} from "@umami/state";
Expand All @@ -20,7 +21,11 @@ import { PermissionRequestModal } from "./PermissionRequestModal";
import { SignPayloadRequestModal } from "../common/SignPayloadRequestModal";
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
import { SingleSignPage } from "../SendFlow/common/SingleSignPage";
import { type SdkSignPageProps, type SignHeaderProps } from "../SendFlow/utils";
import {
type SdkSignPageProps,
type SignHeaderProps,
type SignPayloadProps,
} from "../SendFlow/utils";

/**
* @returns a function that handles a beacon message and opens a modal with the appropriate content
Expand All @@ -32,6 +37,7 @@ export const useHandleBeaconMessage = () => {
const { openWith } = useDynamicModalContext();
const { handleAsyncAction } = useAsyncActionHandler();
const getAccount = useGetOwnedAccountSafe();
const getImplicitAccount = useGetImplicitAccount();
const findNetwork = useFindNetwork();
const removePeer = useRemoveBeaconPeerBySenderId();

Expand Down Expand Up @@ -83,7 +89,16 @@ export const useHandleBeaconMessage = () => {
break;
}
case BeaconMessageType.SignPayloadRequest: {
modal = <SignPayloadRequestModal request={message} />;
const signer = getImplicitAccount(message.sourceAddress);
const signPayloadProps: SignPayloadProps = {
appName: message.appMetadata.name,
appIcon: message.appMetadata.icon,
payload: message.payload,
signer: signer,
signingType: message.signingType,
requestId: { sdkType: "beacon", id: message.id },
};
modal = <SignPayloadRequestModal opts={signPayloadProps} />;
onClose = async () => {
await WalletClient.respond({
id: message.id,
Expand Down
84 changes: 66 additions & 18 deletions apps/web/src/components/common/SignPayloadRequestModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import {
BeaconMessageType,
type SignPayloadRequestOutput,
SigningType,
} from "@airgap/beacon-wallet";
import { SigningType } from "@airgap/beacon-wallet";
import { mockImplicitAccount, mockMnemonicAccount } from "@umami/core";
import { type UmamiStore, WalletClient, accountsActions, makeStore } from "@umami/state";
import { type UmamiStore, WalletClient, accountsActions, makeStore, walletKit } from "@umami/state";
import { encryptedMnemonic1 } from "@umami/test-utils";
import { type JsonRpcResult } from "@walletconnect/jsonrpc-utils";

import { SignPayloadRequestModal } from "./SignPayloadRequestModal";
import { act, renderInModal, screen, userEvent, waitFor } from "../../testUtils";
import { type SignPayloadProps } from "../SendFlow/utils";

jest.mock("@umami/state", () => ({
...jest.requireActual("@umami/state"),
walletKit: {
core: {},
metadata: {
name: "AppMenu test",
description: "Umami Wallet with WalletConnect",
url: "https://umamiwallet.com",
icons: ["https://umamiwallet.com/assets/favicon-32-45gq0g6M.png"],
},
respondSessionRequest: jest.fn(),
},
createWalletKit: jest.fn(),
}));

const payload =
"05010000004254657a6f73205369676e6564204d6573736167653a206d79646170702e636f6d20323032312d30312d31345431353a31363a30345a2048656c6c6f20776f726c6421";
const decodedPayload = "Tezos Signed Message: mydapp.com 2021-01-14T15:16:04Z Hello world!";
const request: SignPayloadRequestOutput = {
const beaconOpts: SignPayloadProps = {
appName: "mockBeaconDappName",
appIcon: "",
payload,
signer: mockImplicitAccount(1),
signingType: SigningType.RAW,
requestId: { sdkType: "beacon", id: "mockMessageId" },
};
const wcOpts: SignPayloadProps = {
appName: "mockWalletConnectDappName",
appIcon: "",
payload,
senderId: "mockSenderId",
type: BeaconMessageType.SignPayloadRequest,
version: "2",
sourceAddress: mockImplicitAccount(1).address.pkh,
signer: mockImplicitAccount(1),
signingType: SigningType.RAW,
id: "mockMessageId",
appMetadata: { name: "mockDappName", senderId: "mockSenderId" },
requestId: { sdkType: "walletconnect", id: 123, topic: "mockTopic" },
};

const account = mockMnemonicAccount(1);
Expand All @@ -41,23 +60,23 @@ beforeEach(() => {

describe("<SignPayloadRequestModal />", () => {
it("renders the dapp name", async () => {
await renderInModal(<SignPayloadRequestModal request={request} />, store);
await renderInModal(<SignPayloadRequestModal opts={beaconOpts} />, store);

await waitFor(() =>
expect(screen.getByText("mockDappName/dApp Pairing Request")).toBeVisible()
expect(screen.getByText("Sign Payload Request from mockBeaconDappName")).toBeVisible()
);
});

it("renders the payload to sign", async () => {
await renderInModal(<SignPayloadRequestModal request={request} />, store);
await renderInModal(<SignPayloadRequestModal opts={beaconOpts} />, store);

await waitFor(() => expect(screen.getByText(new RegExp(decodedPayload))).toBeVisible());
});

it("sends the signed payload back to the DApp", async () => {
it("Beacon sends the signed payload back to the DApp", async () => {
const user = userEvent.setup();
jest.spyOn(WalletClient, "respond");
await renderInModal(<SignPayloadRequestModal request={request} />, store);
await renderInModal(<SignPayloadRequestModal opts={beaconOpts} />, store);

await act(() => user.click(screen.getByLabelText("Password")));
await act(() => user.type(screen.getByLabelText("Password"), "123123123"));
Expand All @@ -76,4 +95,33 @@ describe("<SignPayloadRequestModal />", () => {
})
);
});
it("WalletConnect sends the signed payload back to the DApp", async () => {
const user = userEvent.setup();
jest.spyOn(walletKit, "respondSessionRequest");
await renderInModal(<SignPayloadRequestModal opts={wcOpts} />, store);

await waitFor(() =>
expect(screen.getByText("Sign Payload Request from mockWalletConnectDappName")).toBeVisible()
);
await waitFor(() => expect(screen.getByText(new RegExp(decodedPayload))).toBeVisible());

await act(() => user.click(screen.getByLabelText("Password")));
await act(() => user.type(screen.getByLabelText("Password"), "123123123"));
const confirmButton = screen.getByRole("button", { name: "Sign" });
expect(confirmButton).toBeEnabled();

await act(() => user.click(confirmButton));

const response: JsonRpcResult = {
id: 123,
jsonrpc: "2.0",
result: {
signature:
"edsigtqC1pJWaJ7rGm75PZAWyX75hH2BiKCb1EM3MotDSjEqHEA2tVZ1FPd8k4SwRMR74ytDVcCXrZqKJ9LtsDoduCJLMAeBq88",
},
} as unknown as JsonRpcResult;
await waitFor(() =>
expect(walletKit.respondSessionRequest).toHaveBeenCalledWith({ topic: "mockTopic", response })
);
});
});
Loading

0 comments on commit 83b7286

Please sign in to comment.