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} +
+
+ key image +
+
+ ); +} + +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"} +

+ + + ); +} + +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 ? ( + + ) : null} + + {!!status ? : null} + +

{status}

+ + ); +} + +function StageFour() { + return ( + <> + {/* eslint-disable-next-line react/no-unescaped-entities */} +

You're all set

+ + + + ); +} 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 ( + + ); + })} +
+ ); } - - -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={SAFE logo} + // startContent={SAFE logo} 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 ( + + + + + SAFE logo + + + + + + {!!connectedTo ? ( + <> + + {name ? {name} : null} + + {isBalanceLoaded ? ( + {balance.formatted} ETH + ) : null} + + + {connectedTo === "safe" ? ( + + ) : null} + {connectedTo === "walletconnect" ? ( + + ) : 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"] }