From 8afb2c457f97edde8b4da56d257238beef708f9b Mon Sep 17 00:00:00 2001 From: Edward Sun Date: Sat, 28 Sep 2024 01:24:47 -0700 Subject: [PATCH] Updates --- .../thirdweb/src/pay/buyWithFiat/getQuote.ts | 7 + .../ConnectWallet/screens/Buy/BuyScreen.tsx | 47 +++++- .../screens/Buy/fiat/FiatSteps.tsx | 117 +++----------- .../screens/Buy/fiat/useDeploySmartWallet.tsx | 143 ++++++++++++++++++ 4 files changed, 212 insertions(+), 102 deletions(-) create mode 100644 packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/useDeploySmartWallet.tsx diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts index dfa8fe027e5..6c56f8e6e7c 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -237,6 +237,13 @@ export type BuyWithFiatQuote = { * */ onRampLink: string; + + /** + * If enabled, this onramp transaction is eligible for "single-step execution", + * in which an entire intent is executed end-to-end from the source token/chain + * to the desired destination token or transaction. + */ + isSingleStepExecutionEnabled?: boolean; }; /** diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx index 68f3d2a2c25..402cbabe54e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/BuyScreen.tsx @@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useMemo, useState } from "react"; import { trackPayEvent } from "../../../../../../analytics/track.js"; import type { Chain } from "../../../../../../chains/types.js"; +import { getCachedChain } from "../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../../constants/addresses.js"; import type { GetBuyWithCryptoQuoteParams } from "../../../../../../pay/buyWithCrypto/getQuote.js"; @@ -58,6 +59,7 @@ import { WalletSelectorButton } from "./WalletSelectorButton.js"; import { CurrencySelection } from "./fiat/CurrencySelection.js"; import { FiatFlow } from "./fiat/FiatFlow.js"; import type { CurrencyMeta } from "./fiat/currencies.js"; +import { useDeploySmartWallet } from "./fiat/useDeploySmartWallet.js"; import type { SelectedScreen } from "./main/types.js"; import { type PaymentMethods, @@ -1347,6 +1349,7 @@ function FiatScreenContent(props: { defaultRecipientAddress || props.payer.account.address; const { drawerRef, drawerOverlayRef, isOpen, setIsOpen } = useDrawer(); const [drawerScreen, setDrawerScreen] = useState<"fees">("fees"); + const { deploySmartWallet } = useDeploySmartWallet(props.client); const buyWithFiatOptions = props.payOptions.buyWithFiat; @@ -1369,17 +1372,55 @@ function FiatScreenContent(props: { : undefined, ); - function handleSubmit() { + const executeSmartWalletDeploy = async () => { if (!fiatQuoteQuery.data) { return; } + try { + const chain = getCachedChain( + fiatQuoteQuery.data.onRampToken.token.chainId, + ); + const result = await deploySmartWallet({ + chain, + intentId: fiatQuoteQuery.data.intentId, + onRampTokenMeta: { + tokenAddress: fiatQuoteQuery.data.onRampToken.token.tokenAddress, + }, + onRampTokenAmount: fiatQuoteQuery.data.estimatedToAmountMinWei, + }); + console.log("result is ", result); + console.log("Smart wallet deployed:", result); + + return { + onRampLink: result.onRampLink, + }; + } catch (err) { + console.error("Failed to deploy smart wallet:", err); + // Continue + } + }; + + const handleSubmit = async () => { + if (!fiatQuoteQuery.data) { + return; + } + + const smartWalletDeployData = await executeSmartWalletDeploy(); + // const smartWalletDeployData = fiatQuoteQuery.data + // .isSingleStepExecutionEnabled + // ? await executeSmartWalletDeploy() + // : undefined; + + const onRampLink = + smartWalletDeployData?.onRampLink || fiatQuoteQuery.data.onRampLink; + const hasTwoSteps = isSwapRequiredPostOnramp(fiatQuoteQuery.data); let openedWindow: Window | null = null; if (!hasTwoSteps) { openedWindow = openOnrampPopup( - fiatQuoteQuery.data.onRampLink, + onRampLink, typeof props.theme === "string" ? props.theme : props.theme.type, ); @@ -1394,7 +1435,7 @@ function FiatScreenContent(props: { quote: fiatQuoteQuery.data, openedWindow, }); - } + }; function showFees() { if (!fiatQuoteQuery.data) { diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx index 66c150f0f77..df84ea0539e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx @@ -4,19 +4,12 @@ import { TriangleDownIcon, } from "@radix-ui/react-icons"; import { useMemo } from "react"; -import type { Chain } from "../../../../../../../chains/types.js"; import { getCachedChain } from "../../../../../../../chains/utils.js"; import type { ThirdwebClient } from "../../../../../../../client/client.js"; import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js"; -import { getContract } from "../../../../../../../contract/contract.js"; -import { addSessionKey } from "../../../../../../../extensions/erc4337/account/addSessionKey.js"; import type { BuyWithFiatQuote } from "../../../../../../../pay/buyWithFiat/getQuote.js"; import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import { getPayBaseUrl } from "../../../../../../../pay/utils/definitions.js"; import { formatNumber } from "../../../../../../../utils/formatNumber.js"; -import { createAndSignUserOp } from "../../../../../../../wallets/smart/lib/userop.js"; -import { hexlifyUserOp } from "../../../../../../../wallets/smart/lib/utils.js"; -import { smartWallet } from "../../../../../../../wallets/smart/smart-wallet.js"; import { type Theme, fontSize, @@ -28,8 +21,6 @@ import { useChainExplorers, useChainName, } from "../../../../../../core/hooks/others/useChainQuery.js"; -import { useActiveAccount } from "../../../../../../core/hooks/wallets/useActiveAccount.js"; -import { useSwitchActiveWalletChain } from "../../../../../../core/hooks/wallets/useSwitchActiveWalletChain.js"; import type { TokenInfo } from "../../../../../../core/utils/defaultTokens.js"; import { Spacer } from "../../../../components/Spacer.js"; import { Spinner } from "../../../../components/Spinner.js"; @@ -45,6 +36,7 @@ import { getBuyWithFiatStatusMeta, } from "../pay-transactions/statusMeta.js"; import { getCurrencyMeta } from "./currencies.js"; +import { useDeploySmartWallet } from "./useDeploySmartWallet.js"; export type BuyWithFiatPartialQuote = { intentId: string; @@ -117,97 +109,24 @@ export function FiatSteps(props: { intentId, } = props.partialQuote; - const account = useActiveAccount(); - const switchChain = useSwitchActiveWalletChain(); - - const deploySmartWallet = async ({ chain }: { chain: Chain }) => { - if (!account) return; - - await switchChain(chain); - - const smartWalletHandle = smartWallet({ - chain, - sponsorGas: true, - }); - - const smartAccount = await smartWalletHandle.connect({ - client: props.client, - personalAccount: account, - }); - console.log("smartAccount", smartAccount); - - const smartAccountContract = getContract({ - client: props.client, - chain, - address: smartAccount.address, - }); - - console.log("onRampTokenAmount", props.partialQuote.onRampTokenAmount); - const sessionKeyTx = addSessionKey({ - contract: smartAccountContract, - account: account, - // TODO: Env var this. - // // prod - // sessionKeyAddress: "0x1629Ce9Df01B10E7CF8837f559037A49d983aA10", // pay engine backend wallet - // dev - sessionKeyAddress: "0x32DC86f866e9F5Ed59A60b18c3B0f9b972a928F0", // dev engine backend wallet - permissions: { - approvedTargets: "*", // the addresses of allowed contracts, or '*' for any contract - permissionStartTimestamp: new Date(), // the date when the session key becomes active - permissionEndTimestamp: new Date(Date.now() + 24 * 60 * 60 * 1000), // the date when the session key expires - // TODO: Perhaps add a buffer here to be safe? - nativeTokenLimitPerTransaction: props.partialQuote.onRampTokenAmount, - }, - }); - - const signedUserOp = await createAndSignUserOp({ - transactions: [sessionKeyTx], - adminAccount: account, - client: props.client, - smartWalletOptions: { + const { deploySmartWallet, isLoading, error } = useDeploySmartWallet( + props.client, + ); + + const handleDeploySmartWallet = async ({ chain }) => { + try { + const result = await deploySmartWallet({ chain, - sponsorGas: true, - }, - }); - - const hexlifiedUserOp = hexlifyUserOp(signedUserOp); - console.log("signedUserOp", signedUserOp); - console.log("hexlifiedUserOp", hexlifiedUserOp); - console.log("sessionKeyTx", sessionKeyTx); - - console.log("chainId", chain.id); - console.log("intentId", intentId); - console.log("toAddress", account.address); - const response = await fetch( - `${getPayBaseUrl()}/v2/intent-wallets/deploy`, - { - method: "POST", - body: JSON.stringify({ - chainId: chain.id, - intentId: intentId, - intentType: "buyWithFiat", - signedUserOps: [hexlifiedUserOp], - toAddress: account.address, - smartWalletAddress: smartAccount.address, - }), - headers: { - "Content-Type": "application/json", - "x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, - }, - }, - ); - - const data = await response.json(); - console.log("response from server", data); - - props.setOnRampLinkOverride?.(data.onRampLink); - - return { - smartWalletAddress: smartAccount.address, - userAddress: account.address, - }; + intentId: props.partialQuote.intentId, + onRampTokenMeta: props.partialQuote.onRampToken, + onRampTokenAmount: props.partialQuote.onRampTokenAmount, + setOnRampLinkOverride: props.setOnRampLinkOverride, + }); + console.log("Smart wallet deployed:", result); + } catch (err) { + console.error("Failed to deploy smart wallet:", err); + } }; - const currency = getCurrencyMeta(fromCurrencySymbol); const isPartialSuccess = statusMeta?.progressStatus === "partialSuccess"; @@ -417,7 +336,7 @@ export function FiatSteps(props: { {/* Step 1 */} - diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/useDeploySmartWallet.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/useDeploySmartWallet.tsx new file mode 100644 index 00000000000..640c0fc22d2 --- /dev/null +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/useDeploySmartWallet.tsx @@ -0,0 +1,143 @@ +import { useState } from "react"; +import type { Chain } from "../../../../../../../chains/types.js"; +import type { ThirdwebClient } from "../../../../../../../client/client.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js"; +import { getContract } from "../../../../../../../contract/contract.js"; +import { approve } from "../../../../../../../extensions/erc20/write/approve.js"; +import { addSessionKey } from "../../../../../../../extensions/erc4337/account/addSessionKey.js"; +import { getPayBaseUrl } from "../../../../../../../pay/utils/definitions.js"; +import { createAndSignUserOp } from "../../../../../../../wallets/smart/lib/userop.js"; +import { hexlifyUserOp } from "../../../../../../../wallets/smart/lib/utils.js"; +import { smartWallet } from "../../../../../../../wallets/smart/smart-wallet.js"; +import { useActiveAccount } from "../../../../../../core/hooks/wallets/useActiveAccount.js"; +import { useSwitchActiveWalletChain } from "../../../../../../core/hooks/wallets/useSwitchActiveWalletChain.js"; + +export function useDeploySmartWallet(client: ThirdwebClient) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const account = useActiveAccount(); + const switchChain = useSwitchActiveWalletChain(); + + const deploySmartWallet = async ({ + chain, + intentId, + onRampTokenMeta, + onRampTokenAmount, + setOnRampLinkOverride, + }: { + chain: Chain; + intentId: string; + onRampTokenMeta: { + tokenAddress: string; + }; + onRampTokenAmount: string; + setOnRampLinkOverride?: (link: string) => void; + }) => { + if (!account) { + throw new Error("No active account"); + } + + setIsLoading(true); + setError(null); + + try { + await switchChain(chain); + + const smartWalletHandle = smartWallet({ + chain, + sponsorGas: true, + }); + + const smartAccount = await smartWalletHandle.connect({ + client, + personalAccount: account, + }); + + const smartAccountContract = getContract({ + client, + chain, + address: smartAccount.address, + }); + + const transactionsToSign = []; + + const sessionKeyTx = addSessionKey({ + contract: smartAccountContract, + account: account, + // TODO: Env var this. + sessionKeyAddress: "0x32DC86f866e9F5Ed59A60b18c3B0f9b972a928F0", // dev engine backend wallet + permissions: { + approvedTargets: "*", + permissionStartTimestamp: new Date(), + permissionEndTimestamp: new Date(Date.now() + 24 * 60 * 60 * 1000), + nativeTokenLimitPerTransaction: onRampTokenAmount, + }, + }); + transactionsToSign.push(sessionKeyTx); + + if (onRampTokenMeta.tokenAddress !== NATIVE_TOKEN_ADDRESS) { + console.log("adding approveTx"); + const approveTx = approve({ + contract: getContract({ + client, + chain, + address: onRampTokenMeta.tokenAddress, + }), + spender: smartAccount.address, + amount: onRampTokenAmount, + }); + transactionsToSign.push(approveTx); + } + + console.log("transactionsToSign is ", transactionsToSign); + const signedUserOp = await createAndSignUserOp({ + transactions: transactionsToSign, + adminAccount: account, + client, + smartWalletOptions: { + chain, + sponsorGas: true, + }, + }); + + const hexlifiedUserOp = hexlifyUserOp(signedUserOp); + + const response = await fetch( + `${getPayBaseUrl()}/v2/intent-wallets/deploy`, + { + method: "POST", + body: JSON.stringify({ + chainId: chain.id, + intentId, + intentType: "buyWithFiat", + signedUserOp: hexlifiedUserOp, + toAddress: account.address, + smartWalletAddress: smartAccount.address, + // TODO: Update this to use additional actions in the future. + action: "TRANSFER", + }), + headers: { + "Content-Type": "application/json", + "x-client-id": process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, + }, + }, + ); + + const data = await response.json(); + setOnRampLinkOverride?.(data.onRampLink); + + return { + smartWalletAddress: smartAccount.address, + userAddress: account.address, + onRampLink: data.onRampLink, + }; + } catch (err) { + setError(err instanceof Error ? err : new Error("An error occurred")); + throw err; + } finally { + setIsLoading(false); + } + }; + + return { deploySmartWallet, isLoading, error }; +}