diff --git a/apps/web/src/components/SendFlow/utils.tsx b/apps/web/src/components/SendFlow/utils.tsx
index a2c639c54..dfc8ff88f 100644
--- a/apps/web/src/components/SendFlow/utils.tsx
+++ b/apps/web/src/components/SendFlow/utils.tsx
@@ -1,3 +1,4 @@
+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";
@@ -5,6 +6,7 @@ import {
type Account,
type AccountOperations,
type EstimatedAccountOperations,
+ type ImplicitAccount,
type Operation,
estimate,
executeOperations,
@@ -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 },
diff --git a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
index c1a4b68da..89f476e89 100644
--- a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
+++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
@@ -1,12 +1,26 @@
+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,
+ walletKit,
+} from "@umami/state";
import { WalletConnectError } from "@umami/utils";
+import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
+import { type SdkErrorKey, getSdkError } from "@walletconnect/utils";
+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
@@ -18,6 +32,7 @@ export const useHandleWcRequest = () => {
const { openWith } = useDynamicModalContext();
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
const getAccount = useGetOwnedAccountSafe();
+ const getImplicitAccount = useGetImplicitAccount();
const findNetwork = useFindNetwork();
return async (
@@ -50,11 +65,36 @@ 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 = ;
+ onClose = () => {
+ const sdkErrorKey: SdkErrorKey = "USER_REJECTED";
+ console.info("WC request rejected by user", sdkErrorKey, event);
+ // dApp is waiting so we need to notify it
+ const response = formatJsonRpcError(id, getSdkError(sdkErrorKey).message);
+ void walletKit.respondSessionRequest({ topic, response });
+ };
+ return openWith(modal, { onClose });
}
case "tezos_send": {
diff --git a/apps/web/src/components/beacon/useHandleBeaconMessage.test.tsx b/apps/web/src/components/beacon/useHandleBeaconMessage.test.tsx
index 63c484b9c..00eb28aee 100644
--- a/apps/web/src/components/beacon/useHandleBeaconMessage.test.tsx
+++ b/apps/web/src/components/beacon/useHandleBeaconMessage.test.tsx
@@ -113,7 +113,7 @@ describe("", () => {
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 () => {
@@ -134,7 +134,7 @@ describe("", () => {
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());
diff --git a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx
index a5af8daee..a2ae13931 100644
--- a/apps/web/src/components/beacon/useHandleBeaconMessage.tsx
+++ b/apps/web/src/components/beacon/useHandleBeaconMessage.tsx
@@ -10,6 +10,7 @@ import {
WalletClient,
useAsyncActionHandler,
useFindNetwork,
+ useGetImplicitAccount,
useGetOwnedAccountSafe,
useRemoveBeaconPeerBySenderId,
} from "@umami/state";
@@ -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
@@ -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();
@@ -83,7 +89,16 @@ export const useHandleBeaconMessage = () => {
break;
}
case BeaconMessageType.SignPayloadRequest: {
- modal = ;
+ 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 = ;
onClose = async () => {
await WalletClient.respond({
id: message.id,
diff --git a/apps/web/src/components/common/SignPayloadRequestModal.test.tsx b/apps/web/src/components/common/SignPayloadRequestModal.test.tsx
index 9923d4dbe..fa1b76184 100644
--- a/apps/web/src/components/common/SignPayloadRequestModal.test.tsx
+++ b/apps/web/src/components/common/SignPayloadRequestModal.test.tsx
@@ -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);
@@ -41,23 +60,23 @@ beforeEach(() => {
describe("", () => {
it("renders the dapp name", async () => {
- await renderInModal(, store);
+ await renderInModal(, 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(, store);
+ await renderInModal(, 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(, store);
+ await renderInModal(, store);
await act(() => user.click(screen.getByLabelText("Password")));
await act(() => user.type(screen.getByLabelText("Password"), "123123123"));
@@ -76,4 +95,33 @@ describe("", () => {
})
);
});
+ it("WalletConnect sends the signed payload back to the DApp", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(walletKit, "respondSessionRequest");
+ await renderInModal(, 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 })
+ );
+ });
});
diff --git a/apps/web/src/components/common/SignPayloadRequestModal.tsx b/apps/web/src/components/common/SignPayloadRequestModal.tsx
index 72a7f9887..8f2a77f00 100644
--- a/apps/web/src/components/common/SignPayloadRequestModal.tsx
+++ b/apps/web/src/components/common/SignPayloadRequestModal.tsx
@@ -1,8 +1,4 @@
-import {
- BeaconMessageType,
- type SignPayloadRequestOutput,
- type SignPayloadResponseInput,
-} from "@airgap/beacon-wallet";
+import { BeaconMessageType, type SignPayloadResponseInput } from "@airgap/beacon-wallet";
import { WarningIcon } from "@chakra-ui/icons";
import {
Box,
@@ -20,41 +16,45 @@ import {
import { type TezosToolkit } from "@taquito/taquito";
import { useDynamicModalContext } from "@umami/components";
import { decodeBeaconPayload } from "@umami/core";
-import { WalletClient, useGetImplicitAccount } from "@umami/state";
+import { WalletClient, walletKit } from "@umami/state";
+import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useColor } from "../../styles/useColor";
import { SignButton } from "../SendFlow/SignButton";
+import { type SignPayloadProps } from "../SendFlow/utils";
-export const SignPayloadRequestModal = ({ request }: { request: SignPayloadRequestOutput }) => {
+export const SignPayloadRequestModal = ({ opts }: { opts: SignPayloadProps }) => {
const { goBack } = useDynamicModalContext();
- const getAccount = useGetImplicitAccount();
- const signerAccount = getAccount(request.sourceAddress);
const toast = useToast();
const form = useForm();
const color = useColor();
const [showRaw, setShowRaw] = useState(false);
const { result: parsedPayload, error: parsingError } = decodeBeaconPayload(
- request.payload,
- request.signingType
+ opts.payload,
+ opts.signingType
);
const sign = async (tezosToolkit: TezosToolkit) => {
- const result = await tezosToolkit.signer.sign(request.payload);
-
- const response: SignPayloadResponseInput = {
- type: BeaconMessageType.SignPayloadResponse,
- id: request.id,
- signingType: request.signingType,
- signature: result.prefixSig,
- };
+ const result = await tezosToolkit.signer.sign(opts.payload);
- await WalletClient.respond(response);
+ if (opts.requestId.sdkType === "beacon") {
+ const response: SignPayloadResponseInput = {
+ type: BeaconMessageType.SignPayloadResponse,
+ id: opts.requestId.id.toString(),
+ signingType: opts.signingType,
+ signature: result.prefixSig,
+ };
+ await WalletClient.respond(response);
+ } else {
+ const response = formatJsonRpcResult(opts.requestId.id, { signature: result.prefixSig });
+ await walletKit.respondSessionRequest({ topic: opts.requestId.topic, response });
+ }
toast({
- description: "Successfully submitted Beacon operation",
+ description: "Successfully signed the payload",
status: "success",
});
goBack();
@@ -64,7 +64,7 @@ export const SignPayloadRequestModal = ({ request }: { request: SignPayloadReque
- {`${request.appMetadata.name}/dApp Pairing Request`}
+ {`Sign Payload Request from ${opts.appName}`}
@@ -90,7 +90,7 @@ export const SignPayloadRequestModal = ({ request }: { request: SignPayloadReque
backgroundColor={color("100")}
>
- {showRaw ? request.payload : parsedPayload.trim()}
+ {showRaw ? opts.payload : parsedPayload.trim()}
@@ -105,7 +105,7 @@ export const SignPayloadRequestModal = ({ request }: { request: SignPayloadReque
-
+
diff --git a/apps/web/src/components/common/index.ts b/apps/web/src/components/common/index.ts
new file mode 100644
index 000000000..94068949f
--- /dev/null
+++ b/apps/web/src/components/common/index.ts
@@ -0,0 +1 @@
+export * from "./SignPayloadRequestModal";