Skip to content

Commit

Permalink
feat: WalletConnect integration, part 6, request
Browse files Browse the repository at this point in the history
requests are supported. Tested:
 - send tez
 - delegate / undelegate
 - originate / call contract
 - stake / unstake / finalize unstake
  • Loading branch information
dianasavvatina committed Dec 12, 2024
1 parent 5d7d5c7 commit 17d9d12
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 10 deletions.
53 changes: 53 additions & 0 deletions apps/web/src/components/SendFlow/WalletConnect/useSignWithWc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { type TezosToolkit } from "@taquito/taquito";
import { useDynamicModalContext } from "@umami/components";
import { executeOperations, totalFee } from "@umami/core";
import { useAsyncActionHandler, walletKit } from "@umami/state";
import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
import { useForm } from "react-hook-form";

import { SuccessStep } from "../SuccessStep";
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 } });

if (requestId.sdkType !== "walletconnect") {
return {
fee: 0,
isSigning: false,
onSign: async () => {},
network: null,
};
}

const onSign = async (tezosToolkit: TezosToolkit) =>
handleAsyncAction(
async () => {
const { opHash } = await executeOperations(
{ ...operation, estimates: form.watch("executeParams") },
tezosToolkit
);

const response = formatJsonRpcResult(requestId.id, { hash: opHash });
await walletKit.respondSessionRequest({ topic: requestId.topic, response });
return openWith(<SuccessStep hash={opHash} />);
},
error => ({
description: `Failed to confirm Beacon operation: ${error.message}`,
})
);

return {
fee: totalFee(form.watch("executeParams")),
isSigning,
onSign,
network: headerProps.network,
};
};
5 changes: 4 additions & 1 deletion apps/web/src/components/SendFlow/common/BatchSignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
import { SignButton } from "../SignButton";
import { SignPageFee } from "../SignPageFee";
import { type SdkSignPageProps } from "../utils";
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";

export const BatchSignPage = (
signProps: SdkSignPageProps,
Expand All @@ -31,7 +32,9 @@ export const BatchSignPage = (
const color = useColor();

const beaconCalculatedProps = useSignWithBeacon({ ...signProps });
const calculatedProps = beaconCalculatedProps;
const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps });
const calculatedProps =
signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps;

