diff --git a/apps/desktop/src/utils/beacon/SignPayloadRequestModal.test.tsx b/apps/desktop/src/utils/beacon/SignPayloadRequestModal.test.tsx
index 3600c8f2cf..c84effde80 100644
--- a/apps/desktop/src/utils/beacon/SignPayloadRequestModal.test.tsx
+++ b/apps/desktop/src/utils/beacon/SignPayloadRequestModal.test.tsx
@@ -44,7 +44,7 @@ describe("", () => {
render(, { store });
await waitFor(() =>
- expect(screen.getByText("mockDappName/dApp Pairing Request")).toBeVisible()
+ expect(screen.getByText("Sign Payload Request from mockDappName")).toBeVisible()
);
});
diff --git a/apps/desktop/src/utils/beacon/useHandleBeaconMessage.test.tsx b/apps/desktop/src/utils/beacon/useHandleBeaconMessage.test.tsx
index 05f3a45112..896084e8d1 100644
--- a/apps/desktop/src/utils/beacon/useHandleBeaconMessage.test.tsx
+++ b/apps/desktop/src/utils/beacon/useHandleBeaconMessage.test.tsx
@@ -107,7 +107,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 () => {
@@ -128,7 +128,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/SendFlow/utils.tsx b/apps/web/src/components/SendFlow/utils.tsx
index a2c639c54d..dfc8ff88f3 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 b9af4401c6..d889a99f72 100644
--- a/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
+++ b/apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
@@ -1,8 +1,10 @@
+import { SigningType } from "@airgap/beacon-wallet";
import { useDynamicModalContext } from "@umami/components";
import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core";
import {
useAsyncActionHandler,
useFindNetwork,
+ useGetImplicitAccount,
useGetOwnedAccountSafe,
walletKit,
} from "@umami/state";
@@ -11,9 +13,14 @@ import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
import { 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
@@ -25,6 +32,7 @@ export const useHandleWcRequest = () => {
const { openWith } = useDynamicModalContext();
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
const getAccount = useGetOwnedAccountSafe();
+ const getImplicitAccount = useGetImplicitAccount();
const findNetwork = useFindNetwork();
return async (
@@ -40,22 +48,44 @@ export const useHandleWcRequest = () => {
}>,
session: SessionTypes.Struct
) => {
- await handleAsyncActionUnsafe(
- async () => {
- const { id, topic, params } = event;
- const { request, chainId } = params;
+ await handleAsyncActionUnsafe(async () => {
+ const { id, topic, params } = event;
+ const { request, chainId } = params;
- let modal;
- let onClose;
+ let modal;
+ let onClose;
switch (request.method) {
case "tezos_getAccounts": {
throw new WalletConnectError("Getting accounts is not supported yet", "WC_METHOD_UNSUPPORTED", session);
}
- case "tezos_sign": {
- throw new WalletConnectError("Sign is not supported yet", "WC_METHOD_UNSUPPORTED", session);
+ case "tezos_sign": {
+ console.log("tezos_sign", request.params);
+ 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 = async () => {
+ const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
+ await walletKit.respondSessionRequest({ topic, response });
+ };
+ return openWith(modal, { onClose });
+ }
case "tezos_send": {
if (!request.params.account) {
@@ -90,10 +120,6 @@ export const useHandleWcRequest = () => {
} else {
modal = ;
}
- onClose = async () => {
- const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
- await walletKit.respondSessionRequest({ topic, response });
- };
onClose = () => {
throw new WalletConnectError("Rejected by user", "USER_REJECTED", session);
};
diff --git a/apps/web/src/components/beacon/SignPayloadRequestModal.test.tsx b/apps/web/src/components/beacon/SignPayloadRequestModal.test.tsx
deleted file mode 100644
index 9923d4dbe4..0000000000
--- a/apps/web/src/components/beacon/SignPayloadRequestModal.test.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import {
- BeaconMessageType,
- type SignPayloadRequestOutput,
- SigningType,
-} from "@airgap/beacon-wallet";
-import { mockImplicitAccount, mockMnemonicAccount } from "@umami/core";
-import { type UmamiStore, WalletClient, accountsActions, makeStore } from "@umami/state";
-import { encryptedMnemonic1 } from "@umami/test-utils";
-
-import { SignPayloadRequestModal } from "./SignPayloadRequestModal";
-import { act, renderInModal, screen, userEvent, waitFor } from "../../testUtils";
-
-const payload =
- "05010000004254657a6f73205369676e6564204d6573736167653a206d79646170702e636f6d20323032312d30312d31345431353a31363a30345a2048656c6c6f20776f726c6421";
-const decodedPayload = "Tezos Signed Message: mydapp.com 2021-01-14T15:16:04Z Hello world!";
-const request: SignPayloadRequestOutput = {
- payload,
- senderId: "mockSenderId",
- type: BeaconMessageType.SignPayloadRequest,
- version: "2",
- sourceAddress: mockImplicitAccount(1).address.pkh,
- signingType: SigningType.RAW,
- id: "mockMessageId",
- appMetadata: { name: "mockDappName", senderId: "mockSenderId" },
-};
-
-const account = mockMnemonicAccount(1);
-
-let store: UmamiStore;
-
-beforeEach(() => {
- store = makeStore();
- store.dispatch(
- accountsActions.addMnemonicAccounts({
- seedFingerprint: account.seedFingerPrint,
- accounts: [account],
- encryptedMnemonic: encryptedMnemonic1,
- })
- );
-});
-
-describe("", () => {
- it("renders the dapp name", async () => {
- await renderInModal(, store);
-
- await waitFor(() =>
- expect(screen.getByText("mockDappName/dApp Pairing Request")).toBeVisible()
- );
- });
-
- it("renders the payload to sign", async () => {
- await renderInModal(, store);
-
- await waitFor(() => expect(screen.getByText(new RegExp(decodedPayload))).toBeVisible());
- });
-
- it("sends the signed payload back to the DApp", async () => {
- const user = userEvent.setup();
- jest.spyOn(WalletClient, "respond");
- await renderInModal(, store);
-
- 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));
-
- await waitFor(() =>
- expect(WalletClient.respond).toHaveBeenCalledWith({
- id: "mockMessageId",
- signingType: "raw",
- type: "sign_payload_response",
- signature:
- "edsigtqC1pJWaJ7rGm75PZAWyX75hH2BiKCb1EM3MotDSjEqHEA2tVZ1FPd8k4SwRMR74ytDVcCXrZqKJ9LtsDoduCJLMAeBq88",
- })
- );
- });
-});
diff --git a/apps/web/src/components/beacon/index.ts b/apps/web/src/components/beacon/index.ts
index eec4203f8a..36a1700562 100644
--- a/apps/web/src/components/beacon/index.ts
+++ b/apps/web/src/components/beacon/index.ts
@@ -1,4 +1,3 @@
export * from "./BeaconProvider";
export * from "./PermissionRequestModal";
-export * from "./SignPayloadRequestModal";
export * from "./useHandleBeaconMessage";
diff --git a/apps/web/src/components/beacon/useHandleBeaconMessage.test.tsx b/apps/web/src/components/beacon/useHandleBeaconMessage.test.tsx
index 63c484b9ca..00eb28aeed 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 472cec316f..a2ae139318 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";
@@ -17,10 +18,14 @@ import { type Network } from "@umami/tezos";
import { CustomError } from "@umami/utils";
import { PermissionRequestModal } from "./PermissionRequestModal";
-import { SignPayloadRequestModal } from "./SignPayloadRequestModal";
+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
new file mode 100644
index 0000000000..fa1b76184f
--- /dev/null
+++ b/apps/web/src/components/common/SignPayloadRequestModal.test.tsx
@@ -0,0 +1,127 @@
+import { SigningType } from "@airgap/beacon-wallet";
+import { mockImplicitAccount, mockMnemonicAccount } from "@umami/core";
+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 beaconOpts: SignPayloadProps = {
+ appName: "mockBeaconDappName",
+ appIcon: "",
+ payload,
+ signer: mockImplicitAccount(1),
+ signingType: SigningType.RAW,
+ requestId: { sdkType: "beacon", id: "mockMessageId" },
+};
+const wcOpts: SignPayloadProps = {
+ appName: "mockWalletConnectDappName",
+ appIcon: "",
+ payload,
+ signer: mockImplicitAccount(1),
+ signingType: SigningType.RAW,
+ requestId: { sdkType: "walletconnect", id: 123, topic: "mockTopic" },
+};
+
+const account = mockMnemonicAccount(1);
+
+let store: UmamiStore;
+
+beforeEach(() => {
+ store = makeStore();
+ store.dispatch(
+ accountsActions.addMnemonicAccounts({
+ seedFingerprint: account.seedFingerPrint,
+ accounts: [account],
+ encryptedMnemonic: encryptedMnemonic1,
+ })
+ );
+});
+
+describe("", () => {
+ it("renders the dapp name", async () => {
+ await renderInModal(, store);
+
+ await waitFor(() =>
+ expect(screen.getByText("Sign Payload Request from mockBeaconDappName")).toBeVisible()
+ );
+ });
+
+ it("renders the payload to sign", async () => {
+ await renderInModal(, store);
+
+ await waitFor(() => expect(screen.getByText(new RegExp(decodedPayload))).toBeVisible());
+ });
+
+ it("Beacon sends the signed payload back to the DApp", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(WalletClient, "respond");
+ await renderInModal(, store);
+
+ 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));
+
+ await waitFor(() =>
+ expect(WalletClient.respond).toHaveBeenCalledWith({
+ id: "mockMessageId",
+ signingType: "raw",
+ type: "sign_payload_response",
+ signature:
+ "edsigtqC1pJWaJ7rGm75PZAWyX75hH2BiKCb1EM3MotDSjEqHEA2tVZ1FPd8k4SwRMR74ytDVcCXrZqKJ9LtsDoduCJLMAeBq88",
+ })
+ );
+ });
+ 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/beacon/SignPayloadRequestModal.tsx b/apps/web/src/components/common/SignPayloadRequestModal.tsx
similarity index 65%
rename from apps/web/src/components/beacon/SignPayloadRequestModal.tsx
rename to apps/web/src/components/common/SignPayloadRequestModal.tsx
index 332ca3724e..70332f2cb7 100644
--- a/apps/web/src/components/beacon/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 { onClose } = 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",
});
onClose();
@@ -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 0000000000..94068949fa
--- /dev/null
+++ b/apps/web/src/components/common/index.ts
@@ -0,0 +1 @@
+export * from "./SignPayloadRequestModal";
diff --git a/packages/utils/src/ErrorContext.ts b/packages/utils/src/ErrorContext.ts
index ed4358edc2..c923e81cee 100644
--- a/packages/utils/src/ErrorContext.ts
+++ b/packages/utils/src/ErrorContext.ts
@@ -1,5 +1,5 @@
-import {type SdkErrorKey} from "@walletconnect/utils";
import { type SessionTypes } from "@walletconnect/types";
+import {type SdkErrorKey} from "@walletconnect/utils";
export type ErrorContext = {
timestamp: string;
description: string;