From d82bf9a543591eab3c06eb784043c3e8b9c996f3 Mon Sep 17 00:00:00 2001
From: Igor Stadnyk
Date: Sun, 14 Jul 2024 06:58:10 +0200
Subject: [PATCH] Fix account radio layout
---
frontend/app/page.js | 244 ++++++++++++++++++
frontend/app/providers.tsx | 10 +-
frontend/components/AccountRadioGroup.js | 70 ++---
frontend/components/ConnectSafeButton.tsx | 4 +-
frontend/components/LoadingState.js | 3 +
frontend/components/navbar.js | 89 +++++++
frontend/context/SafeInfoContextProvider.js | 55 ++++
frontend/hooks/useCreateNewWallet.js | 69 +++++
.../hooks/useExternalSmartAccountClient.js | 81 +++---
frontend/hooks/useUniversalAccountInfo.js | 69 +++++
frontend/next.config.js | 3 +
frontend/services/deployNewSmartAccount.js | 15 ++
frontend/services/installModule.js | 2 +-
frontend/tsconfig.json | 7 +-
14 files changed, 631 insertions(+), 90 deletions(-)
create mode 100644 frontend/app/page.js
create mode 100644 frontend/components/LoadingState.js
create mode 100644 frontend/components/navbar.js
create mode 100644 frontend/context/SafeInfoContextProvider.js
create mode 100644 frontend/hooks/useCreateNewWallet.js
create mode 100644 frontend/hooks/useUniversalAccountInfo.js
create mode 100644 frontend/services/deployNewSmartAccount.js
diff --git a/frontend/app/page.js b/frontend/app/page.js
new file mode 100644
index 0000000..596584b
--- /dev/null
+++ b/frontend/app/page.js
@@ -0,0 +1,244 @@
+"use client";
+
+import Image from "next/image";
+import { useEffect, useState } from "react";
+import { Link } from "@nextui-org/link";
+import { Button } from "@nextui-org/button";
+import { CircularProgress } from "@nextui-org/progress";
+import { useWalletClient } from "wagmi";
+import { walletClientToSmartAccountSigner } from "permissionless";
+
+import keyImage from "@/images/key.png";
+import ConnectSafeButton from "@/components/ConnectSafeButton";
+import useUniversalAccountInfo from "@/hooks/useUniversalAccountInfo";
+import { getSafesByOwner } from "@/services/getSafesByOwner";
+import AccountRadioGroup from "@/components/AccountRadioGroup";
+import { useCreateNewWallet } from "@/hooks/useCreateNewWallet";
+import { isWingmanModuleInitialized } from "@/services/installModule";
+import { useSafeInfoContextProvider } from "@/context/SafeInfoContextProvider";
+import {
+ prepareSafeAccount,
+ prepareSmartAccountClient,
+} from "@/services/prepareSmartAccountClient";
+
+export default function Home() {
+ const { connectedTo, address } = useUniversalAccountInfo();
+
+ const [safes, setSafes] = useState({
+ isLoaded: false,
+ data: [],
+ });
+
+ useEffect(() => {
+ if (!address) return;
+
+ getSafesByOwner(address).then((data) => {
+ console.log("data", data);
+ setSafes({
+ isLoaded: true,
+ data,
+ });
+ });
+ }, [address]);
+
+ const [stage, setStage] = useState(1);
+
+ const { data: walletClient } = useWalletClient();
+
+ const { setSafeInfo, safeInfo } = useSafeInfoContextProvider();
+
+ useEffect(() => {
+ (async () => {
+ if (!connectedTo || !walletClient) {
+ console.log("disconnected or walletClient missing");
+
+ return setStage(1);
+ }
+
+ if (!safeInfo.address && safes.isLoaded) {
+ console.log("safes not loaded or not safe wallet - fetch safes");
+
+ return setStage(2);
+ }
+
+ console.log(safeInfo);
+ const isModuleSupported = await safeInfo.accountClient
+ .supportsModule({
+ type: "fallback",
+ })
+ .catch((err) => {
+ console.log(err);
+
+ return false;
+ });
+
+ if (!isModuleSupported) {
+ console.log("Unsupported Safe Wallet");
+
+ return setStage(5);
+ } //dead end
+
+ const isWingmanDeployed = await isWingmanModuleInitialized(
+ safeInfo.accountClient,
+ );
+
+ if (!isWingmanDeployed) {
+ console.log("compatible wallet, need to install module");
+
+ return setStage(3); //but to install wingman only
+ }
+
+ console.log("wingman installed");
+
+ return setStage(4);
+ })();
+ }, [connectedTo, safes.isLoaded, walletClient, safeInfo]);
+
+ async function handleSelectExternalAddress(address) {
+ console.log("wallet client", walletClient);
+ const smartAccountSigner =
+ await walletClientToSmartAccountSigner(walletClient);
+ const safeSmartAccount = await prepareSafeAccount(
+ smartAccountSigner,
+ address,
+ );
+ const smartAccountClient =
+ await prepareSmartAccountClient(safeSmartAccount);
+
+ setSafeInfo({
+ address: address,
+ accountClient: smartAccountClient,
+ });
+ }
+
+ return (
+
+
+ {stage === 1 ? : null}
+
+ {stage === 2 ? (
+ setStage(3)}
+ safes={safes.data}
+ selectAddress={handleSelectExternalAddress}
+ />
+ ) : null}
+
+ {stage === 3 ? setStage(4)} /> : null}
+
+ {stage === 4 ? : null}
+
+ {stage === 5 ? (
+ <>
+ Your Safe wallet do not support erc7579, choose different wallet or
+ connect metamask to create new one
+ >
+ ) : null}
+
+
+
+
+
+ );
+}
+
+function StageOne() {
+ return (
+ <>
+
+ Staying On-Chain in Any Situation
+
+
+ Prepare for the unexpected. Web3 Wingman lets you set automated
+ transfers from your wallet to a chosen receiver on a specific date.
+ Whether facing a medical procedure or an adventure, safeguard your
+ assets and support your loved ones if things don't go as planned.
+
+
+
+ >
+ );
+}
+
+function StageTwo({ safes, selectAddress, createNewSafe }) {
+ return (
+ <>
+ {safes.length ? (
+ <>
+ Select Safe account to use
+
+ >
+ ) : null}
+
+
+ {safes.length ? "Or create a new one" : "Create new Safe account"}
+
+
+ Create new Safe
+
+ >
+ );
+}
+
+function StageThree() {
+ const { safeInfo, setSafeInfo } = useSafeInfoContextProvider();
+ const { createNewWallet, status } = useCreateNewWallet(
+ safeInfo.accountClient,
+ );
+
+ function create() {
+ (async () => {
+ createNewWallet().then((newAccountClient) => {
+ setSafeInfo({
+ accountClient: newAccountClient,
+ address: newAccountClient.account.address,
+ });
+ });
+ })();
+ }
+
+ return (
+ <>
+ Creating new wallet
+
+ {!status ? (
+
+ Create new Safe
+
+ ) : null}
+
+ {!!status ? : null}
+
+ {status}
+ >
+ );
+}
+
+function StageFour() {
+ return (
+ <>
+ {/* eslint-disable-next-line react/no-unescaped-entities */}
+ You're all set
+
+
+ Go to dashboard
+
+ >
+ );
+}
diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx
index 51dd78b..d9e254c 100644
--- a/frontend/app/providers.tsx
+++ b/frontend/app/providers.tsx
@@ -13,14 +13,18 @@ export interface ProvidersProps {
themeProps?: ThemeProviderProps;
}
+import { SafeInfoContextProvider } from "@/context/SafeInfoContextProvider";
+
export function Providers({ children, themeProps }: ProvidersProps) {
const router = useRouter();
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/frontend/components/AccountRadioGroup.js b/frontend/components/AccountRadioGroup.js
index 8eb24a1..c183f6d 100644
--- a/frontend/components/AccountRadioGroup.js
+++ b/frontend/components/AccountRadioGroup.js
@@ -1,47 +1,27 @@
-import React from "react";
-import {RadioGroup, Radio, cn} from "@nextui-org/react";
+import React from 'react';
+import { Button } from '@nextui-org/react';
+export default function AccountRadioGroup({ safes, onChange }) {
+ return (
+
+ {safes.map((safe) => {
+ const isCompatible = safe.version === '1.4.1';
+ const description = !isCompatible ? "Can't be used" : '';
-export default function AccountRadioGroup({safes, onChange}) {
-
- console.log('account radio', safes);
-
- return (
-
- {
- safes.map((safe) => {
- const isCompatible = safe.version === '1.4.1';
- const description = !isCompatible ? 'Can\'t be used' : '';
- return (
-
- {safe.address}
-
- )
- })
- }
-
- );
+ return (
+
+
+ {safe.address}
+ {description}
+
+
+ );
+ })}
+
+ );
}
-
-
-export const CustomRadio = (props) => {
- const {children, ...otherProps} = props;
-
- return (
-
- {children}
-
- );
-};
diff --git a/frontend/components/ConnectSafeButton.tsx b/frontend/components/ConnectSafeButton.tsx
index 5e338ba..e0778f8 100644
--- a/frontend/components/ConnectSafeButton.tsx
+++ b/frontend/components/ConnectSafeButton.tsx
@@ -13,10 +13,10 @@ const ConnectSafeButton = () => {
className="font-semibold"
color="primary"
size="lg"
- startContent={ }
+ // startContent={ }
onClick={() => open({ view: 'Connect' })}
>
- Connect Safe
+ Connect wallet
);
diff --git a/frontend/components/LoadingState.js b/frontend/components/LoadingState.js
new file mode 100644
index 0000000..a8274f4
--- /dev/null
+++ b/frontend/components/LoadingState.js
@@ -0,0 +1,3 @@
+export function LoadingState() {
+
+}
\ No newline at end of file
diff --git a/frontend/components/navbar.js b/frontend/components/navbar.js
new file mode 100644
index 0000000..c0b127c
--- /dev/null
+++ b/frontend/components/navbar.js
@@ -0,0 +1,89 @@
+"use client";
+
+import Image from "next/image";
+import {
+ Navbar as NextUINavbar,
+ NavbarContent,
+ NavbarBrand,
+ NavbarItem,
+} from "@nextui-org/navbar";
+import { Button } from "@nextui-org/button";
+import NextLink from "next/link";
+import { useWeb3Modal } from "@web3modal/wagmi/react";
+import { useBalance, useDisconnect } from "wagmi";
+
+import ConnectSafeButton from "@/components/ConnectSafeButton";
+import logo from "@/images/logo.svg";
+import logout from "@/images/logout.svg";
+import useUniversalAccountInfo from "@/hooks/useUniversalAccountInfo";
+
+export const Navbar = () => {
+ const { disconnect } = useDisconnect();
+ const { open } = useWeb3Modal();
+
+ const { connectedTo, address, name, chainId } = useUniversalAccountInfo();
+
+ const { data: balance, isSuccess: isBalanceLoaded } = useBalance({
+ address: address,
+ chainId: chainId,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {!!connectedTo ? (
+ <>
+
+ {name ? {name} : null}
+
+ {isBalanceLoaded ? (
+ {balance.formatted} ETH
+ ) : null}
+
+
+ {connectedTo === "safe" ? (
+
+ {address && `${address.slice(0, 4)}...${address.slice(-4)}`}
+
+ ) : null}
+ {connectedTo === "walletconnect" ? (
+ }
+ size="lg"
+ variant="bordered"
+ onClick={() => disconnect()}
+ >
+ {address && `${address.slice(0, 4)}...${address.slice(-4)}`}
+
+ ) : null}
+ >
+ ) : (
+
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/frontend/context/SafeInfoContextProvider.js b/frontend/context/SafeInfoContextProvider.js
new file mode 100644
index 0000000..8a5f62c
--- /dev/null
+++ b/frontend/context/SafeInfoContextProvider.js
@@ -0,0 +1,55 @@
+import React, { useState, useContext, useEffect } from "react";
+import { useWalletClient } from "wagmi";
+import { walletClientToSmartAccountSigner } from "permissionless";
+
+import useUniversalAccountInfo from "@/hooks/useUniversalAccountInfo";
+import {
+ prepareSafeAccount,
+ prepareSmartAccountClient,
+} from "@/services/prepareSmartAccountClient";
+
+export const SafeInfoContext = React.createContext({
+ safeInfo: {
+ address: "",
+ accountClient: null,
+ isOnboarded: false,
+ },
+ setSafeInfo: (safeInfo) => {},
+});
+
+export function SafeInfoContextProvider({ children }) {
+ const [safeInfo, setSafeInfo] = useState({
+ address: "",
+ accountClient: null,
+ isOnboarded: false,
+ });
+
+ const { connectedTo, address } = useUniversalAccountInfo();
+
+ const { data: walletClient } = useWalletClient();
+
+ useEffect(() => {
+ if (!walletClient) return;
+ (async () => {
+ if (connectedTo === "safe") {
+ const smartAccountSigner =
+ await walletClientToSmartAccountSigner(walletClient);
+ const safeSmartAccount = await prepareSafeAccount(smartAccountSigner);
+ const smartAccountClient =
+ await prepareSmartAccountClient(safeSmartAccount);
+
+ setSafeInfo({ accountClient: smartAccountClient, address: address });
+ }
+ })();
+ }, [connectedTo, walletClient]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSafeInfoContextProvider() {
+ return useContext(SafeInfoContext);
+}
diff --git a/frontend/hooks/useCreateNewWallet.js b/frontend/hooks/useCreateNewWallet.js
new file mode 100644
index 0000000..c177ebc
--- /dev/null
+++ b/frontend/hooks/useCreateNewWallet.js
@@ -0,0 +1,69 @@
+import { walletClientToSmartAccountSigner } from "permissionless";
+import { useWalletClient } from "wagmi";
+import { useState } from "react";
+
+import { deployNewSmartAccount } from "@/services/deployNewSmartAccount";
+import { prepareSmartAccountClient } from "@/services/prepareSmartAccountClient";
+import {
+ installWingmanModule,
+ isWingmanModuleInitialized,
+} from "@/services/installModule";
+
+export function useCreateNewWallet(existingAccountClient) {
+ const [status, setStatus] = useState("");
+
+ const { data: walletClient } = useWalletClient();
+
+ async function createNewWallet() {
+ if (!walletClient) return console.log("wallet client is not ready");
+
+ setStatus("Connecting to signer wallet");
+
+ let smartAccountClient;
+
+ // creating new wallet
+ if (!existingAccountClient) {
+ const smartAccountSigner =
+ await walletClientToSmartAccountSigner(walletClient);
+
+ console.log("smartAccountSigner", smartAccountSigner);
+
+ setStatus("Deploying new ERC-7579 smart account");
+ const safeSmartAccount = await deployNewSmartAccount(smartAccountSigner);
+
+ console.log("safeSmartAccount", safeSmartAccount);
+
+ setStatus("Preparing smart account client");
+ smartAccountClient = await prepareSmartAccountClient(safeSmartAccount);
+ } else {
+ smartAccountClient = existingAccountClient;
+ }
+
+ console.log("smartAccountClient", smartAccountClient);
+
+ setStatus("Checking if everything fine");
+ const isModuleSupported = await smartAccountClient
+ .supportsModule({
+ type: "fallback",
+ })
+ .catch(() => false);
+
+ console.log("isModuleSupported", isModuleSupported);
+
+ if (!isModuleSupported) throw new Error("module not supported");
+
+ setStatus("Installing Web3 Wingman module");
+ const receipt = await installWingmanModule(smartAccountClient);
+
+ console.log("receipt", receipt);
+
+ const isWingmanDeployed =
+ await isWingmanModuleInitialized(smartAccountClient);
+
+ console.log("isWingmanDeployed", isWingmanDeployed);
+
+ return smartAccountClient
+ }
+
+ return { createNewWallet, status };
+}
diff --git a/frontend/hooks/useExternalSmartAccountClient.js b/frontend/hooks/useExternalSmartAccountClient.js
index 9ff85e2..bede048 100644
--- a/frontend/hooks/useExternalSmartAccountClient.js
+++ b/frontend/hooks/useExternalSmartAccountClient.js
@@ -1,54 +1,63 @@
-import {useEffect, useState} from "react";
-import {useWalletClient} from "wagmi";
-import {walletClientToSmartAccountSigner} from "permissionless";
-import {prepareSafeAccount, prepareSmartAccountClient} from "@/services/prepareSmartAccountClient";
-import {isWingmanModuleInitialized} from "@/services/installModule";
+import { useEffect, useState } from "react";
+import { useWalletClient } from "wagmi";
+import { walletClientToSmartAccountSigner } from "permissionless";
-export function useExternalSmartAccountClient(safeAccountAddress) {
- const [client, setClient] = useState({
- smartAccountClient: null,
- isModuleSupported: false,
- isWingmanDeployed: false
- });
+import {
+ prepareSafeAccount,
+ prepareSmartAccountClient,
+} from "@/services/prepareSmartAccountClient";
+import { isWingmanModuleInitialized } from "@/services/installModule";
- const { data: walletClient } = useWalletClient();
+export function useExternalSmartAccountClient(safeAccountAddress) {
+ const [client, setClient] = useState({
+ smartAccountClient: null,
+ isModuleSupported: false,
+ isWingmanDeployed: false,
+ });
- useEffect(() => {
- if (!walletClient || !safeAccountAddress) return;
+ const { data: walletClient } = useWalletClient();
- console.log('walletClient', walletClient);
+ useEffect(() => {
+ if (!walletClient || !safeAccountAddress) return;
- (async () => {
- const smartAccountSigner = await walletClientToSmartAccountSigner(walletClient);
+ console.log("walletClient", walletClient);
- console.log('smartAccountSigner', smartAccountSigner);
+ (async () => {
+ const smartAccountSigner =
+ await walletClientToSmartAccountSigner(walletClient);
- const safeSmartAccount = await prepareSafeAccount(smartAccountSigner, safeAccountAddress);
+ console.log("smartAccountSigner", smartAccountSigner);
- console.log('safeSmartAccount', safeSmartAccount)
+ const safeSmartAccount = await prepareSafeAccount(
+ smartAccountSigner,
+ safeAccountAddress,
+ );
- const smartAccountClient = await prepareSmartAccountClient(safeSmartAccount);
+ console.log("safeSmartAccount", safeSmartAccount);
- console.log('smartAccountClient', smartAccountClient);
+ const smartAccountClient =
+ await prepareSmartAccountClient(safeSmartAccount);
- const isModuleSupported = await smartAccountClient.supportsModule({
- type: "fallback"
- });
+ console.log("smartAccountClient", smartAccountClient);
- console.log('isModuleSupported', isModuleSupported)
+ const isModuleSupported = await smartAccountClient.supportsModule({
+ type: "fallback",
+ });
- const isWingmanDeployed = await isWingmanModuleInitialized(smartAccountClient);
+ console.log("isModuleSupported", isModuleSupported);
- console.log('isWingmanDeployed', isWingmanDeployed);
+ const isWingmanDeployed =
+ await isWingmanModuleInitialized(smartAccountClient);
- setClient({
- smartAccountClient,
- isModuleSupported,
- isWingmanDeployed
- });
- })()
+ console.log("isWingmanDeployed", isWingmanDeployed);
- }, [walletClient, safeAccountAddress]);
+ setClient({
+ smartAccountClient,
+ isModuleSupported,
+ isWingmanDeployed,
+ });
+ })();
+ }, [walletClient, safeAccountAddress]);
- return client;
+ return client;
}
diff --git a/frontend/hooks/useUniversalAccountInfo.js b/frontend/hooks/useUniversalAccountInfo.js
new file mode 100644
index 0000000..850630b
--- /dev/null
+++ b/frontend/hooks/useUniversalAccountInfo.js
@@ -0,0 +1,69 @@
+import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk";
+import { useWalletInfo } from "@web3modal/wagmi/react";
+import { useAccount } from "wagmi";
+import { useEffect, useState } from "react";
+
+export default function useUniversalAccountInfo() {
+ const { sdk, connected: isConnectedToSafe, safe } = useSafeAppsSDK();
+ const { walletInfo } = useWalletInfo();
+ const {
+ isConnected: isConnectedToWc,
+ address: wcAccount,
+ chainId: wcChainId,
+ } = useAccount();
+
+ const [accountInfo, setAccountInfo] = useState({
+ connectedTo: undefined,
+ });
+
+ useEffect(() => {
+ (async () => {
+ const connectedTo = isConnectedToSafe
+ ? "safe"
+ : isConnectedToWc
+ ? "walletconnect"
+ : undefined;
+
+ if (isConnectedToSafe) {
+ console.log({ sdk, safe });
+ const addressBook = await sdk.safe.requestAddressBook();
+ const accountName = addressBook.find(
+ (account) => account.address === safe.safeAddress,
+ )?.name;
+
+ setAccountInfo({
+ connectedTo,
+ address: safe.safeAddress,
+ chainId: safe.chainId,
+ name: accountName,
+ });
+
+ return;
+ }
+
+ if (isConnectedToWc) {
+ setAccountInfo({
+ connectedTo,
+ address: wcAccount,
+ chainId: wcChainId,
+ });
+
+ return;
+ }
+
+ // setAccountInfo({
+ // connectedTo
+ // })
+ })();
+ }, [
+ sdk,
+ isConnectedToSafe,
+ safe,
+ walletInfo,
+ isConnectedToWc,
+ wcAccount,
+ wcChainId,
+ ]);
+
+ return accountInfo;
+}
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 951ef5b..219273d 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -1,5 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
+ typescript: {
+ ignoreBuildErrors: true,
+ },
async headers() {
return [
{
diff --git a/frontend/services/deployNewSmartAccount.js b/frontend/services/deployNewSmartAccount.js
new file mode 100644
index 0000000..f699cdb
--- /dev/null
+++ b/frontend/services/deployNewSmartAccount.js
@@ -0,0 +1,15 @@
+import { ENTRYPOINT_ADDRESS_V07 } from "permissionless";
+import { signerToSafeSmartAccount } from "permissionless/accounts";
+
+import { publicClient } from "@/services/consts";
+
+export function deployNewSmartAccount(signer) {
+ return signerToSafeSmartAccount(publicClient, {
+ signer,
+ safeVersion: "1.4.1",
+ entryPoint: ENTRYPOINT_ADDRESS_V07,
+ saltNonce: 15n,
+ safe4337ModuleAddress: "0x3Fdb5BC686e861480ef99A6E3FaAe03c0b9F32e2",
+ erc7579LaunchpadAddress: "0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE",
+ });
+}
diff --git a/frontend/services/installModule.js b/frontend/services/installModule.js
index 60ae835..58ba3bc 100644
--- a/frontend/services/installModule.js
+++ b/frontend/services/installModule.js
@@ -44,7 +44,7 @@ export async function installWingmanModule(smartAccountClient) {
context: module.initData,
});
- const receipt = await pimlicoBundlerClient.waitForUserOperationReceipt({hash: opHash})
+ const receipt = await pimlicoBundlerClient.waitForUserOperationReceipt({hash: opHash, timeout: 100000})
console.log(receipt);
return receipt
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index c1bc97f..89540ff 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -4,9 +4,10 @@
"lib": ["esnext"],
"allowJs": true,
"skipLibCheck": true,
- "strict": true,
+ "strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
+ "noEmitOnError": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
@@ -23,6 +24,6 @@
"@/*": ["./*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/backups/page.jsx", "app/backups/page.jsx"],
- "exclude": ["node_modules"]
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules", "web3modal-safe-apps/node_modules"]
}