diff --git a/apps/web/src/components/BridgeModal/BridgeForm.schema.ts b/apps/web/src/components/BridgeModal/BridgeForm.schema.ts
new file mode 100644
index 000000000..4381ef6cf
--- /dev/null
+++ b/apps/web/src/components/BridgeModal/BridgeForm.schema.ts
@@ -0,0 +1,20 @@
+import * as yup from 'yup'
+
+export interface BridgeFormValues {
+ amount?: number
+}
+
+const bridgeFormSchema = (userL1Balance: number) =>
+ yup.object({
+ amount: yup
+ .number()
+ .required()
+ .max(userL1Balance, 'Must bridge less than L1 balance.')
+ .test(
+ 'is-greater-than-0',
+ 'Must bridge more than 0 ETH',
+ (value) => !!value && value > 0
+ ),
+ })
+
+export default bridgeFormSchema
diff --git a/apps/web/src/components/BridgeModal/BridgeForm.styles.css.ts b/apps/web/src/components/BridgeModal/BridgeForm.styles.css.ts
new file mode 100644
index 000000000..cb9a7ed88
--- /dev/null
+++ b/apps/web/src/components/BridgeModal/BridgeForm.styles.css.ts
@@ -0,0 +1,11 @@
+import { style } from '@vanilla-extract/css'
+import { vars } from '@zoralabs/zord'
+
+export const chainPopUpButton = style({
+ backgroundColor: 'white',
+ selectors: {
+ '&:hover': {
+ backgroundColor: vars.color.background2,
+ },
+ },
+})
diff --git a/apps/web/src/components/BridgeModal/BridgeForm.tsx b/apps/web/src/components/BridgeModal/BridgeForm.tsx
new file mode 100644
index 000000000..575595e09
--- /dev/null
+++ b/apps/web/src/components/BridgeModal/BridgeForm.tsx
@@ -0,0 +1,239 @@
+import { useConnectModal } from '@rainbow-me/rainbowkit'
+import { sendTransaction } from '@wagmi/core'
+import { Box, Button, Flex, Heading, Text } from '@zoralabs/zord'
+import { parseEther } from 'ethers/lib/utils.js'
+import { Formik } from 'formik'
+import Image from 'next/image'
+import Link from 'next/link'
+import { useState } from 'react'
+import { useAccount, useBalance, useNetwork, useSwitchNetwork } from 'wagmi'
+
+import Input from 'src/components/Input/Input'
+import { L2ChainType, PUBLIC_L1_BRIDGE_ADDRESS } from 'src/constants/addresses'
+import { PUBLIC_DEFAULT_CHAINS } from 'src/constants/defaultChains'
+import { useBridgeModal } from 'src/hooks/useBridgeModal'
+import { useIsContract } from 'src/hooks/useIsContract'
+import { useChainStore } from 'src/stores/useChainStore'
+import { formatCryptoVal } from 'src/utils/numbers'
+
+import { Icon } from '../Icon'
+import bridgeFormSchema, { BridgeFormValues } from './BridgeForm.schema'
+import { NetworkSelector } from './NetworkSelector'
+
+export const BridgeForm = () => {
+ const { address } = useAccount()
+ const { chain: userChain } = useNetwork()
+ const { switchNetwork } = useSwitchNetwork()
+ const { closeBridgeModal } = useBridgeModal()
+ const { openConnectModal } = useConnectModal()
+ const { data: isContractWallet } = useIsContract({ address })
+ const [loading, setLoading] = useState(false)
+
+ const { chain: appChain } = useChainStore()
+
+ const l1Chain = PUBLIC_DEFAULT_CHAINS[0]
+ const [l2Chain, setL2Chain] = useState(
+ appChain.id !== l1Chain.id ? appChain : PUBLIC_DEFAULT_CHAINS[1]
+ )
+
+ const isWalletOnL1 = userChain?.id === l1Chain.id
+
+ const { data: userL1Balance } = useBalance({
+ address,
+ chainId: l1Chain.id,
+ })
+
+ const { data: userL2Balance } = useBalance({
+ address,
+ chainId: l2Chain.id,
+ })
+
+ const initialValues: BridgeFormValues = {
+ amount: 0,
+ }
+
+ const handleSubmit = async (values: BridgeFormValues) => {
+ const bridge = PUBLIC_L1_BRIDGE_ADDRESS[l2Chain.id as L2ChainType]
+
+ if (!values.amount || !bridge) return
+
+ setLoading(true)
+ try {
+ const { wait } = await sendTransaction({
+ request: {
+ to: PUBLIC_L1_BRIDGE_ADDRESS[l2Chain.id as L2ChainType],
+ value: parseEther(values.amount.toString()),
+ },
+ mode: 'recklesslyUnprepared',
+ })
+ await wait()
+ } catch (err) {
+ console.log('err', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const formattedL1Balance = userL1Balance ? parseFloat(userL1Balance.formatted) : 0
+ const formattedL2Balance = userL2Balance ? parseFloat(userL2Balance.formatted) : 0
+
+ const getButtonText = (isAmountInvalid: boolean) => {
+ if (isContractWallet) return 'Contract wallets are not supported'
+ if (loading) return 'Bridging...'
+ if (isAmountInvalid) return 'Insufficent ETH balance'
+ return 'Bridge'
+ }
+
+ return (
+
+
+
+
+
+ Bridge
+
+
+
+ {({ errors, touched, isValid, submitForm }) => {
+ const isAmountInvalid = !!errors.amount && touched.amount
+
+ return (
+
+
+
+
+ From
+
+
+
+
+ {l1Chain.name}
+
+ }
+ secondaryLabel={'ETH'}
+ autoComplete={'off'}
+ type={'number'}
+ placeholder={0}
+ min={0}
+ max={userL1Balance?.formatted}
+ step={'any'}
+ />
+
+ Balance: {formatCryptoVal(formattedL1Balance)} ETH
+
+
+
+
+
+
+ To
+
+
+
+ }
+ secondaryLabel={'ETH'}
+ autoComplete={'off'}
+ type={'number'}
+ placeholder={0}
+ min={0}
+ max={userL2Balance?.formatted}
+ step={'any'}
+ />
+
+ Balance: {formatCryptoVal(formattedL2Balance)} ETH
+
+
+
+ {!address ? (
+
+ ) : isWalletOnL1 ? (
+
+ ) : (
+
+ )}
+
+
+ By proceeding, you agree to Nouns Builder's{' '}
+
+ terms
+
+ . THIS BRIDGE IS DEPOSIT ONLY. YOU MUST USE ANOTHER BRIDGE TO WITHDRAW.
+ Learn more about{' '}
+
+ bridging
+
+ .
+
+
+ )
+ }}
+
+
+ )
+}
diff --git a/apps/web/src/components/BridgeModal/BridgeModal.tsx b/apps/web/src/components/BridgeModal/BridgeModal.tsx
new file mode 100644
index 000000000..034a8fdc0
--- /dev/null
+++ b/apps/web/src/components/BridgeModal/BridgeModal.tsx
@@ -0,0 +1,21 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+import AnimatedModal from '../Modal/AnimatedModal'
+import { BridgeForm } from './BridgeForm'
+
+export const BridgeModal = () => {
+ const {
+ query: { bridge },
+ } = useRouter()
+
+ useEffect(() => {
+ document.body.style.overflow = !!bridge ? 'hidden' : 'unset'
+ }, [bridge])
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/web/src/components/BridgeModal/NetworkSelector.tsx b/apps/web/src/components/BridgeModal/NetworkSelector.tsx
new file mode 100644
index 000000000..f9de46197
--- /dev/null
+++ b/apps/web/src/components/BridgeModal/NetworkSelector.tsx
@@ -0,0 +1,112 @@
+import { Box, Flex, PopUp, Stack, Text } from '@zoralabs/zord'
+import Image from 'next/image'
+import React from 'react'
+
+import { PUBLIC_DEFAULT_CHAINS } from 'src/constants/defaultChains'
+import { CHAIN_ID, Chain } from 'src/typings'
+
+import { Icon } from '../Icon'
+import { chainPopUpButton } from './BridgeForm.styles.css'
+
+export interface NetworkSelectorProps {
+ selectedChain: Chain
+ setSelectedChain: (value: Chain) => void
+}
+
+export const NetworkSelector: React.FC = ({
+ selectedChain,
+ setSelectedChain,
+}) => {
+ const [isOpenChainMenu, setIsOpenChainMenu] = React.useState(false)
+
+ const onChainChange = (chainId: number) => {
+ const selected = PUBLIC_DEFAULT_CHAINS.find((x) => x.id === chainId)
+ if (selected) setSelectedChain(selected)
+ }
+
+ const isSelectedChain = (chainId: CHAIN_ID) => selectedChain.id === chainId
+
+ return (
+ {
+ setIsOpenChainMenu((bool) => !bool)
+ }}
+ >
+ {
+ setIsOpenChainMenu(open)
+ }}
+ trigger={
+
+
+
+
+
+
+ {selectedChain.name}
+
+
+
+
+
+
+ }
+ >
+
+ {PUBLIC_DEFAULT_CHAINS.slice(1).map((chain, i, chains) => (
+ onChainChange(chain.id)}
+ cursor={isSelectedChain(chain.id) ? undefined : 'pointer'}
+ height={'x10'}
+ px="x4"
+ mb={i !== chains.length - 1 ? 'x2' : undefined}
+ align={'center'}
+ justify={'space-between'}
+ >
+
+
+
+
+ {chain.name}
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/apps/web/src/components/ContractButton.tsx b/apps/web/src/components/ContractButton.tsx
index 6f3c75304..a77b78c58 100644
--- a/apps/web/src/components/ContractButton.tsx
+++ b/apps/web/src/components/ContractButton.tsx
@@ -1,7 +1,8 @@
import { useConnectModal } from '@rainbow-me/rainbowkit'
import { Button, ButtonProps } from '@zoralabs/zord'
-import { useAccount, useNetwork, useSwitchNetwork } from 'wagmi'
+import { useAccount, useBalance, useNetwork, useSwitchNetwork } from 'wagmi'
+import { useBridgeModal } from 'src/hooks/useBridgeModal'
import { useChainStore } from 'src/stores/useChainStore'
interface ContractButtonProps extends ButtonProps {
@@ -16,23 +17,26 @@ export const ContractButton = ({
const { address: userAddress } = useAccount()
const { chain: userChain } = useNetwork()
const appChain = useChainStore((x) => x.chain)
+ const { canUserBridge, openBridgeModal } = useBridgeModal()
+ const { data: userBalance } = useBalance({
+ address: userAddress,
+ chainId: appChain.id,
+ })
const { openConnectModal } = useConnectModal()
const { switchNetwork } = useSwitchNetwork()
const handleSwitchNetwork = () => switchNetwork?.(appChain.id)
+ const handleClickWithValidation = () => {
+ if (!userAddress) return openConnectModal?.()
+ if (canUserBridge && userBalance?.value.eq(0)) return openBridgeModal()
+ if (userChain?.id !== appChain.id) return handleSwitchNetwork()
+ handleClick()
+ }
+
return (
-