Skip to content

Commit 1a7ddfc

Browse files
feat: WalletConnect integration, part 6, request
requests are supported. Tested: - send tez - delegate / undelegate - originate / call contract - stake / unstake / finalize unstake
1 parent 12bc5ea commit 1a7ddfc

File tree

6 files changed

+192
-10
lines changed

6 files changed

+192
-10
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { type TezosToolkit } from "@taquito/taquito";
2+
import { useDynamicModalContext } from "@umami/components";
3+
import { executeOperations, totalFee } from "@umami/core";
4+
import { useAsyncActionHandler, walletKit } from "@umami/state";
5+
import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
6+
import { useForm } from "react-hook-form";
7+
8+
import { SuccessStep } from "../SuccessStep";
9+
import { type CalculatedSignProps, type SdkSignPageProps } from "../utils";
10+
11+
export const useSignWithWalletConnect = ({
12+
operation,
13+
headerProps,
14+
requestId,
15+
}: SdkSignPageProps): CalculatedSignProps => {
16+
const { isLoading: isSigning, handleAsyncAction } = useAsyncActionHandler();
17+
const { openWith } = useDynamicModalContext();
18+
19+
const form = useForm({ defaultValues: { executeParams: operation.estimates } });
20+
21+
if (requestId.sdkType !== "walletconnect") {
22+
return {
23+
fee: 0,
24+
isSigning: false,
25+
onSign: async () => {},
26+
network: null,
27+
};
28+
}
29+
30+
const onSign = async (tezosToolkit: TezosToolkit) =>
31+
handleAsyncAction(
32+
async () => {
33+
const { opHash } = await executeOperations(
34+
{ ...operation, estimates: form.watch("executeParams") },
35+
tezosToolkit
36+
);
37+
38+
const response = formatJsonRpcResult(requestId.id, { hash: opHash });
39+
await walletKit.respondSessionRequest({ topic: requestId.topic, response });
40+
return openWith(<SuccessStep hash={opHash} />);
41+
},
42+
error => ({
43+
description: `Failed to confirm Beacon operation: ${error.message}`,
44+
})
45+
);
46+
47+
return {
48+
fee: totalFee(form.watch("executeParams")),
49+
isSigning,
50+
onSign,
51+
network: headerProps.network,
52+
};
53+
};

apps/web/src/components/SendFlow/common/BatchSignPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
2323
import { SignButton } from "../SignButton";
2424
import { SignPageFee } from "../SignPageFee";
2525
import { type SdkSignPageProps } from "../utils";
26+
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";
2627

2728
export const BatchSignPage = (
2829
signProps: SdkSignPageProps,
@@ -31,7 +32,9 @@ export const BatchSignPage = (
3132
const color = useColor();
3233

3334
const beaconCalculatedProps = useSignWithBeacon({ ...signProps });
34-
const calculatedProps = beaconCalculatedProps;
35+
const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps });
36+
const calculatedProps =
37+
signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps;
3538

3639
const { isSigning, onSign, network, fee } = calculatedProps;
3740
const { signer, operations } = signProps.operation;

