From 2ddaf487ef4a987fc2dc959368c02fe0655023de Mon Sep 17 00:00:00 2001 From: Manan Tank Date: Wed, 14 Feb 2024 20:22:23 +0530 Subject: [PATCH] SmartWallet react integration --- packages/thirdweb/src/react/index.tsx | 29 +- .../src/react/ui/ConnectWallet/Details.tsx | 3 +- .../smartWallet/SmartWalletConnectUI.tsx | 255 ++++++++++++++++++ .../wallets/smartWallet/smartWalletConfig.tsx | 61 +++++ packages/thirdweb/src/wallets/index.ts | 10 +- .../thirdweb/src/wallets/injected/index.ts | 8 +- .../thirdweb/src/wallets/injected/types.ts | 2 + .../thirdweb/src/wallets/interfaces/wallet.ts | 6 +- .../thirdweb/src/wallets/manager/storage.ts | 4 +- packages/thirdweb/src/wallets/smart/index.ts | 110 ++++++-- .../thirdweb/src/wallets/smart/lib/calls.ts | 5 +- .../thirdweb/src/wallets/smart/lib/userop.ts | 7 +- packages/thirdweb/src/wallets/smart/types.ts | 5 +- 13 files changed, 461 insertions(+), 44 deletions(-) create mode 100644 packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx create mode 100644 packages/thirdweb/src/react/wallets/smartWallet/smartWalletConfig.tsx diff --git a/packages/thirdweb/src/react/index.tsx b/packages/thirdweb/src/react/index.tsx index 9f70545d671..bf7cdfb32b0 100644 --- a/packages/thirdweb/src/react/index.tsx +++ b/packages/thirdweb/src/react/index.tsx @@ -58,11 +58,32 @@ export { export { createContractQuery } from "./utils/createQuery.js"; // wallets -export { metamaskConfig } from "./wallets/metamask/metamaskConfig.js"; +export { + metamaskConfig, + type MetamaskConfigOptions, +} from "./wallets/metamask/metamaskConfig.js"; + export { coinbaseConfig } from "./wallets/coinbase/coinbaseConfig.js"; -export { rainbowConfig } from "./wallets/rainbow/rainbowConfig.js"; -export { walletConnectConfig } from "./wallets/walletConnect/walletConnectConfig.js"; -export { zerionConfig } from "./wallets/zerion/zerionConfig.js"; + +export { + rainbowConfig, + type RainbowConfigOptions, +} from "./wallets/rainbow/rainbowConfig.js"; + +export { + walletConnectConfig, + type WalletConnectConfigOptions, +} from "./wallets/walletConnect/walletConnectConfig.js"; + +export { + zerionConfig, + type ZerionConfigOptions, +} from "./wallets/zerion/zerionConfig.js"; + +export { + smartWalletConfig, + type SmartWalletConfigOptions, +} from "./wallets/smartWallet/smartWalletConfig.js"; export type { SupportedTokens } from "./ui/ConnectWallet/defaultTokens.js"; export { defaultTokens } from "./ui/ConnectWallet/defaultTokens.js"; diff --git a/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx index 5c0f79db265..a2ecd868a36 100644 --- a/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx +++ b/packages/thirdweb/src/react/ui/ConnectWallet/Details.tsx @@ -117,7 +117,8 @@ export const ConnectedWalletDetails: React.FC<{ // const wrapperWalletConfig = // wrapperWallet && walletContext.getWalletConfig(wrapperWallet); - const disableSwitchChain = false; + const disableSwitchChain = !activeAccount?.wallet.switchChain; + // const disableSwitchChain = !!personalWallet; // const isActuallyMetaMask = diff --git a/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx b/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx new file mode 100644 index 00000000000..bcae5d0684b --- /dev/null +++ b/packages/thirdweb/src/react/wallets/smartWallet/SmartWalletConnectUI.tsx @@ -0,0 +1,255 @@ +import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import type { ConnectUIProps, WalletConfig } from "../../types/wallets.js"; +import { HeadlessConnectUI } from "../headlessConnectUI.js"; +import { SmartWallet, type Account } from "../../../wallets/index.js"; +import { useThirdwebProviderProps } from "../../hooks/others/useThirdwebProviderProps.js"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { useTWLocale } from "../../providers/locale-provider.js"; +import { ModalConfigCtx } from "../../providers/wallet-ui-states-provider.js"; +import { Spacer } from "../../ui/components/Spacer.js"; +import { Spinner } from "../../ui/components/Spinner.js"; +import { Container, ModalHeader } from "../../ui/components/basic.js"; +import { Button } from "../../ui/components/buttons.js"; +import { iconSize, spacing, fontSize } from "../../ui/design-system/index.js"; +import { Text } from "../../ui/components/text.js"; + +/** + * @internal + */ +export const SmartConnectUI = (props: { + connectUIProps: ConnectUIProps; + personalWalletConfig: WalletConfig; + smartWalletChainId: bigint; +}) => { + const [personalAccount, setPersonalAccount] = useState(null); + // const { personalWalletConnection } = useWalletContext(); + const { personalWalletConfig } = props; + + const { client, dappMetadata } = useThirdwebProviderProps(); + + if (!personalAccount) { + const _props: ConnectUIProps = { + walletConfig: personalWalletConfig, + screenConfig: props.connectUIProps.screenConfig, + createInstance() { + return props.personalWalletConfig.create({ + client: client, + dappMetadata: dappMetadata, + }); + }, + done(account: Account) { + setPersonalAccount(account); + }, + chains: [], + chainId: props.smartWalletChainId, + }; + + if (personalWalletConfig.connectUI) { + return ; + } + + return ; + } + + return ( + { + // props.connected(); + // }} + // smartWallet={walletConfig} + // personalWalletConfig={personalWalletConfig} + // personalWallet={personalWalletConnection.activeWallet} + // personalWalletChainId={personalWalletConnection.chainId || 1} + // switchChainPersonalWallet={personalWalletConnection.switchChain} + /> + ); +}; + +const SmartWalletConnecting = (props: { + connectUIProps: ConnectUIProps; + personalAccount: Account; + personalWalletConfig: WalletConfig; + smartWalletChainId: bigint; +}) => { + const locale = useTWLocale().wallets.smartWallet; + // const { personalWallet, personalWalletChainId, switchChainPersonalWallet } = + // props; + const createSmartWalletInstance = props.connectUIProps.createInstance; + const wrongNetwork = + props.personalAccount.wallet.chainId !== props.smartWalletChainId; + const { personalAccount } = props; + const { done } = props.connectUIProps; + + const [smartWalletConnectionStatus, setSmartWalletConnectionStatus] = + useState<"connecting" | "connect-error" | "idle">("idle"); + const [personalWalletChainSwitchStatus, setPersonalWalletChainSwitchStatus] = + useState<"switching" | "switch-error" | "idle">("idle"); + + const connectStarted = useRef(false); + + const modalSize = useContext(ModalConfigCtx).modalSize; + + const handleConnect = useCallback(async () => { + if (!personalAccount) { + throw new Error("No personal account"); + } + + setSmartWalletConnectionStatus("connecting"); + + try { + const smartWallet = createSmartWalletInstance() as SmartWallet; // TODO: fix this type + const smartAccount = await smartWallet.connect({ + personalAccount, + }); + + done(smartAccount); + setSmartWalletConnectionStatus("idle"); + } catch (e) { + console.error(e); + setSmartWalletConnectionStatus("connect-error"); + } + }, [createSmartWalletInstance, done, personalAccount]); + + useEffect(() => { + if (!wrongNetwork && !connectStarted.current) { + handleConnect(); + connectStarted.current = true; + } + }, [handleConnect, wrongNetwork]); + + if (smartWalletConnectionStatus === "connecting") { + return ( + + + {locale.connecting} + + + + + ); + } + + if (smartWalletConnectionStatus === "connect-error") { + return ( + + {locale.failedToConnect} + + ); + } + + return ( + + + + + + {modalSize === "compact" && } + + + + + + + + + + + {locale.wrongNetworkScreen.title} + + + + + + {locale.wrongNetworkScreen.subtitle} + + + + + + + + + + {locale.wrongNetworkScreen.failedToSwitch} + + + + + + ); +}; diff --git a/packages/thirdweb/src/react/wallets/smartWallet/smartWalletConfig.tsx b/packages/thirdweb/src/react/wallets/smartWallet/smartWalletConfig.tsx new file mode 100644 index 00000000000..9fe94eb6452 --- /dev/null +++ b/packages/thirdweb/src/react/wallets/smartWallet/smartWalletConfig.tsx @@ -0,0 +1,61 @@ +import { + smartWallet, + type SmartWalletOptions, +} from "../../../wallets/index.js"; +import type { WalletConfig } from "../../types/wallets.js"; +import { SmartConnectUI } from "./SmartWalletConnectUI.js"; + +export type SmartWalletConfigOptions = Omit< + SmartWalletOptions, + "personalAccount" | "client" +>; + +/** + * Integrate Smart wallet connection into your app. + * @param walletConfig - WalletConfig object of a personal wallet to use with the smart wallet. + * @param options - Options for configuring the Smart wallet. + * @example + * ```tsx + * + * wallets={[ + * smartWalletConfig(metamaskConfig(), smartWalletOptions), + * smartWalletConfig(coinbaseConfig(), smartWalletOptions) + * ]} + * + * + * ``` + * @returns WalletConfig object to be passed into `ThirdwebProvider` + */ +export const smartWalletConfig = ( + walletConfig: WalletConfig, + options: SmartWalletConfigOptions, +): WalletConfig => { + const config: WalletConfig = { + metadata: walletConfig.metadata, + create(createOptions) { + const wallet = smartWallet({ ...options, client: createOptions.client }); + wallet.metadata = walletConfig.metadata; + return wallet; + }, + connectUI(props) { + const chain = options.chain; + const chainId = + typeof chain === "bigint" + ? chain + : typeof chain === "number" + ? BigInt(chain) + : BigInt(chain.id); + + return ( + + ); + }, + }; + + return config; +}; diff --git a/packages/thirdweb/src/wallets/index.ts b/packages/thirdweb/src/wallets/index.ts index a82c51d862b..420b6a6401a 100644 --- a/packages/thirdweb/src/wallets/index.ts +++ b/packages/thirdweb/src/wallets/index.ts @@ -1,8 +1,4 @@ -export type { - Wallet, - Account, - WalletConnectionOptions, -} from "./interfaces/wallet.js"; +export type { Wallet, Account } from "./interfaces/wallet.js"; export type { WalletEventListener } from "./interfaces/listeners.js"; export type { WalletMetadata } from "./types.js"; @@ -28,6 +24,7 @@ export type { WalletRDNS, InjectedWalletOptions, SpecificInjectedWalletOptions, + InjectedWalletConnectOptions, } from "./injected/types.js"; export { injectedProvider } from "./injected/mipdStore.js"; @@ -64,4 +61,5 @@ export { export type { WalletConnectConnectionOptions } from "./wallet-connect/types.js"; // smart -export { smartWallet } from "./smart/index.js"; +export { smartWallet, SmartWallet } from "./smart/index.js"; +export type { SmartWalletOptions } from "./smart/types.js"; diff --git a/packages/thirdweb/src/wallets/injected/index.ts b/packages/thirdweb/src/wallets/injected/index.ts index d12fe6f9ce3..500e239234c 100644 --- a/packages/thirdweb/src/wallets/injected/index.ts +++ b/packages/thirdweb/src/wallets/injected/index.ts @@ -11,10 +11,12 @@ import { import type { Address } from "abitype"; import type { Ethereum } from "../interfaces/ethereum.js"; import { normalizeChainId } from "../utils/normalizeChainId.js"; -import type { InjectedWalletOptions } from "./types.js"; +import type { + InjectedWalletConnectOptions, + InjectedWalletOptions, +} from "./types.js"; import { getMIPDStore, injectedProvider } from "./mipdStore.js"; import type { - WalletConnectionOptions, SendTransactionOption, Wallet, Account, @@ -133,7 +135,7 @@ export class InjectedWallet implements Wallet { * ``` * @returns A Promise that resolves to the connected address. */ - async connect(options?: WalletConnectionOptions) { + async connect(options?: InjectedWalletConnectOptions) { const provider = this.getProvider(); this.provider = provider; diff --git a/packages/thirdweb/src/wallets/injected/types.ts b/packages/thirdweb/src/wallets/injected/types.ts index fe72fbdb791..53369e0f55b 100644 --- a/packages/thirdweb/src/wallets/injected/types.ts +++ b/packages/thirdweb/src/wallets/injected/types.ts @@ -29,3 +29,5 @@ export type SpecificInjectedWalletOptions = Omit< InjectedWalletOptions, "walletId" >; + +export type InjectedWalletConnectOptions = { chainId?: bigint | number }; diff --git a/packages/thirdweb/src/wallets/interfaces/wallet.ts b/packages/thirdweb/src/wallets/interfaces/wallet.ts index da963298231..eb17142470c 100644 --- a/packages/thirdweb/src/wallets/interfaces/wallet.ts +++ b/packages/thirdweb/src/wallets/interfaces/wallet.ts @@ -15,13 +15,11 @@ export type SendTransactionOption = TransactionSerializable & { chainId: number; }; -export type WalletConnectionOptions = { chainId?: number | bigint }; - export type Wallet = { // REQUIRED metadata: WalletMetadata; - connect: (options?: WalletConnectionOptions) => Promise; - autoConnect: () => Promise; + connect: (options?: any) => Promise; + autoConnect: (options?: any) => Promise; disconnect: () => Promise; // OPTIONAL diff --git a/packages/thirdweb/src/wallets/manager/storage.ts b/packages/thirdweb/src/wallets/manager/storage.ts index 76da5aa2c16..b5fa13fe96f 100644 --- a/packages/thirdweb/src/wallets/manager/storage.ts +++ b/packages/thirdweb/src/wallets/manager/storage.ts @@ -1,5 +1,3 @@ -import type { WalletConnectionOptions } from "../interfaces/wallet.js"; - export type WalletStorage = { get: (key: string) => Promise; set: (key: string, value: string) => Promise; @@ -72,7 +70,7 @@ export async function saveConnectParamsToStorage( export async function deleteConnectParamsFromStorage(walletId: string) { const currentValueStr = await walletStorage.get(CONNECT_PARAMS_MAP_KEY); - let value: Record; + let value: Record; if (currentValueStr) { try { diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index 7e6b2d23388..4c3913dafe8 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -3,7 +3,10 @@ import type { SendTransactionOption, Wallet, } from "../interfaces/wallet.js"; -import type { SmartWalletOptions } from "./types.js"; +import type { + SmartWalletConnectionOptions, + SmartWalletOptions, +} from "./types.js"; import { createUnsignedUserOp, signUserOp } from "./lib/userop.js"; import { bundleUserOp } from "./lib/bundler.js"; import { getContract } from "../../contract/contract.js"; @@ -25,29 +28,96 @@ import { predictAddress } from "./lib/calls.js"; * ``` */ export function smartWallet(options: SmartWalletOptions): Wallet { - const wallet = { - metadata: { + return new SmartWallet(options); +} + +/** + * + */ +export class SmartWallet implements Wallet { + private options: SmartWalletOptions; + metadata: Wallet["metadata"]; + chainId?: bigint | undefined; + + /** + * Create an instance of the SmartWallet. + * @param options - The options for the smart wallet. + * @example + * ```ts + * const wallet = new SmartWallet(options) + * ``` + */ + constructor(options: SmartWalletOptions) { + this.options = options; + this.metadata = { name: "SmartWallet", id: "smart-wallet", iconUrl: "", - }, - async connect(): Promise { - return smartAccount(wallet, options); - }, - async autoConnect(): Promise { - // TODO autoconnect personal account too - return smartAccount(wallet, options); - }, - async disconnect(): Promise { - // TODO - }, - }; - return wallet; + }; + } + + /** + * Connect to the smart wallet using a personal account. + * @param connectionOptions - The options for connecting to the smart wallet. + * @example + * ```ts + * const smartAccount = await wallet.connect({ + * personalAccount, + * }) + * ``` + * @returns The connected smart account. + */ + async connect( + connectionOptions: SmartWalletConnectionOptions, + ): Promise { + const chainId = BigInt( + typeof this.options.chain === "object" + ? this.options.chain.id + : this.options.chain, + ); + + if (connectionOptions.personalAccount.wallet.chainId !== chainId) { + throw new Error( + "Personal account's wallet is on a different chain than the smart wallet.", + ); + } + + return smartAccount(this, { ...this.options, ...connectionOptions }); + } + + /** + * Auto connect the smart wallet using a personal account. + * @param connectionOptions - The options for connecting to the smart wallet. + * @example + * ```ts + * const smartAccount = await wallet.autoConnect({ + * personalAccount, + * }) + * ``` + * @returns The connected smart account. + */ + async autoConnect( + connectionOptions: SmartWalletConnectionOptions, + ): Promise { + return this.connect(connectionOptions); + } + + /** + * Disconnect smart wallet. + * @example + * ```ts + * await wallet.disconnect() + * ``` + */ + async disconnect(): Promise { + // noop + // should we disconnect the personal account? + } } async function smartAccount( wallet: Wallet, - options: SmartWalletOptions, + options: SmartWalletOptions & { personalAccount: Account }, ): Promise { const factoryContract = getContract({ client: options.client, @@ -60,6 +130,12 @@ async function smartAccount( address: accountAddress, chain: options.chain, }); + + wallet.chainId = + typeof options.chain === "object" + ? BigInt(options.chain.id) + : BigInt(options.chain); + return { wallet, address: accountAddress, diff --git a/packages/thirdweb/src/wallets/smart/lib/calls.ts b/packages/thirdweb/src/wallets/smart/lib/calls.ts index 39fc4b03ada..b9e8ab6a834 100644 --- a/packages/thirdweb/src/wallets/smart/lib/calls.ts +++ b/packages/thirdweb/src/wallets/smart/lib/calls.ts @@ -6,13 +6,14 @@ import { prepareContractCall, type PreparedTransaction, } from "../../../index.js"; +import type { Account } from "../../index.js"; /** * @internal */ export async function predictAddress( factoryContract: ThirdwebContract, - options: SmartWalletOptions, + options: SmartWalletOptions & { personalAccount: Account }, ): Promise { if (options.overrides?.predictAddress) { return options.overrides.predictAddress(factoryContract); @@ -32,7 +33,7 @@ export async function predictAddress( */ export function prepareCreateAccount(args: { factoryContract: ThirdwebContract; - options: SmartWalletOptions; + options: SmartWalletOptions & { personalAccount: Account }; }): PreparedTransaction { const { factoryContract, options } = args; if (options.overrides?.createAccount) { diff --git a/packages/thirdweb/src/wallets/smart/lib/userop.ts b/packages/thirdweb/src/wallets/smart/lib/userop.ts index 3e96355a1b9..7da06be343e 100644 --- a/packages/thirdweb/src/wallets/smart/lib/userop.ts +++ b/packages/thirdweb/src/wallets/smart/lib/userop.ts @@ -11,6 +11,7 @@ import { getPaymasterAndData } from "./paymaster.js"; import { estimateUserOpGas } from "./bundler.js"; import { randomNonce } from "./utils.js"; import { prepareCreateAccount, prepareExecute } from "./calls.js"; +import type { Account } from "../../interfaces/wallet.js"; /** * Create an unsigned user operation @@ -24,7 +25,7 @@ export async function createUnsignedUserOp(args: { factoryContract: ThirdwebContract; accountContract: ThirdwebContract; transaction: SendTransactionOption; - options: SmartWalletOptions; + options: SmartWalletOptions & { personalAccount: Account }; }): Promise { const { factoryContract, accountContract, transaction, options } = args; const isDeployed = await isContractDeployed(accountContract); @@ -140,7 +141,7 @@ export async function createUnsignedUserOp(args: { */ export async function signUserOp(args: { userOp: UserOperation; - options: SmartWalletOptions; + options: SmartWalletOptions & { personalAccount: Account }; }): Promise { const { userOp, options } = args; const userOpHash = getUserOpHash({ @@ -165,7 +166,7 @@ export async function signUserOp(args: { async function getAccountInitCode(args: { factoryContract: ThirdwebContract; - options: SmartWalletOptions; + options: SmartWalletOptions & { personalAccount: Account }; }): Promise { const { factoryContract, options } = args; const deployTx = prepareCreateAccount({ diff --git a/packages/thirdweb/src/wallets/smart/types.ts b/packages/thirdweb/src/wallets/smart/types.ts index a352a8e8cfc..88f3e46589d 100644 --- a/packages/thirdweb/src/wallets/smart/types.ts +++ b/packages/thirdweb/src/wallets/smart/types.ts @@ -6,7 +6,6 @@ import type { Address, Hex } from "viem"; export type SmartWalletOptions = { client: ThirdwebClient; - personalAccount: Account; chain: Chain; gasless: boolean; factoryAddress: string; // TODO make this optional @@ -27,6 +26,10 @@ export type SmartWalletOptions = { }; }; +export type SmartWalletConnectionOptions = { + personalAccount: Account; +}; + export type UserOperation = { sender: Address; nonce: bigint;