const { isSigning, onSign, network, fee } = calculatedProps;
const { signer, operations } = signProps.operation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const OriginationOperationSignPage = ({
}: SdkSignPageProps & CalculatedSignProps) => {
const color = useColor();
const { code, storage } = operation.operations[0] as ContractOrigination;

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

return (
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/SendFlow/common/SingleSignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import { TezSignPage } from "./TezSignPage";
import { UndelegationSignPage } from "./UndelegationSignPage";
import { UnstakeSignPage } from "./UnstakeSignPage";
import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";

export const SingleSignPage = (signProps: SdkSignPageProps) => {
const operationType = signProps.operation.operations[0].type;

const beaconCalculatedProps = useSignWithBeacon({ ...signProps });
const calculatedProps = beaconCalculatedProps;
const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps });
const calculatedProps =
signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps;

switch (operationType) {
case "tez": {
Expand Down
16 changes: 9 additions & 7 deletions apps/web/src/components/WalletConnect/WalletConnectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getSdkError } from "@walletconnect/utils";
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";

import { SessionProposalModal } from "./SessionProposalModal";
import { useHandleWcRequest } from "./useHandleWcRequest";

enum WalletKitState {
NOT_INITIALIZED,
Expand All @@ -36,6 +37,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {

const availableNetworks: Network[] = useAvailableNetworks();

const handleWcRequest = useHandleWcRequest();

const onSessionProposal = useCallback(
(proposal: WalletKitTypes.SessionProposal) =>
handleAsyncActionUnsafe(async () => {
Expand Down Expand Up @@ -87,8 +90,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
);

const onSessionRequest = useCallback(
async (event: WalletKitTypes.SessionRequest) => {
try {
async (event: WalletKitTypes.SessionRequest) =>
handleAsyncActionUnsafe(async () => {
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
if (!(event.topic in activeSessions)) {
console.error("WalletConnect session request failed. Session not found", event);
Expand All @@ -101,8 +104,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
description: `Session request from dApp ${session.peer.metadata.name}`,
status: "info",
});
throw new CustomError("Not implemented");
} catch (error) {
await handleWcRequest(event, session);
}).catch(async error => {
const { id, topic } = event;
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
console.error("WalletConnect session request failed", event, error);
Expand All @@ -121,9 +124,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
// dApp is waiting so we need to notify it
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
await walletKit.respondSessionRequest({ topic, response });
}
},
[toast]
}),
[handleAsyncActionUnsafe, handleWcRequest, toast]
);

useEffect(() => {
Expand Down
122 changes: 122 additions & 0 deletions apps/web/src/components/WalletConnect/useHandleWcRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useToast } from "@chakra-ui/react";
import { useDynamicModalContext } from "@umami/components";
import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core";
import {
useAsyncActionHandler,
useFindNetwork,
useGetOwnedAccountSafe,
walletKit,
} from "@umami/state";
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
import { getSdkError } from "@walletconnect/utils";

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

/**
* @returns a function that handles a beacon message and opens a modal with the appropriate content
*
* For operation requests it will also try to convert the operation(s) to our {@link Operation} format,
* estimate the fee and open the BeaconSignPage only if it succeeds
*/
export const useHandleWcRequest = () => {
const { openWith } = useDynamicModalContext();
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
const getAccount = useGetOwnedAccountSafe();
const findNetwork = useFindNetwork();
const toast = useToast();

return async (
event: {
verifyContext: Verify.Context;
} & SignClientTypes.BaseEventArgs<{
request: {
method: string;
params: any;
expiryTimestamp?: number;
};
chainId: string;
}>,
session: SessionTypes.Struct
) => {
await handleAsyncActionUnsafe(
async () => {
const { id, topic, params } = event;
const { request, chainId } = params;

let modal;
let onClose;

switch (request.method) {
case "tezos_getAccounts": {
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
await walletKit.respondSessionRequest({ topic, response });
return;
}

case "tezos_sign": {
// onClose = async () => {
// const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
// await walletKit.respondSessionRequest({ topic, response });
// };
// return openWith(<SignPayloadRequestModal request={"FIXME"} />, { onClose });
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
await walletKit.respondSessionRequest({ topic, response });
return;
}

case "tezos_send": {
if (!request.params.account) {
throw new Error("Missing account in request");
}
const signer = getAccount(request.params.account);
if (!signer) {
throw new Error(`Unknown account, no signer: ${request.params.account}`);
}
const operation = toAccountOperations(
request.params.operations,
signer as ImplicitAccount
);
const network = findNetwork(chainId.split(":")[1]);
if (!network) {
const response = formatJsonRpcError(id, getSdkError("INVALID_EVENT").message);
await walletKit.respondSessionRequest({ topic, response });
toast({ description: `Unsupported network: ${chainId}`, status: "error" });
return;
}
const estimatedOperations = await estimate(operation, network);
const headerProps: SignHeaderProps = {
network,
appName: session.peer.metadata.name,
appIcon: session.peer.metadata.icons[0],
};
const signProps: SdkSignPageProps = {
headerProps: headerProps,
operation: estimatedOperations,
requestId: { sdkType: "walletconnect", id: id, topic },
};

if (operation.operations.length === 1) {
modal = <SingleSignPage {...signProps} />;
} else {
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
}
onClose = async () => {
const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
await walletKit.respondSessionRequest({ topic, response });
};

return openWith(modal, { onClose });
}
default:
throw new Error(`Unsupported method ${request.method}`);
}
}
// error => ({
// description: `Error while processing WalletConnect request: ${error.message}`,
// })
);
};
};

0 comments on commit 17d9d12

Please sign in to comment.