apps/web/src/components/SendFlow/common/OriginationOperationSignPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export const OriginationOperationSignPage = ({
3737
}: SdkSignPageProps & CalculatedSignProps) => {
3838
const color = useColor();
3939
const { code, storage } = operation.operations[0] as ContractOrigination;
40-
4140
const form = useForm({ defaultValues: { executeParams: operation.estimates } });
4241

4342
return (

apps/web/src/components/SendFlow/common/SingleSignPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import { TezSignPage } from "./TezSignPage";
1010
import { UndelegationSignPage } from "./UndelegationSignPage";
1111
import { UnstakeSignPage } from "./UnstakeSignPage";
1212
import { useSignWithBeacon } from "../Beacon/useSignWithBeacon";
13+
import { useSignWithWalletConnect } from "../WalletConnect/useSignWithWc";
1314

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

1718
const beaconCalculatedProps = useSignWithBeacon({ ...signProps });
18-
const calculatedProps = beaconCalculatedProps;
19+
const walletConnectCalculatedProps = useSignWithWalletConnect({ ...signProps });
20+
const calculatedProps =
21+
signProps.requestId.sdkType === "beacon" ? beaconCalculatedProps : walletConnectCalculatedProps;
1922

2023
switch (operationType) {
2124
case "tez": {

apps/web/src/components/WalletConnect/WalletConnectProvider.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getSdkError } from "@walletconnect/utils";
1919
import { type PropsWithChildren, useCallback, useEffect, useRef } from "react";
2020

2121
import { SessionProposalModal } from "./SessionProposalModal";
22+
import { useHandleWcRequest } from "./useHandleWcRequest";
2223

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

3738
const availableNetworks: Network[] = useAvailableNetworks();
3839

40+
const handleWcRequest = useHandleWcRequest();
41+
3942
const onSessionProposal = useCallback(
4043
(proposal: WalletKitTypes.SessionProposal) =>
4144
handleAsyncActionUnsafe(async () => {
@@ -87,8 +90,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
8790
);
8891

8992
const onSessionRequest = useCallback(
90-
async (event: WalletKitTypes.SessionRequest) => {
91-
try {
93+
async (event: WalletKitTypes.SessionRequest) =>
94+
handleAsyncActionUnsafe(async () => {
9295
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
9396
if (!(event.topic in activeSessions)) {
9497
console.error("WalletConnect session request failed. Session not found", event);
@@ -101,8 +104,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
101104
description: `Session request from dApp ${session.peer.metadata.name}`,
102105
status: "info",
103106
});
104-
throw new CustomError("Not implemented");
105-
} catch (error) {
107+
await handleWcRequest(event, session);
108+
}).catch(async error => {
106109
const { id, topic } = event;
107110
const activeSessions: Record<string, SessionTypes.Struct> = walletKit.getActiveSessions();
108111
console.error("WalletConnect session request failed", event, error);
@@ -121,9 +124,8 @@ export const WalletConnectProvider = ({ children }: PropsWithChildren) => {
121124
// dApp is waiting so we need to notify it
122125
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
123126
await walletKit.respondSessionRequest({ topic, response });
124-
}
125-
},
126-
[toast]
127+
}),
128+
[handleAsyncActionUnsafe, handleWcRequest, toast]
127129
);
128130

129131
useEffect(() => {
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { useToast } from "@chakra-ui/react";
2+
import { useDynamicModalContext } from "@umami/components";
3+
import { type ImplicitAccount, estimate, toAccountOperations } from "@umami/core";
4+
import {
5+
useAsyncActionHandler,
6+
useFindNetwork,
7+
useGetOwnedAccountSafe,
8+
walletKit,
9+
} from "@umami/state";
10+
import { formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
11+
import { type SessionTypes, type SignClientTypes, type Verify } from "@walletconnect/types";
12+
import { getSdkError } from "@walletconnect/utils";
13+
14+
import { BatchSignPage } from "../SendFlow/common/BatchSignPage";
15+
import { SingleSignPage } from "../SendFlow/common/SingleSignPage";
16+
import { type SdkSignPageProps, type SignHeaderProps } from "../SendFlow/utils";
17+
18+
/**
19+
* @returns a function that handles a beacon message and opens a modal with the appropriate content
20+
*
21+
* For operation requests it will also try to convert the operation(s) to our {@link Operation} format,
22+
* estimate the fee and open the BeaconSignPage only if it succeeds
23+
*/
24+
export const useHandleWcRequest = () => {
25+
const { openWith } = useDynamicModalContext();
26+
const { handleAsyncActionUnsafe } = useAsyncActionHandler();
27+
const getAccount = useGetOwnedAccountSafe();
28+
const findNetwork = useFindNetwork();
29+
const toast = useToast();
30+
31+
return async (
32+
event: {
33+
verifyContext: Verify.Context;
34+
} & SignClientTypes.BaseEventArgs<{
35+
request: {
36+
method: string;
37+
params: any;
38+
expiryTimestamp?: number;
39+
};
40+
chainId: string;
41+
}>,
42+
session: SessionTypes.Struct
43+
) => {
44+
await handleAsyncActionUnsafe(
45+
async () => {
46+
const { id, topic, params } = event;
47+
const { request, chainId } = params;
48+
49+
let modal;
50+
let onClose;
51+
52+
switch (request.method) {
53+
case "tezos_getAccounts": {
54+
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
55+
await walletKit.respondSessionRequest({ topic, response });
56+
return;
57+
}
58+
59+
case "tezos_sign": {
60+
// onClose = async () => {
61+
// const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
62+
// await walletKit.respondSessionRequest({ topic, response });
63+
// };
64+
// return openWith(<SignPayloadRequestModal request={"FIXME"} />, { onClose });
65+
const response = formatJsonRpcError(id, getSdkError("INVALID_METHOD").message);
66+
await walletKit.respondSessionRequest({ topic, response });
67+
return;
68+
}
69+
70+
case "tezos_send": {
71+
if (!request.params.account) {
72+
throw new Error("Missing account in request");
73+
}
74+
const signer = getAccount(request.params.account);
75+
if (!signer) {
76+
throw new Error(`Unknown account, no signer: ${request.params.account}`);
77+
}
78+
const operation = toAccountOperations(
79+
request.params.operations,
80+
signer as ImplicitAccount
81+
);
82+
const network = findNetwork(chainId.split(":")[1]);
83+
if (!network) {
84+
const response = formatJsonRpcError(id, getSdkError("INVALID_EVENT").message);
85+
await walletKit.respondSessionRequest({ topic, response });
86+
toast({ description: `Unsupported network: ${chainId}`, status: "error" });
87+
return;
88+
}
89+
const estimatedOperations = await estimate(operation, network);
90+
const headerProps: SignHeaderProps = {
91+
network,
92+
appName: session.peer.metadata.name,
93+
appIcon: session.peer.metadata.icons[0],
94+
};
95+
const signProps: SdkSignPageProps = {
96+
headerProps: headerProps,
97+
operation: estimatedOperations,
98+
requestId: { sdkType: "walletconnect", id: id, topic },
99+
};
100+
101+
if (operation.operations.length === 1) {
102+
modal = <SingleSignPage {...signProps} />;
103+
} else {
104+
modal = <BatchSignPage {...signProps} {...event.params.request.params} />;
105+
}
106+
onClose = async () => {
107+
const response = formatJsonRpcError(id, getSdkError("USER_REJECTED").message);
108+
await walletKit.respondSessionRequest({ topic, response });
109+
};
110+
111+
return openWith(modal, { onClose });
112+
}
113+
default:
114+
throw new Error(`Unsupported method ${request.method}`);
115+
}
116+
}
117+
// error => ({
118+
// description: `Error while processing WalletConnect request: ${error.message}`,
119+
// })
120+
);
121+
};
122+
};

0 commit comments

Comments
 (0)