From 94e568f5b006758d9464cdf4c7a0ac80cae61c7c Mon Sep 17 00:00:00 2001
From: sehyunc <41171808+sehyunc@users.noreply.github.com>
Date: Thu, 26 Sep 2024 20:19:50 -0700
Subject: [PATCH] transfer dialog: wrap eth
---
app/components/wrap-eth-dialog.tsx | 139 +++++
components/dialogs/token-select.tsx | 21 +-
components/dialogs/transfer/default-form.tsx | 450 +++++++++++++++
components/dialogs/transfer/helpers.ts | 84 +++
.../dialogs/transfer/transfer-dialog.tsx | 527 +-----------------
components/dialogs/transfer/transfer-form.tsx | 52 ++
components/dialogs/transfer/weth-form.tsx | 507 +++++++++++++++++
.../dialogs/transfer/wrap-eth-warning.tsx | 73 +++
hooks/use-base-per-usd-price.ts | 18 +
hooks/use-wrap-eth.ts | 67 +++
lib/generated.ts | 72 +++
wagmi.config.ts | 6 +
12 files changed, 1486 insertions(+), 530 deletions(-)
create mode 100644 app/components/wrap-eth-dialog.tsx
create mode 100644 components/dialogs/transfer/default-form.tsx
create mode 100644 components/dialogs/transfer/helpers.ts
create mode 100644 components/dialogs/transfer/transfer-form.tsx
create mode 100644 components/dialogs/transfer/weth-form.tsx
create mode 100644 components/dialogs/transfer/wrap-eth-warning.tsx
create mode 100644 hooks/use-base-per-usd-price.ts
create mode 100644 hooks/use-wrap-eth.ts
diff --git a/app/components/wrap-eth-dialog.tsx b/app/components/wrap-eth-dialog.tsx
new file mode 100644
index 00000000..b54413db
--- /dev/null
+++ b/app/components/wrap-eth-dialog.tsx
@@ -0,0 +1,139 @@
+import React from "react"
+
+import { VisuallyHidden } from "@radix-ui/react-visually-hidden"
+import { Token } from "@renegade-fi/react"
+import { useQueryClient } from "@tanstack/react-query"
+import { formatUnits } from "viem"
+import { useAccount, useBalance } from "wagmi"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { Input } from "@/components/ui/input"
+
+import { useWrapEth } from "@/hooks/use-wrap-eth"
+import { useReadErc20BalanceOf } from "@/lib/generated"
+import { cn } from "@/lib/utils"
+
+export function WrapEthDialog(props: React.PropsWithChildren) {
+ return (
+
+ )
+}
+
+function Content() {
+ const { address } = useAccount()
+ const queryClient = useQueryClient()
+ const { data: ethBalance, queryKey: ethBalanceQueryKey } = useBalance({
+ address,
+ })
+ const weth = Token.findByTicker("WETH")
+ const { data: l2Balance, queryKey: wethBalanceQueryKey } =
+ useReadErc20BalanceOf({
+ address: weth?.address,
+ args: [address ?? "0x"],
+ })
+
+ const [amount, setAmount] = React.useState("")
+ const { wrapEth, status } = useWrapEth({
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ethBalanceQueryKey })
+ queryClient.invalidateQueries({ queryKey: wethBalanceQueryKey })
+ },
+ onError: (error) => {
+ console.error("Error wrapping ETH:", error)
+ // You can add error handling logic here, e.g., showing an error toast
+ },
+ })
+
+ const onSubmit = () => {
+ wrapEth(amount)
+ }
+
+ return (
+ <>
+
+
setAmount(e.target.value)}
+ />
+
+ WETH Balance:
+
+ {formatUnits(l2Balance ?? BigInt(0), weth?.decimals ?? 18)}
+
+
+
+ {ethBalance?.symbol} Balance:
+
+ {formatUnits(
+ ethBalance?.value ?? BigInt(0),
+ ethBalance?.decimals ?? 18,
+ )}
+
+
+
+
+
+
+ {status === "success" && Transaction confirmed!
}
+ >
+ )
+}
diff --git a/components/dialogs/token-select.tsx b/components/dialogs/token-select.tsx
index 085cb833..d94b94ac 100644
--- a/components/dialogs/token-select.tsx
+++ b/components/dialogs/token-select.tsx
@@ -3,9 +3,9 @@ import * as React from "react"
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
import { Token, useBackOfQueueWallet } from "@renegade-fi/react"
import { erc20Abi, isAddress } from "viem"
-import { useAccount, useReadContracts } from "wagmi"
+import { useAccount, useBalance, useReadContracts } from "wagmi"
-import { ExternalTransferDirection } from "@/components/dialogs/transfer/transfer-dialog"
+import { ExternalTransferDirection } from "@/components/dialogs/transfer/helpers"
import { Button } from "@/components/ui/button"
import {
Command,
@@ -43,6 +43,9 @@ export function TokenSelect({
}) {
const [open, setOpen] = React.useState(false)
const { address } = useAccount()
+
+ const { data: ethBalance } = useBalance({ address })
+
const { data: l2Balances, queryKey } = useReadContracts({
contracts: DISPLAY_TOKENS().map((token) => ({
address: token.address,
@@ -73,10 +76,16 @@ export function TokenSelect({
},
})
- const displayBalances =
- direction === ExternalTransferDirection.Deposit
- ? l2Balances
- : renegadeBalances
+ // TODO: Sometimes old balances are added
+ const displayBalances = React.useMemo(() => {
+ if (direction !== ExternalTransferDirection.Deposit) return renegadeBalances
+ if (!l2Balances) return undefined
+ const weth = Token.findByTicker("WETH")
+ const combinedEthBalance =
+ (l2Balances?.get(weth.address) ?? BigInt(0)) +
+ (ethBalance?.value ?? BigInt(0))
+ return new Map(l2Balances).set(weth.address, combinedEthBalance)
+ }, [direction, ethBalance?.value, l2Balances, renegadeBalances])
const isDesktop = useMediaQuery("(min-width: 1024px)")
diff --git a/components/dialogs/transfer/default-form.tsx b/components/dialogs/transfer/default-form.tsx
new file mode 100644
index 00000000..765b4c8c
--- /dev/null
+++ b/components/dialogs/transfer/default-form.tsx
@@ -0,0 +1,450 @@
+import * as React from "react"
+
+import { usePathname, useRouter } from "next/navigation"
+
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Token, UpdateType, useBalances } from "@renegade-fi/react"
+import { useQueryClient } from "@tanstack/react-query"
+import { useForm, UseFormReturn, useWatch } from "react-hook-form"
+import { toast } from "sonner"
+import { formatUnits } from "viem"
+import { useAccount } from "wagmi"
+import { z } from "zod"
+
+import { TokenSelect } from "@/components/dialogs/token-select"
+import {
+ checkAmount,
+ checkBalance,
+ ExternalTransferDirection,
+ formSchema,
+ isMaxBalance,
+} from "@/components/dialogs/transfer/helpers"
+import { MaxBalancesWarning } from "@/components/dialogs/transfer/max-balances-warning"
+import { useIsMaxBalances } from "@/components/dialogs/transfer/use-is-max-balances"
+import { NumberInput } from "@/components/number-input"
+import { Button } from "@/components/ui/button"
+import { DialogClose, DialogFooter } from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import {
+ ResponsiveTooltip,
+ ResponsiveTooltipContent,
+ ResponsiveTooltipTrigger,
+} from "@/components/ui/responsive-tooltip"
+
+import { useApprove } from "@/hooks/use-approve"
+import { useCheckChain } from "@/hooks/use-check-chain"
+import { useDeposit } from "@/hooks/use-deposit"
+import { useMaintenanceMode } from "@/hooks/use-maintenance-mode"
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { usePriceQuery } from "@/hooks/use-price-query"
+import { useRefreshOnBlock } from "@/hooks/use-refresh-on-block"
+import { useWithdraw } from "@/hooks/use-withdraw"
+import { MIN_DEPOSIT_AMOUNT, Side } from "@/lib/constants/protocol"
+import { constructStartToastMessage } from "@/lib/constants/task"
+import { formatNumber } from "@/lib/format"
+import { useReadErc20BalanceOf } from "@/lib/generated"
+import { cn } from "@/lib/utils"
+import { useSide } from "@/providers/side-provider"
+
+export function DefaultForm({
+ className,
+ direction,
+ form,
+ onSuccess,
+}: React.ComponentProps<"form"> & {
+ direction: ExternalTransferDirection
+ onSuccess: () => void
+ form: UseFormReturn>
+}) {
+ const isDesktop = useMediaQuery("(min-width: 1024px)")
+
+ const mint = useWatch({
+ control: form.control,
+ name: "mint",
+ })
+ const baseToken = mint
+ ? Token.findByAddress(mint as `0x${string}`)
+ : undefined
+ const { address } = useAccount()
+ const isMaxBalances = useIsMaxBalances(mint)
+
+ const renegadeBalances = useBalances()
+ const renegadeBalance = baseToken
+ ? renegadeBalances.get(baseToken.address)?.amount
+ : undefined
+
+ const formattedRenegadeBalance = baseToken
+ ? formatUnits(renegadeBalance ?? BigInt(0), baseToken.decimals)
+ : ""
+ const renegadeBalanceLabel = baseToken
+ ? formatNumber(renegadeBalance ?? BigInt(0), baseToken.decimals, true)
+ : ""
+
+ const { data: l2Balance, queryKey } = useReadErc20BalanceOf({
+ address: baseToken?.address,
+ args: [address ?? "0x"],
+ query: {
+ enabled:
+ direction === ExternalTransferDirection.Deposit &&
+ !!baseToken &&
+ !!address,
+ },
+ })
+
+ useRefreshOnBlock({ queryKey })
+
+ const formattedL2Balance = baseToken
+ ? formatUnits(l2Balance ?? BigInt(0), baseToken.decimals)
+ : ""
+ const l2BalanceLabel = baseToken
+ ? formatNumber(l2Balance ?? BigInt(0), baseToken.decimals, true)
+ : ""
+
+ const balance =
+ direction === ExternalTransferDirection.Deposit
+ ? formattedL2Balance
+ : formattedRenegadeBalance
+ const balanceLabel =
+ direction === ExternalTransferDirection.Deposit
+ ? l2BalanceLabel
+ : renegadeBalanceLabel
+
+ const amount = useWatch({
+ control: form.control,
+ name: "amount",
+ })
+ const hideMaxButton =
+ !mint || balance === "0" || amount.toString() === balance
+
+ const { handleDeposit, status: depositStatus } = useDeposit({
+ amount,
+ mint,
+ })
+
+ const { handleWithdraw } = useWithdraw({
+ amount,
+ mint,
+ })
+
+ const {
+ needsApproval,
+ handleApprove,
+ status: approveStatus,
+ } = useApprove({
+ amount: amount.toString(),
+ mint,
+ enabled: direction === ExternalTransferDirection.Deposit,
+ })
+
+ const { checkChain } = useCheckChain()
+
+ let buttonText = ""
+ if (direction === ExternalTransferDirection.Withdraw) {
+ buttonText = "Withdraw"
+ } else if (needsApproval) {
+ if (approveStatus === "pending") {
+ buttonText = "Confirm in wallet"
+ } else {
+ buttonText = "Approve & Deposit"
+ }
+ } else {
+ if (depositStatus === "pending") {
+ buttonText = "Confirm in wallet"
+ } else {
+ buttonText = "Deposit"
+ }
+ }
+ const router = useRouter()
+ const pathname = usePathname()
+ const isTradePage = pathname.includes("/trade")
+ const queryClient = useQueryClient()
+ // Ensure price is loaded
+ usePriceQuery(baseToken?.address || "0x")
+ const { setSide } = useSide()
+
+ async function onSubmit(values: z.infer) {
+ const isAmountSufficient = checkAmount(
+ queryClient,
+ values.amount,
+ baseToken,
+ )
+
+ if (direction === ExternalTransferDirection.Deposit) {
+ if (!isAmountSufficient) {
+ form.setError("amount", {
+ message: `Amount must be greater than or equal to ${MIN_DEPOSIT_AMOUNT} USDC`,
+ })
+ return
+ }
+ await checkChain()
+ const isBalanceSufficient = checkBalance({
+ amount: values.amount,
+ mint: values.mint,
+ balance: l2Balance,
+ })
+ if (!isBalanceSufficient) {
+ form.setError("amount", {
+ message: "Insufficient Arbitrum balance",
+ })
+ return
+ }
+ if (needsApproval) {
+ handleApprove({
+ onSuccess: () => {
+ handleDeposit({
+ onSuccess: (data) => {
+ form.reset()
+ onSuccess?.()
+ const message = constructStartToastMessage(UpdateType.Deposit)
+ toast.loading(message, {
+ id: data.taskId,
+ })
+ setSide(baseToken?.ticker === "USDC" ? Side.BUY : Side.SELL)
+ if (isTradePage && baseToken?.ticker !== "USDC") {
+ router.push(`/trade/${baseToken?.ticker}`)
+ }
+ },
+ })
+ },
+ })
+ } else {
+ handleDeposit({
+ onSuccess: (data) => {
+ form.reset()
+ onSuccess?.()
+ const message = constructStartToastMessage(UpdateType.Deposit)
+ toast.loading(message, {
+ id: data.taskId,
+ })
+ setSide(baseToken?.ticker === "USDC" ? Side.BUY : Side.SELL)
+ if (isTradePage && baseToken?.ticker !== "USDC") {
+ router.push(`/trade/${baseToken?.ticker}`)
+ }
+ },
+ })
+ }
+ } else {
+ const renegadeBalance = renegadeBalances.get(
+ values.mint as `0x${string}`,
+ )?.amount
+ // User is allowed to withdraw whole balance even if amount is < MIN_TRANSFER_AMOUNT
+ if (
+ !isAmountSufficient &&
+ !isMaxBalance({
+ amount: values.amount,
+ mint: values.mint,
+ balance: renegadeBalance,
+ })
+ ) {
+ form.setError("amount", {
+ message: `Amount must be greater than or equal to ${MIN_DEPOSIT_AMOUNT} USDC`,
+ })
+ return
+ }
+ // TODO: Check if balance is sufficient
+ const isBalanceSufficient = checkBalance({
+ amount: values.amount,
+ mint: values.mint,
+ balance: renegadeBalance,
+ })
+ if (!isBalanceSufficient) {
+ form.setError("amount", {
+ message: "Insufficient Renegade balance",
+ })
+ return
+ }
+
+ handleWithdraw({
+ onSuccess: (data) => {
+ form.reset()
+ onSuccess?.()
+ },
+ })
+ }
+ }
+
+ const { data: maintenanceMode } = useMaintenanceMode()
+
+ return (
+
+
+ )
+}
diff --git a/components/dialogs/transfer/helpers.ts b/components/dialogs/transfer/helpers.ts
new file mode 100644
index 00000000..e159fe91
--- /dev/null
+++ b/components/dialogs/transfer/helpers.ts
@@ -0,0 +1,84 @@
+import { Token } from "@renegade-fi/react"
+import { QueryClient } from "@tanstack/react-query"
+import { formatUnits } from "viem"
+import { z } from "zod"
+
+import { MIN_DEPOSIT_AMOUNT } from "@/lib/constants/protocol"
+import { safeParseUnits } from "@/lib/format"
+import { createPriceQueryKey } from "@/lib/query"
+
+export enum ExternalTransferDirection {
+ Deposit,
+ Withdraw,
+}
+
+export const formSchema = z.object({
+ amount: z
+ .string()
+ .min(1, { message: "Amount is required" })
+ .refine(
+ (value) => {
+ const num = parseFloat(value)
+ return !isNaN(num) && num > 0
+ },
+ { message: "Amount must be greater than zero" },
+ ),
+ mint: z.string().min(1, {
+ message: "Token is required",
+ }),
+})
+
+// Return true if the amount is greater than or equal to the minimum deposit amount (1 USDC)
+export function checkAmount(
+ queryClient: QueryClient,
+ amount: string,
+ baseToken?: Token,
+) {
+ if (!baseToken) return false
+ const usdPrice = queryClient.getQueryData(
+ createPriceQueryKey("binance", baseToken.address),
+ )
+ if (!usdPrice) return false
+ const amountInUsd = Number(amount) * usdPrice
+ return amountInUsd >= MIN_DEPOSIT_AMOUNT
+}
+
+// Returns true if the amount is less than or equal to the balance
+// Returns false if the amount is greater than the balance or if the amount is invalid
+export function checkBalance({
+ amount,
+ mint,
+ balance,
+}: z.infer & { balance?: bigint }) {
+ if (!balance) {
+ return false
+ }
+ try {
+ const token = Token.findByAddress(mint as `0x${string}`)
+ const parsedAmount = safeParseUnits(amount, token.decimals)
+ if (parsedAmount instanceof Error) {
+ return false
+ }
+ return parsedAmount <= balance
+ } catch (error) {
+ return false
+ }
+}
+
+// Returns true iff the amount is equal to the balance
+export function isMaxBalance({
+ amount,
+ mint,
+ balance,
+}: z.infer & { balance?: bigint }) {
+ if (!balance) {
+ return false
+ }
+ try {
+ const token = Token.findByAddress(mint as `0x${string}`)
+ const formattedAmount = formatUnits(balance, token.decimals)
+ return amount === formattedAmount
+ } catch (error) {
+ return false
+ }
+}
diff --git a/components/dialogs/transfer/transfer-dialog.tsx b/components/dialogs/transfer/transfer-dialog.tsx
index 742dd311..d8aa8ec9 100644
--- a/components/dialogs/transfer/transfer-dialog.tsx
+++ b/components/dialogs/transfer/transfer-dialog.tsx
@@ -1,84 +1,23 @@
import * as React from "react"
-import { usePathname, useRouter } from "next/navigation"
-
-import { zodResolver } from "@hookform/resolvers/zod"
import { VisuallyHidden } from "@radix-ui/react-visually-hidden"
-import { Token, UpdateType, useBalances, usePayFees } from "@renegade-fi/react"
-import { QueryClient, useQueryClient } from "@tanstack/react-query"
-import { Loader2 } from "lucide-react"
-import { useForm, useWatch } from "react-hook-form"
-import { toast } from "sonner"
-import { formatUnits } from "viem"
-import { useAccount } from "wagmi"
-import { z } from "zod"
+import { usePayFees } from "@renegade-fi/react"
-import { TokenSelect } from "@/components/dialogs/token-select"
-import { MaxBalancesWarning } from "@/components/dialogs/transfer/max-balances-warning"
-import { useIsMaxBalances } from "@/components/dialogs/transfer/use-is-max-balances"
-import { NumberInput } from "@/components/number-input"
+import { ExternalTransferDirection } from "@/components/dialogs/transfer/helpers"
+import { TransferForm } from "@/components/dialogs/transfer/transfer-form"
import { Button } from "@/components/ui/button"
import {
Dialog,
- DialogClose,
DialogContent,
DialogDescription,
- DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- ResponsiveTooltip,
- ResponsiveTooltipContent,
- ResponsiveTooltipTrigger,
-} from "@/components/ui/responsive-tooltip"
-import { useApprove } from "@/hooks/use-approve"
-import { useCheckChain } from "@/hooks/use-check-chain"
-import { useDeposit } from "@/hooks/use-deposit"
import { useFeeOnZeroBalance } from "@/hooks/use-fee-on-zero-balance"
-import { useMaintenanceMode } from "@/hooks/use-maintenance-mode"
import { useMediaQuery } from "@/hooks/use-media-query"
-import { usePriceQuery } from "@/hooks/use-price-query"
-import { useRefreshOnBlock } from "@/hooks/use-refresh-on-block"
-import { useWithdraw } from "@/hooks/use-withdraw"
-import { MIN_DEPOSIT_AMOUNT, Side } from "@/lib/constants/protocol"
-import { constructStartToastMessage } from "@/lib/constants/task"
-import { formatNumber, safeParseUnits } from "@/lib/format"
-import { useReadErc20BalanceOf } from "@/lib/generated"
-import { createPriceQueryKey } from "@/lib/query"
import { cn } from "@/lib/utils"
-import { useSide } from "@/providers/side-provider"
-
-const formSchema = z.object({
- amount: z
- .string()
- .min(1, { message: "Amount is required" })
- .refine(
- (value) => {
- const num = parseFloat(value)
- return !isNaN(num) && num > 0
- },
- { message: "Amount must be greater than zero" },
- ),
- mint: z.string().min(1, {
- message: "Token is required",
- }),
-})
-
-export enum ExternalTransferDirection {
- Deposit,
- Withdraw,
-}
export function TransferDialog({
mint,
@@ -236,463 +175,3 @@ export function TransferDialog({
)
}
-
-function TransferForm({
- className,
- direction,
- initialMint,
- onSuccess,
-}: React.ComponentProps<"form"> & {
- direction: ExternalTransferDirection
- initialMint?: string
- onSuccess: () => void
-}) {
- const isDesktop = useMediaQuery("(min-width: 1024px)")
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- amount: "",
- mint: initialMint ?? "",
- },
- })
-
- const mint = useWatch({
- control: form.control,
- name: "mint",
- })
- const baseToken = mint
- ? Token.findByAddress(mint as `0x${string}`)
- : undefined
- const { address } = useAccount()
- const isMaxBalances = useIsMaxBalances(mint)
-
- const renegadeBalances = useBalances()
- const renegadeBalance = baseToken
- ? renegadeBalances.get(baseToken.address)?.amount
- : undefined
-
- const formattedRenegadeBalance = baseToken
- ? formatUnits(renegadeBalance ?? BigInt(0), baseToken.decimals)
- : ""
- const renegadeBalanceLabel = baseToken
- ? formatNumber(renegadeBalance ?? BigInt(0), baseToken.decimals, true)
- : ""
-
- const { data: l2Balance, queryKey } = useReadErc20BalanceOf({
- address: baseToken?.address,
- args: [address ?? "0x"],
- query: {
- enabled:
- direction === ExternalTransferDirection.Deposit &&
- !!baseToken &&
- !!address,
- },
- })
-
- useRefreshOnBlock({ queryKey })
-
- const formattedL2Balance = baseToken
- ? formatUnits(l2Balance ?? BigInt(0), baseToken.decimals)
- : ""
- const l2BalanceLabel = baseToken
- ? formatNumber(l2Balance ?? BigInt(0), baseToken.decimals, true)
- : ""
-
- const balance =
- direction === ExternalTransferDirection.Deposit
- ? formattedL2Balance
- : formattedRenegadeBalance
- const balanceLabel =
- direction === ExternalTransferDirection.Deposit
- ? l2BalanceLabel
- : renegadeBalanceLabel
-
- const amount = useWatch({
- control: form.control,
- name: "amount",
- })
- const hideMaxButton =
- !mint || balance === "0" || amount.toString() === balance
-
- const { handleDeposit, status: depositStatus } = useDeposit({
- amount,
- mint,
- })
-
- const { handleWithdraw } = useWithdraw({
- amount,
- mint,
- })
-
- const {
- needsApproval,
- handleApprove,
- status: approveStatus,
- } = useApprove({
- amount: amount.toString(),
- mint,
- enabled: direction === ExternalTransferDirection.Deposit,
- })
-
- const { checkChain } = useCheckChain()
-
- let buttonText = ""
- if (direction === ExternalTransferDirection.Withdraw) {
- buttonText = "Withdraw"
- } else if (needsApproval) {
- if (approveStatus === "pending") {
- buttonText = "Confirm in wallet"
- } else {
- buttonText = "Approve & Deposit"
- }
- } else {
- if (depositStatus === "pending") {
- buttonText = "Confirm in wallet"
- } else {
- buttonText = "Deposit"
- }
- }
- const router = useRouter()
- const pathname = usePathname()
- const isTradePage = pathname.includes("/trade")
- const queryClient = useQueryClient()
- // Ensure price is loaded
- usePriceQuery(baseToken?.address || "0x")
- const { setSide } = useSide()
-
- async function onSubmit(values: z.infer) {
- const isAmountSufficient = checkAmount(
- queryClient,
- values.amount,
- baseToken,
- )
-
- if (direction === ExternalTransferDirection.Deposit) {
- if (!isAmountSufficient) {
- form.setError("amount", {
- message: `Amount must be greater than or equal to ${MIN_DEPOSIT_AMOUNT} USDC`,
- })
- return
- }
- await checkChain()
- const isBalanceSufficient = checkBalance({
- amount: values.amount,
- mint: values.mint,
- balance: l2Balance,
- })
- if (!isBalanceSufficient) {
- form.setError("amount", {
- message: "Insufficient Arbitrum balance",
- })
- return
- }
- if (needsApproval) {
- handleApprove({
- onSuccess: () => {
- handleDeposit({
- onSuccess: (data) => {
- form.reset()
- onSuccess?.()
- const message = constructStartToastMessage(UpdateType.Deposit)
- toast.success(message, {
- id: data.taskId,
- icon: ,
- })
- setSide(baseToken?.ticker === "USDC" ? Side.BUY : Side.SELL)
- if (isTradePage && baseToken?.ticker !== "USDC") {
- router.push(`/trade/${baseToken?.ticker}`)
- }
- },
- })
- },
- })
- } else {
- handleDeposit({
- onSuccess: (data) => {
- form.reset()
- onSuccess?.()
- const message = constructStartToastMessage(UpdateType.Deposit)
- toast.success(message, {
- id: data.taskId,
- icon: ,
- })
- setSide(baseToken?.ticker === "USDC" ? Side.BUY : Side.SELL)
- if (isTradePage && baseToken?.ticker !== "USDC") {
- router.push(`/trade/${baseToken?.ticker}`)
- }
- },
- })
- }
- } else {
- const renegadeBalance = renegadeBalances.get(
- values.mint as `0x${string}`,
- )?.amount
- // User is allowed to withdraw whole balance even if amount is < MIN_TRANSFER_AMOUNT
- if (
- !isAmountSufficient &&
- !isMaxBalance({
- amount: values.amount,
- mint: values.mint,
- balance: renegadeBalance,
- })
- ) {
- form.setError("amount", {
- message: `Amount must be greater than or equal to ${MIN_DEPOSIT_AMOUNT} USDC`,
- })
- return
- }
- // TODO: Check if balance is sufficient
- const isBalanceSufficient = checkBalance({
- amount: values.amount,
- mint: values.mint,
- balance: renegadeBalance,
- })
- if (!isBalanceSufficient) {
- form.setError("amount", {
- message: "Insufficient Renegade balance",
- })
- return
- }
-
- handleWithdraw({
- onSuccess: (data) => {
- form.reset()
- onSuccess?.()
- },
- })
- }
- }
-
- const { data: maintenanceMode } = useMaintenanceMode()
-
- return (
-
-
- )
-}
-
-// Return true if the amount is greater than or equal to the minimum deposit amount (1 USDC)
-function checkAmount(
- queryClient: QueryClient,
- amount: string,
- baseToken?: Token,
-) {
- if (!baseToken) return false
- const usdPrice = queryClient.getQueryData(
- createPriceQueryKey("binance", baseToken.address),
- )
- if (!usdPrice) return false
- const amountInUsd = Number(amount) * usdPrice
- return amountInUsd >= MIN_DEPOSIT_AMOUNT
-}
-
-// Returns true if the amount is less than or equal to the balance
-// Returns false if the amount is greater than the balance or if the amount is invalid
-function checkBalance({
- amount,
- mint,
- balance,
-}: z.infer & { balance?: bigint }) {
- if (!balance) {
- return false
- }
- try {
- const token = Token.findByAddress(mint as `0x${string}`)
- const parsedAmount = safeParseUnits(amount, token.decimals)
- if (parsedAmount instanceof Error) {
- return false
- }
- return parsedAmount <= balance
- } catch (error) {
- return false
- }
-}
-
-// Returns true iff the amount is equal to the balance
-function isMaxBalance({
- amount,
- mint,
- balance,
-}: z.infer & { balance?: bigint }) {
- if (!balance) {
- return false
- }
- try {
- const token = Token.findByAddress(mint as `0x${string}`)
- const formattedAmount = formatUnits(balance, token.decimals)
- return amount === formattedAmount
- } catch (error) {
- return false
- }
-}
diff --git a/components/dialogs/transfer/transfer-form.tsx b/components/dialogs/transfer/transfer-form.tsx
new file mode 100644
index 00000000..b4cce394
--- /dev/null
+++ b/components/dialogs/transfer/transfer-form.tsx
@@ -0,0 +1,52 @@
+import * as React from "react"
+
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Token } from "@renegade-fi/react"
+import { useForm } from "react-hook-form"
+import { z } from "zod"
+
+import { DefaultForm } from "@/components/dialogs/transfer/default-form"
+import {
+ ExternalTransferDirection,
+ formSchema,
+} from "@/components/dialogs/transfer/helpers"
+import { WETHForm } from "@/components/dialogs/transfer/weth-form"
+
+export function TransferForm({
+ className,
+ direction,
+ initialMint,
+ onSuccess,
+}: React.ComponentProps<"form"> & {
+ direction: ExternalTransferDirection
+ initialMint?: string
+ onSuccess: () => void
+}) {
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ amount: "",
+ mint: initialMint ?? "",
+ },
+ })
+ if (
+ direction === ExternalTransferDirection.Deposit &&
+ form.watch("mint") === Token.findByTicker("WETH").address
+ ) {
+ return (
+
+ )
+ }
+ return (
+
+ )
+}
diff --git a/components/dialogs/transfer/weth-form.tsx b/components/dialogs/transfer/weth-form.tsx
new file mode 100644
index 00000000..35c693af
--- /dev/null
+++ b/components/dialogs/transfer/weth-form.tsx
@@ -0,0 +1,507 @@
+import * as React from "react"
+
+import { usePathname, useRouter } from "next/navigation"
+
+import { Token, UpdateType } from "@renegade-fi/react"
+import { useQueryClient } from "@tanstack/react-query"
+import { UseFormReturn, useWatch } from "react-hook-form"
+import { toast } from "sonner"
+import { formatEther, formatUnits, parseEther } from "viem"
+import { useAccount, useBalance } from "wagmi"
+import { z } from "zod"
+
+import { TokenSelect } from "@/components/dialogs/token-select"
+import {
+ ExternalTransferDirection,
+ checkAmount,
+ checkBalance,
+ formSchema,
+} from "@/components/dialogs/transfer/helpers"
+import { MaxBalancesWarning } from "@/components/dialogs/transfer/max-balances-warning"
+import { useIsMaxBalances } from "@/components/dialogs/transfer/use-is-max-balances"
+import { NumberInput } from "@/components/number-input"
+import { Button } from "@/components/ui/button"
+import { DialogClose, DialogFooter } from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import {
+ ResponsiveTooltip,
+ ResponsiveTooltipContent,
+ ResponsiveTooltipTrigger,
+} from "@/components/ui/responsive-tooltip"
+
+import { useApprove } from "@/hooks/use-approve"
+import { useBasePerQuotePrice } from "@/hooks/use-base-per-usd-price"
+import { useCheckChain } from "@/hooks/use-check-chain"
+import { useDeposit } from "@/hooks/use-deposit"
+import { useMaintenanceMode } from "@/hooks/use-maintenance-mode"
+import { useMediaQuery } from "@/hooks/use-media-query"
+import { usePriceQuery } from "@/hooks/use-price-query"
+import { useRefreshOnBlock } from "@/hooks/use-refresh-on-block"
+import { useWrapEth } from "@/hooks/use-wrap-eth"
+import { MIN_DEPOSIT_AMOUNT, Side } from "@/lib/constants/protocol"
+import { constructStartToastMessage } from "@/lib/constants/task"
+import { formatNumber } from "@/lib/format"
+import { useReadErc20BalanceOf } from "@/lib/generated"
+import { cn } from "@/lib/utils"
+import { useSide } from "@/providers/side-provider"
+
+import { WrapEthWarning } from "./wrap-eth-warning"
+
+// Assume direction is deposit and mint is WETH
+export function WETHForm({
+ className,
+ form,
+ onSuccess,
+}: React.ComponentProps<"form"> & {
+ onSuccess: () => void
+ form: UseFormReturn>
+}) {
+ const isDesktop = useMediaQuery("(min-width: 1024px)")
+
+ const mint = useWatch({
+ control: form.control,
+ name: "mint",
+ })
+ const baseToken = mint
+ ? Token.findByAddress(mint as `0x${string}`)
+ : undefined
+ const { address } = useAccount()
+ const isMaxBalances = useIsMaxBalances(mint)
+
+ const { data: wethBalance, queryKey: wethBalanceQueryKey } =
+ useReadErc20BalanceOf({
+ address: baseToken?.address,
+ args: [address ?? "0x"],
+ })
+
+ useRefreshOnBlock({ queryKey: wethBalanceQueryKey })
+
+ const formattedWethBalance = baseToken
+ ? formatUnits(wethBalance ?? BigInt(0), baseToken.decimals)
+ : ""
+ const wethBalanceLabel = baseToken
+ ? formatNumber(wethBalance ?? BigInt(0), baseToken.decimals, true)
+ : ""
+
+ // ETH-specific logic
+ const { data: ethBalance, queryKey: ethBalanceQueryKey } = useBalance({
+ address,
+ })
+ const formattedEthBalance = formatUnits(
+ ethBalance?.value ?? BigInt(0),
+ ethBalance?.decimals ?? 18,
+ )
+ const ethBalanceLabel = formatNumber(ethBalance?.value ?? BigInt(0), 18, true)
+ const basePerQuotePrice = useBasePerQuotePrice(baseToken?.address ?? "0x")
+
+ // Calculate the minimum ETH to keep unwrapped for gas fees
+ const minEthToKeepUnwrapped = (basePerQuotePrice ?? BigInt(4e15)) * BigInt(5)
+
+ const combinedBalance =
+ (wethBalance ?? BigInt(0)) + (ethBalance?.value ?? BigInt(0))
+ const maxAmountToWrap = combinedBalance - minEthToKeepUnwrapped
+
+ const amount = useWatch({
+ control: form.control,
+ name: "amount",
+ })
+
+ const remainingEthBalance =
+ parseEther(amount) > (wethBalance ?? BigInt(0))
+ ? (ethBalance?.value ?? BigInt(0)) +
+ (wethBalance ?? BigInt(0)) -
+ parseEther(amount)
+ : ethBalance?.value ?? BigInt(0)
+
+ const hideMaxButton =
+ !mint ||
+ formattedWethBalance === "0" ||
+ amount.toString() === formattedWethBalance
+
+ const { handleDeposit, status: depositStatus } = useDeposit({
+ amount,
+ mint,
+ })
+
+ const {
+ needsApproval,
+ handleApprove,
+ status: approveStatus,
+ } = useApprove({
+ amount: amount.toString(),
+ mint,
+ })
+
+ // If the amount is greater than the WETH balance, we need to wrap ETH
+ const needsWrapEth = parseEther(amount) > (wethBalance ?? BigInt(0))
+
+ const { checkChain } = useCheckChain()
+
+ const router = useRouter()
+ const pathname = usePathname()
+ const isTradePage = pathname.includes("/trade")
+ const queryClient = useQueryClient()
+ // Ensure price is loaded
+ usePriceQuery(baseToken?.address || "0x")
+ const { setSide } = useSide()
+
+ const [totalSteps, setTotalSteps] = React.useState(0)
+ const [currentStep, setCurrentStep] = React.useState(0)
+
+ async function onSubmit(values: z.infer) {
+ const isAmountSufficient = checkAmount(
+ queryClient,
+ values.amount,
+ baseToken,
+ )
+
+ if (!isAmountSufficient) {
+ form.setError("amount", {
+ message: `Amount must be greater than or equal to ${MIN_DEPOSIT_AMOUNT} USDC`,
+ })
+ return
+ }
+ await checkChain()
+ const isBalanceSufficient = checkBalance({
+ amount: values.amount,
+ mint: values.mint,
+ balance: needsWrapEth ? combinedBalance : wethBalance,
+ })
+ if (!isBalanceSufficient) {
+ form.setError("amount", {
+ message: "Insufficient Arbitrum balance",
+ })
+ return
+ }
+
+ // Calculate total steps
+ let steps = 1 // Deposit is always required
+ if (needsWrapEth) steps++
+ if (needsApproval) steps++
+ setTotalSteps(steps)
+ setCurrentStep(0)
+
+ if (needsWrapEth) {
+ wrapEth(values.amount)
+ } else if (needsApproval) {
+ handleApprove({
+ onSuccess: () => {
+ setCurrentStep((prev) => prev + 1)
+ handleDeposit({
+ onSuccess: handleDepositSuccess,
+ })
+ },
+ })
+ } else {
+ handleDeposit({
+ onSuccess: handleDepositSuccess,
+ })
+ }
+ }
+
+ const handleDepositSuccess = (data: any) => {
+ setCurrentStep((prev) => prev + 1)
+ form.reset()
+ onSuccess?.()
+ const message = constructStartToastMessage(UpdateType.Deposit)
+ toast.loading(message, {
+ id: data.taskId,
+ })
+ setSide(baseToken?.ticker === "USDC" ? Side.BUY : Side.SELL)
+ if (isTradePage && baseToken?.ticker !== "USDC") {
+ router.push(`/trade/${baseToken?.ticker}`)
+ }
+ }
+
+ const { wrapEth, status: wrapStatus } = useWrapEth({
+ onSuccess: () => {
+ setCurrentStep((prev) => prev + 1)
+ queryClient.invalidateQueries({ queryKey: ethBalanceQueryKey })
+ queryClient.invalidateQueries({ queryKey: wethBalanceQueryKey })
+ if (needsApproval) {
+ handleApprove({
+ onSuccess: () => {
+ setCurrentStep((prev) => prev + 1)
+ handleDeposit({
+ onSuccess: handleDepositSuccess,
+ })
+ },
+ })
+ } else {
+ handleDeposit({
+ onSuccess: handleDepositSuccess,
+ })
+ }
+ },
+ onError: (error) => {
+ console.error("Error wrapping ETH:", error)
+ // Reset steps on error
+ setTotalSteps(0)
+ setCurrentStep(0)
+ },
+ })
+
+ const { data: maintenanceMode } = useMaintenanceMode()
+
+ let buttonText = ""
+ let buttonTextInParentheses = ""
+ if (needsWrapEth) {
+ if (wrapStatus === "pending") {
+ buttonText = "Confirm in wallet"
+ buttonTextInParentheses = `(${currentStep + 1} of ${totalSteps})`
+ } else if (needsApproval) {
+ buttonText = "Wrap, Approve & Deposit"
+ } else {
+ buttonText = "Wrap & Deposit"
+ }
+ } else if (needsApproval) {
+ if (approveStatus === "pending") {
+ buttonText = "Confirm in wallet"
+ buttonTextInParentheses = `(${currentStep + 1} of ${totalSteps})`
+ } else {
+ buttonText = "Approve & Deposit"
+ }
+ } else {
+ if (depositStatus === "pending") {
+ buttonText = "Confirm in wallet"
+ buttonTextInParentheses = `(${currentStep + 1} of ${totalSteps})`
+ } else {
+ buttonText = "Deposit"
+ }
+ }
+
+ return (
+
+
+ )
+}
diff --git a/components/dialogs/transfer/wrap-eth-warning.tsx b/components/dialogs/transfer/wrap-eth-warning.tsx
new file mode 100644
index 00000000..b2020ad3
--- /dev/null
+++ b/components/dialogs/transfer/wrap-eth-warning.tsx
@@ -0,0 +1,73 @@
+import { Token } from "@renegade-fi/react"
+import { AlertTriangle } from "lucide-react"
+import { formatUnits } from "viem/utils"
+
+import {
+ ResponsiveTooltip,
+ ResponsiveTooltipContent,
+ ResponsiveTooltipTrigger,
+} from "@/components/ui/responsive-tooltip"
+
+import { useUSDPrice } from "@/hooks/use-usd-price"
+import { formatCurrencyFromString, formatNumber } from "@/lib/format"
+import { cn } from "@/lib/utils"
+
+export function WrapEthWarning({
+ remainingEthBalance,
+ minEthToKeepUnwrapped,
+}: {
+ remainingEthBalance: bigint
+ minEthToKeepUnwrapped: bigint
+}) {
+ const formattedRemainingEthBalance = formatNumber(
+ remainingEthBalance,
+ 18,
+ true,
+ )
+
+ const weth = Token.findByTicker("WETH")
+ const usdValue = useUSDPrice(weth, remainingEthBalance)
+ const formattedUsdValue = formatUnits(usdValue, weth.decimals)
+ const formattedUsdValueLabel = formatCurrencyFromString(formattedUsdValue)
+
+ let className: string
+ let mainText: string
+ let tooltipText: string
+
+ if (remainingEthBalance < BigInt(0)) {
+ className = "bg-[#2A0000] text-red-400"
+ mainText = "Insufficient ETH balance"
+ tooltipText = `You don't have enough ETH to cover the transaction and gas fees.`
+ } else if (remainingEthBalance === BigInt(0)) {
+ className = "bg-[#2A0000] text-red-500"
+ mainText = "Transaction will result in 0 ETH balance"
+ tooltipText = "You will have no ETH left to cover gas fees."
+ } else if (remainingEthBalance < minEthToKeepUnwrapped) {
+ className = "bg-[#2A0000] text-red-400"
+ mainText = "Transaction will result in very low ETH balance"
+ tooltipText = `You will have ${formattedRemainingEthBalance} ETH (${formattedUsdValueLabel}) after wrapping`
+ } else {
+ className = "bg-[#2A1700] text-orange-400"
+ mainText = `Transaction will wrap ETH to WETH`
+ tooltipText = `You will have ${formattedRemainingEthBalance} ETH (${formattedUsdValueLabel}) after wrapping`
+ }
+
+ return (
+
+
+
+
+
+ {tooltipText}
+
+
+ )
+}
diff --git a/hooks/use-base-per-usd-price.ts b/hooks/use-base-per-usd-price.ts
new file mode 100644
index 00000000..660b8e10
--- /dev/null
+++ b/hooks/use-base-per-usd-price.ts
@@ -0,0 +1,18 @@
+import { Token } from "@renegade-fi/react"
+import { parseUnits } from "viem/utils"
+
+import { usePriceQuery } from "@/hooks/use-price-query"
+
+// Returns the price of the base token in quote token terms, denominated in the base token's decimals
+export function useBasePerQuotePrice(baseMint: `0x${string}`) {
+ const token = Token.findByAddress(baseMint)
+ const { data: usdPerBasePrice } = usePriceQuery(baseMint)
+ if (!usdPerBasePrice) {
+ return null
+ }
+
+ // Use Math.ceil to prevent division by zero
+ const basePerQuotePrice =
+ parseUnits("1", token.decimals) / BigInt(Math.ceil(usdPerBasePrice))
+ return basePerQuotePrice
+}
diff --git a/hooks/use-wrap-eth.ts b/hooks/use-wrap-eth.ts
new file mode 100644
index 00000000..78558b3c
--- /dev/null
+++ b/hooks/use-wrap-eth.ts
@@ -0,0 +1,67 @@
+import React from "react"
+
+import { Token } from "@renegade-fi/react"
+import { toast } from "sonner"
+import { parseEther } from "viem"
+import { BaseError, useWaitForTransactionReceipt } from "wagmi"
+
+import { useWriteWethDeposit } from "@/lib/generated"
+
+export function useWrapEth({
+ onSuccess,
+ onError,
+}: {
+ onSuccess: () => void
+ onError: (error: Error) => void
+}) {
+ const weth = Token.findByTicker("WETH")
+ const [isConfirmed, setIsConfirmed] = React.useState(false)
+
+ const {
+ writeContract,
+ data: hash,
+ status: writeStatus,
+ } = useWriteWethDeposit({
+ mutation: {
+ onError: (error) => {
+ console.error("Error wrapping ETH", error)
+ toast.error(
+ `Error wrapping ETH: ${(error as BaseError).shortMessage || error.message}`,
+ )
+ onError?.(error)
+ },
+ },
+ })
+
+ const { status: txStatus } = useWaitForTransactionReceipt({
+ hash,
+ })
+
+ const wrapEth = React.useCallback(
+ (amount: string) => {
+ const parsedAmount = parseEther(amount)
+ console.log("wrapping", parsedAmount)
+ writeContract({
+ address: weth?.address,
+ value: parsedAmount,
+ })
+ },
+ [weth?.address, writeContract],
+ )
+
+ React.useEffect(() => {
+ if (txStatus === "success" && !isConfirmed) {
+ setIsConfirmed(true)
+ onSuccess?.()
+ }
+ }, [txStatus, isConfirmed, onSuccess])
+
+ const status = hash ? txStatus : writeStatus
+
+ return {
+ wrapEth,
+ status,
+ hash,
+ isConfirmed,
+ }
+}
diff --git a/lib/generated.ts b/lib/generated.ts
index 262469c4..4feeec17 100644
--- a/lib/generated.ts
+++ b/lib/generated.ts
@@ -43,6 +43,20 @@ export const erc20Abi = [
},
] as const
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// weth
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+export const wethAbi = [
+ {
+ type: "function",
+ inputs: [],
+ name: "deposit",
+ outputs: [],
+ stateMutability: "payable",
+ },
+] as const
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Action
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -96,6 +110,34 @@ export const simulateErc20Approve = /*#__PURE__*/ createSimulateContract({
functionName: "approve",
})
+/**
+ * Wraps __{@link writeContract}__ with `abi` set to __{@link wethAbi}__
+ */
+export const writeWeth = /*#__PURE__*/ createWriteContract({ abi: wethAbi })
+
+/**
+ * Wraps __{@link writeContract}__ with `abi` set to __{@link wethAbi}__ and `functionName` set to `"deposit"`
+ */
+export const writeWethDeposit = /*#__PURE__*/ createWriteContract({
+ abi: wethAbi,
+ functionName: "deposit",
+})
+
+/**
+ * Wraps __{@link simulateContract}__ with `abi` set to __{@link wethAbi}__
+ */
+export const simulateWeth = /*#__PURE__*/ createSimulateContract({
+ abi: wethAbi,
+})
+
+/**
+ * Wraps __{@link simulateContract}__ with `abi` set to __{@link wethAbi}__ and `functionName` set to `"deposit"`
+ */
+export const simulateWethDeposit = /*#__PURE__*/ createSimulateContract({
+ abi: wethAbi,
+ functionName: "deposit",
+})
+
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// React
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -152,3 +194,33 @@ export const useSimulateErc20Approve = /*#__PURE__*/ createUseSimulateContract({
abi: erc20Abi,
functionName: "approve",
})
+
+/**
+ * Wraps __{@link useWriteContract}__ with `abi` set to __{@link wethAbi}__
+ */
+export const useWriteWeth = /*#__PURE__*/ createUseWriteContract({
+ abi: wethAbi,
+})
+
+/**
+ * Wraps __{@link useWriteContract}__ with `abi` set to __{@link wethAbi}__ and `functionName` set to `"deposit"`
+ */
+export const useWriteWethDeposit = /*#__PURE__*/ createUseWriteContract({
+ abi: wethAbi,
+ functionName: "deposit",
+})
+
+/**
+ * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link wethAbi}__
+ */
+export const useSimulateWeth = /*#__PURE__*/ createUseSimulateContract({
+ abi: wethAbi,
+})
+
+/**
+ * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link wethAbi}__ and `functionName` set to `"deposit"`
+ */
+export const useSimulateWethDeposit = /*#__PURE__*/ createUseSimulateContract({
+ abi: wethAbi,
+ functionName: "deposit",
+})
diff --git a/wagmi.config.ts b/wagmi.config.ts
index 6ad5c952..20db1dcf 100644
--- a/wagmi.config.ts
+++ b/wagmi.config.ts
@@ -8,6 +8,8 @@ const abi = parseAbi([
"function allowance(address owner, address spender) view returns (uint256)",
])
+const wethAbi = parseAbi(["function deposit() payable"])
+
export default defineConfig({
out: "lib/generated.ts",
contracts: [
@@ -15,6 +17,10 @@ export default defineConfig({
name: "erc20",
abi,
},
+ {
+ name: "weth",
+ abi: wethAbi,
+ },
],
plugins: [actions(), react()],
})