Skip to content

Commit aa1f743

Browse files
feat: WalletConnect, first version
Working WalletConnect integration with Umami: - listing dApps connected with WalletConnect - tezos_getAccounts returns the current account - tezos_sign signs payload with the current account - tezos_send supports all perations: - transaction signing - delegate / undelegate - origination, calling smart contract - stake, unstake, finalize - approve and reject by user - success and error from Tezos node Limitations: - the operation result is not shown to the user - pairings list doesn't work on remote disconnect - no tests - no documentation - several lint errors
1 parent 2273e92 commit aa1f743

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+16828
-7139
lines changed

apps/web/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"@chakra-ui/theme-tools": "^2.1.2",
3333
"@emotion/react": "^11.13.3",
3434
"@emotion/styled": "^11.13.0",
35+
"@json-rpc-tools/utils": "^1.7.6",
36+
"@nextui-org/react": "^2.4.6",
3537
"@reduxjs/toolkit": "^2.2.7",
3638
"@tanstack/react-query": "^5.55.0",
3739
"@taquito/beacon-wallet": "^20.0.1",
@@ -49,6 +51,8 @@
4951
"@umami/state": "workspace:^",
5052
"@umami/tezos": "workspace:^",
5153
"@umami/tzkt": "workspace:^",
54+
"@walletconnect/types": "^2.16.1",
55+
"@walletconnect/utils": "^2.16.1",
5256
"bignumber.js": "^9.1.2",
5357
"bip39": "^3.1.0",
5458
"cross-env": "^7.0.3",
@@ -76,6 +80,7 @@
7680
"react-test-renderer": "^18.3.1",
7781
"redux": "^5.0.1",
7882
"redux-persist": "^6.0.0",
83+
"valtio": "^2.0.0",
7984
"zod": "^3.23.8"
8085
},
8186
"devDependencies": {

apps/web/src/components/App/App.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ import { useCurrentAccount } from "@umami/state";
33
import { Layout } from "../../Layout";
44
import { Welcome } from "../../views/Welcome";
55
import { BeaconProvider } from "../beacon";
6+
import { WalletConnectProvider } from "../WalletConnect";
7+
import Modal from "../WalletConnect/Modal";
68

79
export const App = () => {
810
const currentAccount = useCurrentAccount();
911

1012
return currentAccount ? (
1113
<BeaconProvider>
12-
<Layout />
14+
<WalletConnectProvider>
15+
<Layout />
16+
<Modal />
17+
</WalletConnectProvider>
1318
</BeaconProvider>
1419
) : (
1520
<Welcome />
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Button, Divider, Text } from "@chakra-ui/react";
2-
import { useAddPeer } from "@umami/state";
2+
import { onConnect, useAddPeer } from "@umami/state";
33

44
import { BeaconPeers } from "../../beacon";
5+
import PairingsPage from "../../SendFlow/WalletConnect/pairings";
56
import { DrawerContentWrapper } from "../DrawerContentWrapper";
67

78
export const AppsMenu = () => {
@@ -10,19 +11,25 @@ export const AppsMenu = () => {
1011
return (
1112
<DrawerContentWrapper title="Apps">
1213
<Text marginTop="12px" size="lg">
13-
Connect with Pairing Request
14+
Connect with Pairing Request for Beacon or WalletConnect
1415
</Text>
1516
<Button
1617
width="fit-content"
1718
marginTop="18px"
1819
padding="0 24px"
19-
onClick={() => navigator.clipboard.readText().then(addPeer)}
20+
onClick={() =>
21+
navigator.clipboard.readText().then(
22+
// if payload starts with wc, call OnConnect else call addPeer
23+
payload => (payload.startsWith("wc:") ? onConnect(payload) : addPeer(payload))
24+
)
25+
}
2026
variant="secondary"
2127
>
2228
Connect
2329
</Button>
2430
<Divider marginTop={{ base: "36px", lg: "40px" }} />
2531
<BeaconPeers />
32+
<PairingsPage />
2633
</DrawerContentWrapper>
2734
);
2835
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
import { Button, Card, CardBody, Link, Text, Tooltip } from "@chakra-ui/react"
3+
import { truncate } from "@umami/tezos"
4+
5+
import { CrossedCircleIcon } from "../../../assets/icons"
6+
7+
/**
8+
* Types
9+
*/
10+
interface IProps {
11+
name?: string
12+
url?: string
13+
topic?: string
14+
onDelete: () => Promise<void>
15+
}
16+
17+
/**
18+
* Component
19+
*/
20+
export default function PairingCard({ name, url, topic, onDelete }: IProps) {
21+
return (
22+
<Card className="relative mb-6 min-h-[70px] border border-light">
23+
<CardBody className="flex flex-row items-center justify-between overflow-hidden p-4">
24+
<div className="flex-1">
25+
<Text className="ml-9" data-testid={"pairing-text-" + topic}>
26+
{name}
27+
</Text>
28+
<Link className="ml-9" data-testid={"pairing-text-" + topic} href={url}>
29+
{truncate(url?.split("https://")[1] ?? "Unknown", 23)}
30+
</Link>
31+
</div>
32+
<Tooltip content="Delete" placement="left">
33+
<Button className="min-w-auto text-error border-0 p-1 hover:bg-red-100 transition-all"
34+
data-testid={"pairing-delete-" + topic}
35+
onClick={onDelete}
36+
>
37+
<CrossedCircleIcon alt="delete icon" />
38+
</Button>
39+
</Tooltip>
40+
</CardBody>
41+
</Card>
42+
)
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
2+
import { Text } from "@chakra-ui/react"
3+
import { SettingsStore, web3wallet } from "@umami/state"
4+
import { type PairingTypes } from "@walletconnect/types"
5+
import { getSdkError } from "@walletconnect/utils"
6+
import { Fragment, useEffect } from "react"
7+
import { useSnapshot } from "valtio"
8+
9+
import PairingCard from "./PairingCard"
10+
11+
export default function PairingsPage() {
12+
const { pairings } = useSnapshot(SettingsStore.state)
13+
// const [walletPairings ] = useState(web3wallet.core.pairing.getPairings())
14+
15+
async function onDelete(topic: string) {
16+
await web3wallet.disconnectSession({ topic, reason: getSdkError("USER_DISCONNECTED") })
17+
const newPairings = pairings.filter(pairing => pairing.topic !== topic)
18+
SettingsStore.setPairings(newPairings as PairingTypes.Struct[])
19+
}
20+
21+
useEffect(() => {
22+
SettingsStore.setPairings(web3wallet.core.pairing.getPairings())
23+
}, [])
24+
25+
// console.log("pairings", walletPairings)
26+
return (
27+
<Fragment>
28+
{pairings.length ? (
29+
pairings.map(pairing => {
30+
const { peerMetadata } = pairing
31+
32+
return (
33+
<PairingCard
34+
key={pairing.topic}
35+
data-testid={"pairing-" + pairing.topic}
36+
logo={peerMetadata?.icons[0]}
37+
name={peerMetadata?.name}
38+
onDelete={() => onDelete(pairing.topic)}
39+
topic={pairing.topic}
40+
url={peerMetadata?.url}
41+
/>
42+
)
43+
})
44+
) : (
45+
<Text css={{ opacity: "0.5", textAlign: "center", marginTop: "$20" }}>No pairings</Text>
46+
)}
47+
</Fragment>
48+
)
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { formatJsonRpcError, formatJsonRpcResult } from "@json-rpc-tools/utils";
2+
import { type TezosToolkit } from "@taquito/taquito";
3+
import {
4+
type Account,
5+
type ImplicitAccount,
6+
type SecretKeyAccount,
7+
estimate,
8+
executeOperations,
9+
toAccountOperations,
10+
} from "@umami/core";
11+
import {
12+
TEZOS_SIGNING_METHODS,
13+
} from "@umami/state";
14+
import { type Network } from "@umami/tezos";
15+
import { type SignClientTypes } from "@walletconnect/types";
16+
import { getSdkError } from "@walletconnect/utils";
17+
18+
export async function approveTezosRequest(
19+
requestEvent: SignClientTypes.EventArguments["session_request"],
20+
tezosToolkit: TezosToolkit,
21+
signer: Account,
22+
network: Network
23+
) {
24+
const { params, id } = requestEvent;
25+
const { request } = params;
26+
27+
console.log("approveTezosRequest", request);
28+
29+
switch (request.method) {
30+
case TEZOS_SIGNING_METHODS.TEZOS_GET_ACCOUNTS: {
31+
console.log("TEZOS_GET_ACCOUNTS");
32+
return formatJsonRpcResult(id, [{
33+
algo: (signer as SecretKeyAccount).curve,
34+
address: signer.address.pkh,
35+
pubkey: (signer as SecretKeyAccount).pk,
36+
}]);
37+
}
38+
39+
case TEZOS_SIGNING_METHODS.TEZOS_SEND: {
40+
console.log("TEZOS_SEND");
41+
try {
42+
const operation = toAccountOperations(request.params.operations, signer as ImplicitAccount);
43+
const estimatedOperations = await estimate(operation, network);
44+
console.log("TEZOS_SEND: executing operation", estimatedOperations);
45+
const { opHash } = await executeOperations(estimatedOperations, tezosToolkit);
46+
console.log("TEZOS_SEND: executed operation", request.params.method, operation, opHash);
47+
return formatJsonRpcResult(id, { hash: opHash });
48+
} catch (error) {
49+
if (error instanceof Error) {
50+
console.error("Tezos_send operation failed with error: ", error.message);
51+
return formatJsonRpcError(id, error.message);
52+
} else {
53+
console.error("Tezos_send operation failed with unknown error: ", error);
54+
return formatJsonRpcError(id, "TEZOS_SEND failed with unknown error.");
55+
}
56+
}
57+
}
58+
// try {
59+
// const sendResponse = await wallet.signTransaction(request.params.operations)
60+
// return formatJsonRpcResult(id, { hash: sendResponse })
61+
// } catch (error) {
62+
// if (error instanceof Error) {
63+
// console.error('Tezos_send operation failed with error: ', error.message)
64+
// return formatJsonRpcError(id, error.message)
65+
// } else {
66+
// console.error('Tezos_send operation failed with unknown error: ', error)
67+
// return formatJsonRpcError(id, 'TEZOS_SEND failed with unknown error.')
68+
// }
69+
// }
70+
case TEZOS_SIGNING_METHODS.TEZOS_SIGN: {
71+
const result = await tezosToolkit.signer.sign(request.params.payload);
72+
console.log("TEZOS_SIGN", result.prefixSig);
73+
return formatJsonRpcResult(id, { signature: result.prefixSig });
74+
}
75+
// const signResponse = await wallet.signPayload(request.params.payload)
76+
// return formatJsonRpcResult(id, { signature: signResponse.prefixSig })
77+
78+
default:
79+
throw new Error(getSdkError("INVALID_METHOD").message);
80+
}
81+
}
82+
83+
export function rejectTezosRequest(request: SignClientTypes.EventArguments["session_request"]) {
84+
const { id } = request;
85+
86+
return formatJsonRpcError(id, getSdkError("USER_REJECTED_METHODS").message);
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
interface Props {
2+
address?: string;
3+
}
4+
5+
export default function ChainAddressMini({ address }: Props) {
6+
if (!address || address === "N/A") {return <></>;}
7+
return (
8+
<>
9+
<div>
10+
<span style={{ marginLeft: "5px" }}>
11+
{address.substring(0, 6)}...{address.substring(address.length - 6)}
12+
</span>
13+
</div>
14+
</>
15+
);
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Card, CardBody } from "@chakra-ui/react";
2+
import { type ReactNode } from "react";
3+
4+
interface Props {
5+
children: ReactNode | ReactNode[];
6+
rgb: string;
7+
flexDirection: "row" | "col";
8+
alignItems: "center" | "flex-start";
9+
flexWrap?: "wrap" | "nowrap";
10+
}
11+
12+
export default function ChainCard({ rgb, children, flexDirection, alignItems, flexWrap }: Props) {
13+
return (
14+
<Card
15+
className="mb-6 min-h-[70px] shadow-md rounded-lg border"
16+
style={{
17+
borderColor: `rgba(${rgb}, 0.4)`,
18+
boxShadow: `0 0 10px 0 rgba(${rgb}, 0.15)`,
19+
backgroundColor: `rgba(${rgb}, 0.25)`,
20+
}}
21+
>
22+
<CardBody
23+
className={`flex justify-between overflow-hidden
24+
${flexWrap === "wrap" ? "flex-wrap" : "flex-nowrap"}
25+
${flexDirection === "row" ? "flex-row" : "flex-col"}
26+
${alignItems === "center" ? "items-center" : "items-start"}
27+
`}
28+
>
29+
{children}
30+
</CardBody>
31+
</Card>
32+
);
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { getChainData } from "@umami/state";
2+
import { useMemo } from "react";
3+
4+
import { TezosLogoIcon } from "../../assets/icons";
5+
6+
interface Props {
7+
chainId?: string; // namespace + ":" + reference
8+
}
9+
10+
export default function ChainDataMini({ chainId }: Props) {
11+
const chainData = useMemo(() => getChainData(chainId), [chainId]);
12+
13+
if (!chainData) {return <></>;}
14+
return (
15+
<>
16+
<div>
17+
<TezosLogoIcon size="sm" />
18+
<span style={{ marginLeft: "5px" }}>{chainData.name}</span>
19+
</div>
20+
</>
21+
);
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Card } from "@chakra-ui/react";
2+
3+
import ChainAddressMini from "./ChainAddressMini";
4+
5+
type SmartAccount = {
6+
address: string;
7+
type: string;
8+
};
9+
10+
interface Props {
11+
account: SmartAccount;
12+
}
13+
14+
export default function ChainSmartAddressMini({ account }: Props) {
15+
return (
16+
<div>
17+
<div>
18+
<Card>({account.type})</Card>
19+
<ChainAddressMini address={account.address} />
20+
</div>
21+
</div>
22+
);
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Card, Divider } from "@chakra-ui/react";
2+
import { ModalStore } from "@umami/state";
3+
import { useSnapshot } from "valtio";
4+
5+
import RequestModalContainer from "./RequestModalContainer";
6+
7+
8+
export default function LoadingModal() {
9+
const state = useSnapshot(ModalStore.state);
10+
const message = state.data?.loadingMessage;
11+
12+
return (
13+
<RequestModalContainer title="">
14+
<div style={{ textAlign: "center", padding: "20px" }}>
15+
<div>
16+
<div>
17+
<h3>Loading your request...</h3>
18+
</div>
19+
</div>
20+
{message ? (
21+
<div style={{ textAlign: "center" }}>
22+
<Divider />
23+
<Card>{message}</Card>
24+
</div>
25+
) : null}
26+
</div>
27+
</RequestModalContainer>
28+
);
29+
}

0 commit comments

Comments
 (0)