Skip to content

feat: WalletConnect, prototype #1933

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"@chakra-ui/theme-tools": "^2.1.2",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@json-rpc-tools/utils": "^1.7.6",
"@nextui-org/react": "^2.4.6",
"@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-query": "^5.55.0",
"@taquito/beacon-wallet": "^20.0.1",
Expand All @@ -49,6 +51,8 @@
"@umami/state": "workspace:^",
"@umami/tezos": "workspace:^",
"@umami/tzkt": "workspace:^",
"@walletconnect/types": "^2.16.1",
"@walletconnect/utils": "^2.16.1",
"bignumber.js": "^9.1.2",
"bip39": "^3.1.0",
"cross-env": "^7.0.3",
Expand Down Expand Up @@ -76,6 +80,7 @@
"react-test-renderer": "^18.3.1",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"valtio": "^2.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sure we don't need it. most likely you need to use redux that's already included

"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ import { useCurrentAccount } from "@umami/state";
import { Layout } from "../../Layout";
import { Welcome } from "../../views/Welcome";
import { BeaconProvider } from "../beacon";
import { WalletConnectProvider } from "../WalletConnect";
import Modal from "../WalletConnect/Modal";

export const App = () => {
const currentAccount = useCurrentAccount();

return currentAccount ? (
<BeaconProvider>
<Layout />
<WalletConnectProvider>
<Layout />
<Modal />
</WalletConnectProvider>
</BeaconProvider>
) : (
<Welcome />
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/components/Menu/AppsMenu/AppsMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Button, Divider, Text } from "@chakra-ui/react";
import { useAddPeer } from "@umami/state";
import { onConnect, useAddPeer } from "@umami/state";

import { BeaconPeers } from "../../beacon";
import PairingsPage from "../../SendFlow/WalletConnect/pairings";
import { DrawerContentWrapper } from "../DrawerContentWrapper";

export const AppsMenu = () => {
Expand All @@ -10,19 +11,25 @@ export const AppsMenu = () => {
return (
<DrawerContentWrapper title="Apps">
<Text marginTop="12px" size="lg">
Connect with Pairing Request
Connect with Pairing Request for Beacon or WalletConnect
</Text>
<Button
width="fit-content"
marginTop="18px"
padding="0 24px"
onClick={() => navigator.clipboard.readText().then(addPeer)}
onClick={() =>
navigator.clipboard.readText().then(
// if payload starts with wc, call OnConnect else call addPeer
payload => (payload.startsWith("wc:") ? onConnect(payload) : addPeer(payload))
)
}
variant="secondary"
>
Connect
</Button>
<Divider marginTop={{ base: "36px", lg: "40px" }} />
<BeaconPeers />
<PairingsPage />
</DrawerContentWrapper>
);
};
43 changes: 43 additions & 0 deletions apps/web/src/components/SendFlow/WalletConnect/PairingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

import { Button, Card, CardBody, Link, Text, Tooltip } from "@chakra-ui/react"
import { truncate } from "@umami/tezos"

import { CrossedCircleIcon } from "../../../assets/icons"

/**
* Types
*/
interface IProps {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't use interfaces, type is preferred

name?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why all the props are optional?

url?: string
topic?: string
onDelete: () => Promise<void>
}

/**
* Component
*/
export default function PairingCard({ name, url, topic, onDelete }: IProps) {
return (
<Card className="relative mb-6 min-h-[70px] border border-light">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't use classNames in the app. please check out chakra docs

<CardBody className="flex flex-row items-center justify-between overflow-hidden p-4">
<div className="flex-1">
<Text className="ml-9" data-testid={"pairing-text-" + topic}>
{name}
</Text>
<Link className="ml-9" data-testid={"pairing-text-" + topic} href={url}>
{truncate(url?.split("https://")[1] ?? "Unknown", 23)}
</Link>
</div>
<Tooltip content="Delete" placement="left">
<Button className="min-w-auto text-error border-0 p-1 hover:bg-red-100 transition-all"
data-testid={"pairing-delete-" + topic}
onClick={onDelete}
>
<CrossedCircleIcon alt="delete icon" />
</Button>
</Tooltip>
</CardBody>
</Card>
)
}
49 changes: 49 additions & 0 deletions apps/web/src/components/SendFlow/WalletConnect/pairings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

import { Text } from "@chakra-ui/react"
import { SettingsStore, web3wallet } from "@umami/state"
import { type PairingTypes } from "@walletconnect/types"
import { getSdkError } from "@walletconnect/utils"
import { Fragment, useEffect } from "react"
import { useSnapshot } from "valtio"

import PairingCard from "./PairingCard"

export default function PairingsPage() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't use default exports and function declarations like this. instead please use fatarrow functions like

export PairingsPage = () => {...}

const { pairings } = useSnapshot(SettingsStore.state)
// const [walletPairings ] = useState(web3wallet.core.pairing.getPairings())

async function onDelete(topic: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here as above

await web3wallet.disconnectSession({ topic, reason: getSdkError("USER_DISCONNECTED") })
const newPairings = pairings.filter(pairing => pairing.topic !== topic)
SettingsStore.setPairings(newPairings as PairingTypes.Struct[])
}

useEffect(() => {
SettingsStore.setPairings(web3wallet.core.pairing.getPairings())
}, [])

// console.log("pairings", walletPairings)
return (
<Fragment>
{pairings.length ? (
pairings.map(pairing => {
const { peerMetadata } = pairing

return (
<PairingCard
key={pairing.topic}
data-testid={"pairing-" + pairing.topic}
logo={peerMetadata?.icons[0]}
name={peerMetadata?.name}
onDelete={() => onDelete(pairing.topic)}
topic={pairing.topic}
url={peerMetadata?.url}
/>
)
})
) : (
<Text css={{ opacity: "0.5", textAlign: "center", marginTop: "$20" }}>No pairings</Text>
)}
</Fragment>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { formatJsonRpcError, formatJsonRpcResult } from "@json-rpc-tools/utils";
import { type TezosToolkit } from "@taquito/taquito";
import {
type Account,
type ImplicitAccount,
type SecretKeyAccount,
estimate,
executeOperations,
toAccountOperations,
} from "@umami/core";
import {
TEZOS_SIGNING_METHODS,
} from "@umami/state";
import { type Network } from "@umami/tezos";
import { type SignClientTypes } from "@walletconnect/types";
import { getSdkError } from "@walletconnect/utils";

export async function approveTezosRequest(
requestEvent: SignClientTypes.EventArguments["session_request"],
tezosToolkit: TezosToolkit,
signer: Account,
network: Network
) {
const { params, id } = requestEvent;
const { request } = params;

console.log("approveTezosRequest", request);

switch (request.method) {
case TEZOS_SIGNING_METHODS.TEZOS_GET_ACCOUNTS: {
console.log("TEZOS_GET_ACCOUNTS");
return formatJsonRpcResult(id, [{
algo: (signer as SecretKeyAccount).curve,
address: signer.address.pkh,
pubkey: (signer as SecretKeyAccount).pk,
}]);
}

case TEZOS_SIGNING_METHODS.TEZOS_SEND: {
console.log("TEZOS_SEND");
try {
const operation = toAccountOperations(request.params.operations, signer as ImplicitAccount);
const estimatedOperations = await estimate(operation, network);
console.log("TEZOS_SEND: executing operation", estimatedOperations);
const { opHash } = await executeOperations(estimatedOperations, tezosToolkit);
console.log("TEZOS_SEND: executed operation", request.params.method, operation, opHash);
return formatJsonRpcResult(id, { hash: opHash });
} catch (error) {
if (error instanceof Error) {
console.error("Tezos_send operation failed with error: ", error.message);
return formatJsonRpcError(id, error.message);
} else {
console.error("Tezos_send operation failed with unknown error: ", error);
return formatJsonRpcError(id, "TEZOS_SEND failed with unknown error.");
}
}
}

case TEZOS_SIGNING_METHODS.TEZOS_SIGN: {
const result = await tezosToolkit.signer.sign(request.params.payload);
console.log("TEZOS_SIGN", result.prefixSig);
return formatJsonRpcResult(id, { signature: result.prefixSig });
}

default:
throw new Error(getSdkError("INVALID_METHOD").message);
}
}

export function rejectTezosRequest(request: SignClientTypes.EventArguments["session_request"]) {
const { id } = request;

return formatJsonRpcError(id, getSdkError("USER_REJECTED_METHODS").message);
}
16 changes: 16 additions & 0 deletions apps/web/src/components/WalletConnect/ChainAddressMini.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface Props {
address?: string;
}

export default function ChainAddressMini({ address }: Props) {
if (!address || address === "N/A") {return <></>;}
return (
<>
<div>
<span style={{ marginLeft: "5px" }}>
{address.substring(0, 6)}...{address.substring(address.length - 6)}
</span>
</div>
</>
);
}
33 changes: 33 additions & 0 deletions apps/web/src/components/WalletConnect/ChainCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Card, CardBody } from "@chakra-ui/react";
import { type ReactNode } from "react";

interface Props {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this interface can be replaced with PropsWithChildren<CardProps>

children: ReactNode | ReactNode[];
rgb: string;
flexDirection: "row" | "col";
alignItems: "center" | "flex-start";
flexWrap?: "wrap" | "nowrap";
}

export default function ChainCard({ rgb, children, flexDirection, alignItems, flexWrap }: Props) {
return (
<Card
className="mb-6 min-h-[70px] shadow-md rounded-lg border"
style={{
borderColor: `rgba(${rgb}, 0.4)`,
boxShadow: `0 0 10px 0 rgba(${rgb}, 0.15)`,
backgroundColor: `rgba(${rgb}, 0.25)`,
}}
>
<CardBody
className={`flex justify-between overflow-hidden
${flexWrap === "wrap" ? "flex-wrap" : "flex-nowrap"}
${flexDirection === "row" ? "flex-row" : "flex-col"}
${alignItems === "center" ? "items-center" : "items-start"}
`}
>
{children}
</CardBody>
</Card>
);
}
22 changes: 22 additions & 0 deletions apps/web/src/components/WalletConnect/ChainDataMini.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getChainData } from "@umami/state";
import { useMemo } from "react";

import { TezosLogoIcon } from "../../assets/icons";

interface Props {
chainId?: string; // namespace + ":" + reference
}

export default function ChainDataMini({ chainId }: Props) {
const chainData = useMemo(() => getChainData(chainId), [chainId]);

if (!chainData) {return <></>;}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return null

return (
<>
<div>
<TezosLogoIcon size="sm" />
<span style={{ marginLeft: "5px" }}>{chainData.name}</span>
</div>
</>
);
}
23 changes: 23 additions & 0 deletions apps/web/src/components/WalletConnect/ChainSmartAddressMini.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Card } from "@chakra-ui/react";

import ChainAddressMini from "./ChainAddressMini";

type SmartAccount = {
address: string;
type: string;
};

interface Props {
account: SmartAccount;
}

export default function ChainSmartAddressMini({ account }: Props) {
return (
<div>
<div>
<Card>({account.type})</Card>
<ChainAddressMini address={account.address} />
</div>
</div>
);
}
29 changes: 29 additions & 0 deletions apps/web/src/components/WalletConnect/LoadingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Card, Divider } from "@chakra-ui/react";
import { ModalStore } from "@umami/state";
import { useSnapshot } from "valtio";

import RequestModalContainer from "./RequestModalContainer";


export default function LoadingModal() {
const state = useSnapshot(ModalStore.state);
const message = state.data?.loadingMessage;

return (
<RequestModalContainer title="">
<div style={{ textAlign: "center", padding: "20px" }}>
<div>
<div>
<h3>Loading your request...</h3>
</div>
</div>
{message ? (
<div style={{ textAlign: "center" }}>
<Divider />
<Card>{message}</Card>
</div>
) : null}
</div>
</RequestModalContainer>
);
}
Loading
Loading