("")
+
+ const handleClick = (event: SyntheticEvent) => {
+ event.preventDefault()
+ if (!value) {
+ return
+ }
+ copy(value)
+ setText("copied!")
+ setTimeout(() => {
+ setText("")
+ }, 3 * 1e3)
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+export default CopyButton
diff --git a/src/pages/nftBridge/Header/PageTitle.tsx b/src/pages/nftBridge/Header/PageTitle.tsx
new file mode 100644
index 000000000..1ead52a86
--- /dev/null
+++ b/src/pages/nftBridge/Header/PageTitle.tsx
@@ -0,0 +1,15 @@
+import { Typography } from "@mui/material"
+import { Stack } from "@mui/system"
+
+const PageTitle = () => {
+ return (
+
+ NFT Bridge
+
+ Transfer your NFTs across different networks
+
+
+ )
+}
+
+export default PageTitle
diff --git a/src/pages/nftBridge/Header/TransactionHistory.tsx b/src/pages/nftBridge/Header/TransactionHistory.tsx
new file mode 100644
index 000000000..d8ca0c539
--- /dev/null
+++ b/src/pages/nftBridge/Header/TransactionHistory.tsx
@@ -0,0 +1,82 @@
+import { makeStyles } from "tss-react/mui"
+
+import { CircularProgress, Stack, Typography } from "@mui/material"
+
+import { BRIDGE_PAGE_SIZE } from "@/constants"
+import { useApp } from "@/contexts/AppContextProvider"
+import useNFTTxStore from "@/stores/nftTxStore"
+
+import TxTable from "../components/TxTable"
+
+const useStyles = makeStyles()(theme => {
+ return {
+ tableWrapper: {
+ boxShadow: "unset",
+ border: `1px solid ${theme.palette.border.main}`,
+ borderRadius: "1rem",
+ [theme.breakpoints.down("sm")]: {
+ border: "unset",
+ borderRadius: "unset",
+ margin: "0 -2rem",
+ width: "calc(100% + 4rem)",
+ },
+ },
+ tableTitle: {
+ marginTop: "2.8rem",
+ marginBottom: "3rem",
+ [theme.breakpoints.down("sm")]: {
+ marginTop: "1.6rem",
+ marginBottom: "1.6rem",
+ },
+ },
+ tableHeader: {
+ backgroundColor: theme.palette.scaleBackground.primary,
+ },
+ }
+})
+
+const TransactionsList = (props: any) => {
+ const { classes, cx } = useStyles()
+
+ const {
+ txHistory: { refreshPageTransactions },
+ } = useApp()
+
+ const { page, total, loading, frontTransactions } = useNFTTxStore()
+
+ // TODO: waiting for api
+ if (!frontTransactions?.length) {
+ return (
+
+ Your transactions will appear here...
+
+ )
+ }
+
+ const handleChangePage = currentPage => {
+ refreshPageTransactions(currentPage)
+ }
+
+ return (
+ <>
+
+
+
+ Recent Bridge Transactions
+
+ {loading && }
+
+
+
+ >
+ )
+}
+
+export default TransactionsList
diff --git a/src/pages/nftBridge/Header/index.tsx b/src/pages/nftBridge/Header/index.tsx
new file mode 100644
index 000000000..b1c2e6dc7
--- /dev/null
+++ b/src/pages/nftBridge/Header/index.tsx
@@ -0,0 +1,143 @@
+import { makeStyles } from "tss-react/mui"
+
+import CloseIcon from "@mui/icons-material/Close"
+import { Box, Card, Container, Divider, Typography } from "@mui/material"
+import { Stack } from "@mui/system"
+
+import { ReactComponent as ExitIcon } from "@/assets/svgs/exit.svg"
+import Link from "@/components/Link"
+import TextIconButton from "@/components/TextIconButton"
+import WalletIndicator from "@/components/WalletIndicator"
+// import { useApp } from "@/contexts/AppContextProvider"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import useBridgeStore from "@/stores/bridgeStore"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+import { truncateAddress } from "@/utils"
+
+import CopyButton from "./CopyButton"
+import PageTitle from "./PageTitle"
+import TransactionHistory from "./TransactionHistory"
+
+const useStyles = makeStyles()(theme => ({
+ container: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ backgroundColor: theme.palette.background.default,
+ marginTop: "4rem",
+ marginBottom: "4rem",
+ },
+
+ modalContent: {
+ width: "max-content",
+ padding: "2.8rem",
+ borderRadius: "1rem",
+ boxSizing: "border-box",
+ boxShadow: "0px 2px 10px rgba(0, 0, 0, 0.2)",
+ [theme.breakpoints.down("sm")]: {
+ width: "calc(100vw - 3.2rem)",
+ margin: "0 1.6rem",
+ padding: "2rem",
+ },
+ },
+ title: {
+ [theme.breakpoints.down("sm")]: {
+ fontSize: "1.8rem",
+ fontWeight: 600,
+ },
+ },
+
+ transactionsList: {
+ margin: " 3rem 0",
+ [theme.breakpoints.down("sm")]: {
+ margin: "2.4rem 0",
+ },
+ },
+ changeButton: {
+ position: "absolute",
+ top: "1rem",
+ right: "1rem",
+ borderRadius: "1.5rem",
+ boxShadow: "none",
+ },
+ disconnectButton: {
+ position: "absolute",
+ bottom: "1rem",
+ right: "1rem",
+ fontSize: "1.2rem",
+ marginBottom: 0,
+ borderRadius: "1.5rem",
+ boxShadow: "none",
+ },
+ address: {
+ cursor: "default",
+ marginRight: "7.2rem",
+ [theme.breakpoints.down("sm")]: {
+ marginBottom: "2.4rem",
+ },
+ },
+ copyButton: {
+ marginRight: "2.4rem",
+ },
+}))
+
+// TODO: after token minted on L2, need to call setTokenURI for the token (blocked by history)
+
+const Header = () => {
+ const { classes } = useStyles()
+ const { walletCurrentAddress, disconnectWallet } = useWeb3Context()
+
+ const { clearViewingList, clearSelectedList } = useNFTBridgeStore()
+
+ // const {
+ // txHistory: { refreshPageTransactions },
+ // } = useApp()
+ const { historyVisible, changeHistoryVisible } = useBridgeStore()
+
+ const handleOpen = () => {
+ changeHistoryVisible(true)
+ // refreshPageTransactions(1)
+ }
+
+ const handleClose = () => {
+ changeHistoryVisible(false)
+ }
+
+ const handleDisconnect = () => {
+ handleClose()
+ clearViewingList()
+ clearSelectedList()
+ disconnectWallet()
+ }
+
+ return (
+
+
+
+
+
+
+ Connected Wallet
+
+
+
+
+
+ {truncateAddress(walletCurrentAddress as string)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Header
diff --git a/src/pages/nftBridge/NFTPanel/Transfer/ApproveLoadingModal.tsx b/src/pages/nftBridge/NFTPanel/Transfer/ApproveLoadingModal.tsx
new file mode 100644
index 000000000..96d0d4ce9
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Transfer/ApproveLoadingModal.tsx
@@ -0,0 +1,25 @@
+import { Box, Typography } from "@mui/material"
+
+import Link from "@/components/Link"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+
+import Modal from "../../components/Modal"
+
+const ApproveLoading = props => {
+ const { open, onClose } = props
+ const { walletName } = useWeb3Context()
+
+ return (
+
+ Not responding? Click here for help
+
+
+ Allow Scroll to bridge any your NFTs in the contract
+
+ Approve on your {walletName} wallet
+
+
+ )
+}
+
+export default ApproveLoading
diff --git a/src/pages/nftBridge/NFTPanel/Transfer/Fee.tsx b/src/pages/nftBridge/NFTPanel/Transfer/Fee.tsx
new file mode 100644
index 000000000..297cc6a83
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Transfer/Fee.tsx
@@ -0,0 +1,31 @@
+import { useMemo } from "react"
+
+import { Stack, Typography } from "@mui/material"
+
+import { ETH_SYMBOL } from "@/constants"
+import useGasFee from "@/hooks/useGasFee"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+import { toTokenDisplay } from "@/utils"
+
+const Fee = () => {
+ const { gasFee } = useGasFee()
+ const { selectedList } = useNFTBridgeStore()
+ const formattedGasFee = useMemo(() => {
+ if (!selectedList.length) {
+ return "-"
+ }
+ return toTokenDisplay(gasFee, undefined, ETH_SYMBOL)
+ }, [gasFee, selectedList])
+ return (
+
+
+ Fees
+
+
+ {formattedGasFee}
+
+
+ )
+}
+
+export default Fee
diff --git a/src/pages/nftBridge/NFTPanel/Transfer/Send.tsx b/src/pages/nftBridge/NFTPanel/Transfer/Send.tsx
new file mode 100644
index 000000000..fb15fbcac
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Transfer/Send.tsx
@@ -0,0 +1,208 @@
+import { useMemo, useState } from "react"
+
+import { Stack } from "@mui/material"
+
+import LoadingButton from "@/components/LoadingButton"
+import { ChainId, TOEKN_TYPE } from "@/constants"
+import { useApp } from "@/contexts/AppContextProvider"
+import { useNFTBridgeContext } from "@/contexts/NFTBridgeProvider"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import { useAsyncMemo } from "@/hooks"
+import useGasFee from "@/hooks/useGasFee"
+import useApprove from "@/hooks/useNFTApprove"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+import useNFTTxStore from "@/stores/nftTxStore"
+
+import ApproveLoadingModal from "./ApproveLoadingModal"
+import SendLoadingModal from "./SendLoadingModal"
+import TransactionResultModal from "./TransactionResultModal"
+
+const Send = () => {
+ const { walletCurrentAddress } = useWeb3Context()
+ const { networksAndSigners } = useApp()
+
+ const { addNFTTransaction, updateNFTTransaction } = useNFTTxStore()
+ const { tokenInstance, gatewayAddress, isLayer1 } = useNFTBridgeContext()
+ const { contract, selectedList, exciseSelected, updatePromptMessage, fromNetwork, toNetwork } = useNFTBridgeStore()
+ const selectedTokenIds = useNFTBridgeStore(state => state.selectedTokenIds())
+
+ const { setApproval, checkApproval } = useApprove()
+ const { gasLimit, gasFee } = useGasFee()
+
+ // button loading
+ const [sendLoading, setSendLoading] = useState(false)
+ const [approveLoading, setApproveLoading] = useState(false)
+
+ // modal loading
+ const [sendModalLoading, setSendModalLoading] = useState(false)
+ const [approveModalLoading, setApproveModalLoading] = useState(false)
+
+ const [txHash, setTxHash] = useState("")
+
+ // TODO: how to call this after approve
+ const needApproval = useAsyncMemo(async () => {
+ if (tokenInstance) {
+ const isApproved = await checkApproval(tokenInstance, gatewayAddress)
+ return !isApproved
+ }
+ return false
+ }, [tokenInstance, contract])
+
+ const sendActive = useMemo(() => {
+ if (!walletCurrentAddress) {
+ return false
+ } else if (!selectedList.length) {
+ return false
+ } else if (contract.type === TOEKN_TYPE[1155]) {
+ const isValid = selectedList.every(item => item.transferAmount && item.transferAmount > 0)
+ return isValid
+ }
+
+ return true
+ }, [walletCurrentAddress, selectedList, contract.type])
+
+ const handleApprove = async () => {
+ try {
+ setApproveLoading(true)
+ setApproveModalLoading(true)
+ await setApproval(tokenInstance, gatewayAddress)
+ } finally {
+ setApproveLoading(false)
+ setApproveModalLoading(false)
+ }
+ }
+
+ const handleSend = async () => {
+ setSendLoading(true)
+ setSendModalLoading(true)
+ try {
+ const tx = isLayer1 ? await deposite() : await withdraw()
+ addNFTTransaction({
+ hash: tx.hash,
+ fromName: fromNetwork.name,
+ toName: toNetwork.name,
+ fromExplore: fromNetwork.explorer,
+ toExplore: toNetwork.explorer,
+ tokenType: contract.type,
+ tokenAddress: isLayer1 ? contract.l1 : contract.l2,
+ amounts: selectedList.map(item => item.transferAmount),
+ tokenIds: selectedTokenIds,
+ isL1: isLayer1,
+ })
+
+ tx.wait()
+ .then(receipt => {
+ updateNFTTransaction(tx.hash, {
+ fromBlockNumber: receipt.blockNumber,
+ })
+ setTxHash(receipt.transactionHash)
+ exciseSelected()
+ })
+ .catch(error => {
+ updatePromptMessage(error.message)
+ })
+ .finally(() => {
+ setSendLoading(false)
+ setSendModalLoading(false)
+ })
+ } catch (e) {
+ setSendLoading(false)
+ setSendModalLoading(false)
+ }
+ }
+
+ const deposite = () => {
+ if (contract.type === TOEKN_TYPE[721]) {
+ return deposite721()
+ }
+ return deposite1155()
+ }
+
+ const deposite721 = async () => {
+ const tx = await networksAndSigners[ChainId.SCROLL_LAYER_1].gateway_721["batchDepositERC721(address,address,uint256[],uint256)"](
+ contract.l1,
+ walletCurrentAddress,
+ selectedTokenIds,
+ gasLimit,
+ {
+ value: gasFee,
+ },
+ )
+ return tx
+ }
+
+ const deposite1155 = async () => {
+ const tx = await networksAndSigners[ChainId.SCROLL_LAYER_1].gateway_1155["batchDepositERC1155(address,address,uint256[],uint256[],uint256)"](
+ contract.l1,
+ walletCurrentAddress,
+ selectedTokenIds,
+ selectedList.map(item => item.transferAmount),
+ gasLimit,
+ {
+ value: gasFee,
+ },
+ )
+ return tx
+ }
+
+ const withdraw = () => {
+ if (contract.type === "ERC721") {
+ return withdraw721()
+ }
+ return withdraw1155()
+ }
+
+ const withdraw721 = async () => {
+ const tx = await networksAndSigners[ChainId.SCROLL_LAYER_2].gateway_721["batchWithdrawERC721(address,address,uint256[],uint256)"](
+ contract.l2,
+ walletCurrentAddress,
+ selectedTokenIds,
+ gasLimit,
+ { value: gasFee },
+ )
+ return tx
+ }
+
+ const withdraw1155 = async () => {
+ const tx = await networksAndSigners[ChainId.SCROLL_LAYER_2].gateway_1155["batchWithdrawERC1155(address,address,uint256[],uint256[],uint256)"](
+ contract.l2,
+ walletCurrentAddress,
+ selectedTokenIds,
+ selectedList.map(item => item.transferAmount),
+ gasLimit,
+ { value: gasFee },
+ )
+ return tx
+ }
+
+ const handleCloseSendModal = () => {
+ setSendModalLoading(false)
+ }
+
+ const handleCloseApproveModal = () => {
+ setApproveModalLoading(false)
+ }
+
+ const handleCloseResultModal = () => {
+ setTxHash("")
+ }
+
+ return (
+
+ {needApproval ? (
+
+ APPROVE
+
+ ) : (
+
+ SEND
+
+ )}
+
+
+
+
+ )
+}
+
+export default Send
diff --git a/src/pages/nftBridge/NFTPanel/Transfer/SendLoadingModal.tsx b/src/pages/nftBridge/NFTPanel/Transfer/SendLoadingModal.tsx
new file mode 100644
index 000000000..bbb6d4dd2
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Transfer/SendLoadingModal.tsx
@@ -0,0 +1,27 @@
+import { Box, Typography } from "@mui/material"
+
+import Link from "@/components/Link"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+
+import Modal from "../../components/Modal"
+
+const SendLoading = props => {
+ const { open, onClose } = props
+ const { walletName } = useWeb3Context()
+ const { fromNetwork, toNetwork } = useNFTBridgeStore()
+
+ return (
+
+ Not responding? Click here for help
+
+
+ Bridging your NFTs from {fromNetwork.name} to {toNetwork.name}
+
+ Confirm this transaction on your {walletName} wallet
+
+
+ )
+}
+
+export default SendLoading
diff --git a/src/pages/nftBridge/NFTPanel/Transfer/TransactionResultModal.tsx b/src/pages/nftBridge/NFTPanel/Transfer/TransactionResultModal.tsx
new file mode 100644
index 000000000..303883694
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Transfer/TransactionResultModal.tsx
@@ -0,0 +1,37 @@
+import { useMemo } from "react"
+
+import { Button } from "@mui/material"
+
+import Link from "@/components/Link"
+import { BLOCK_EXPLORER } from "@/constants"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import { generateTxLink } from "@/utils"
+
+import Modal from "../../components/Modal"
+
+const TransactionResultModal = props => {
+ const { open, hash, onClose } = props
+
+ const { chainId } = useWeb3Context()
+
+ const txUrl = useMemo(() => {
+ if (hash && chainId) {
+ const explorer = BLOCK_EXPLORER[chainId]
+ return generateTxLink(explorer, hash)
+ }
+ return ""
+ }, [chainId, hash])
+
+ return (
+
+
+ View on block explorer
+
+
+
+ )
+}
+
+export default TransactionResultModal
diff --git a/src/pages/nftBridge/NFTPanel/Transfer/index.tsx b/src/pages/nftBridge/NFTPanel/Transfer/index.tsx
new file mode 100644
index 000000000..16a943bbb
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Transfer/index.tsx
@@ -0,0 +1,42 @@
+import { Box, Divider, Stack, Typography } from "@mui/material"
+
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+
+import Gallery from "../../components/Gallery"
+import SelectedItem from "../../components/Gallery/SelectedItem"
+import NetworkSelect from "../../components/NetworkSelect"
+import Fee from "./Fee"
+import Send from "./Send"
+
+const Transfer = props => {
+ const { toNetwork, selectedList } = useNFTBridgeStore()
+
+ return (
+
+
+
+ Transfer to
+
+
+
+
+
+ Selected NFTs
+
+
+ {selectedList.map(item => (
+
+ ))}
+
+
+
+
+
+ )
+}
+
+export default Transfer
diff --git a/src/pages/nftBridge/NFTPanel/Viewing/index.tsx b/src/pages/nftBridge/NFTPanel/Viewing/index.tsx
new file mode 100644
index 000000000..19d01ad18
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/Viewing/index.tsx
@@ -0,0 +1,178 @@
+import { useState } from "react"
+import useSWR from "swr"
+
+import { Box, InputBase, Stack, Typography } from "@mui/material"
+
+import { nftTokenListUrl } from "@/apis/dynamic"
+import { TOEKN_TYPE, networks } from "@/constants"
+import { useNFTBridgeContext } from "@/contexts/NFTBridgeProvider"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+import { requireEnv, switchNetwork } from "@/utils"
+
+import ContractSelect from "../../components/ContractSelect"
+import Gallery from "../../components/Gallery"
+import ViewingItem from "../../components/Gallery/ViewingItem"
+import NetworkSelect from "../../components/NetworkSelect"
+import LoadingButton from "../../components/SearchButton"
+
+const branchName = requireEnv("REACT_APP_SCROLL_ENVIRONMENT").toLocaleLowerCase()
+
+const Viewing = props => {
+ const { walletCurrentAddress } = useWeb3Context()
+ const { tokenInstance } = useNFTBridgeContext()
+ const { fromNetwork, viewingList, addViewingList, clearViewingList, clearSelectedList, contract, changeContract, updatePromptMessage } =
+ useNFTBridgeStore()
+
+ const [currentTokenId, setCurrentTokenId] = useState("")
+ const [searchLoading, setSearchLoading] = useState(false)
+
+ const { data: contractList } = useSWR(
+ nftTokenListUrl(branchName),
+ url => {
+ return scrollRequest(url)
+ .then((data: any) => {
+ if (!contract.type) {
+ changeContract(data.tokens[0])
+ }
+ return data.tokens
+ })
+ .catch(() => {
+ // setFetchTokenListError("Fail to fetch token list")
+ // setTokenSymbol(ETH_SYMBOL)
+ return []
+ })
+ },
+ {
+ revalidateOnFocus: false,
+ },
+ )
+ const handleChangeFromNetwork = value => {
+ switchNetwork(value)
+ clearViewingList()
+ clearSelectedList()
+ }
+
+ const handleChangeContract = value => {
+ changeContract(value)
+ clearViewingList()
+ clearSelectedList()
+ }
+
+ const handleChangeTokenId = e => {
+ const { value } = e.target
+ if (value) {
+ setCurrentTokenId(isNaN(+value) ? "" : +value)
+ } else {
+ setCurrentTokenId("")
+ }
+ }
+
+ // TODO: tip for not owned/exists token
+ const handleSearchToken = async () => {
+ try {
+ setSearchLoading(true)
+ if (viewingList.find(item => item.id === currentTokenId)) {
+ throw new Error("Duplicate TokenId!")
+ }
+ let uri
+ let amount = 1
+ if (contract?.type === TOEKN_TYPE[721]) {
+ const owned = await isOwned(currentTokenId)
+
+ if (owned) {
+ uri = await tokenInstance["tokenURI(uint256)"](currentTokenId)
+ } else {
+ throw new Error("Not your token!")
+ }
+ } else {
+ const exists = await isExists(currentTokenId)
+ if (exists) {
+ uri = await tokenInstance["uri(uint256)"](currentTokenId)
+ amount = await tokenInstance["balanceOf(address,uint256)"](walletCurrentAddress, currentTokenId)
+ amount = Number(amount)
+ } else {
+ throw new Error("Token does not exist!")
+ }
+ }
+
+ if (uri && uri.startsWith("http")) {
+ scrollRequest(uri).then(data => {
+ addViewingList({ id: currentTokenId, amount, ...data, transferAmount: amount })
+ })
+ } else {
+ addViewingList({ id: currentTokenId, amount, name: uri, transferAmount: amount })
+ }
+ setCurrentTokenId("")
+ } catch (e) {
+ updatePromptMessage(e.message)
+ } finally {
+ setSearchLoading(false)
+ }
+ }
+
+ const isOwned = async tokenId => {
+ try {
+ const owner = await tokenInstance["ownerOf(uint256)"](tokenId)
+ return owner === walletCurrentAddress
+ } catch (e) {
+ return false
+ }
+ }
+ const isExists = async tokenId => {
+ try {
+ const exists = await tokenInstance["exists(uint256)"](tokenId)
+ return exists
+ } catch (e) {
+ return true
+ }
+ }
+
+ return (
+
+
+
+ Select your NFTs on
+
+
+
+
+
+ theme.palette.background.default,
+ },
+ }}
+ placeholder="TOKEN ID"
+ value={currentTokenId}
+ onChange={handleChangeTokenId}
+ />
+
+ Search
+
+
+
+ {viewingList.map(item => (
+
+ ))}
+
+
+ )
+}
+
+export default Viewing
diff --git a/src/pages/nftBridge/NFTPanel/index.tsx b/src/pages/nftBridge/NFTPanel/index.tsx
new file mode 100644
index 000000000..47d593b55
--- /dev/null
+++ b/src/pages/nftBridge/NFTPanel/index.tsx
@@ -0,0 +1,58 @@
+import { useEffect } from "react"
+
+import { Container } from "@mui/material"
+
+import { ChainId, networks } from "@/constants"
+import NFTBridgeProvider from "@/contexts/NFTBridgeProvider"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+import { switchNetwork } from "@/utils"
+
+import CornerTip from "../components/CornerTip"
+import Transfer from "./Transfer"
+import Viewing from "./Viewing"
+
+const NFTPanel = () => {
+ const { chainId } = useWeb3Context()
+ const { changeFromNetwork, changeToNetwork, promptMessage, updatePromptMessage } = useNFTBridgeStore()
+
+ useEffect(() => {
+ if (chainId && Object.values(ChainId).includes(chainId)) {
+ const fromNetworkIndex = networks.findIndex(item => item.chainId === chainId)
+ changeFromNetwork(networks[fromNetworkIndex])
+ changeToNetwork(networks[+!fromNetworkIndex])
+ } else if (chainId) {
+ changeFromNetwork(networks[0])
+ changeToNetwork(networks[1])
+ switchNetwork(networks[0].chainId)
+ } else {
+ changeFromNetwork(networks[0])
+ changeToNetwork(networks[1])
+ }
+ }, [chainId])
+
+ const handleClearpromptMessage = () => {
+ updatePromptMessage("")
+ }
+ return (
+
+
+
+
+
+ {promptMessage}
+
+
+
+ )
+}
+
+export default NFTPanel
diff --git a/src/pages/nftBridge/components/ContractSelect/index.tsx b/src/pages/nftBridge/components/ContractSelect/index.tsx
new file mode 100644
index 000000000..67b265289
--- /dev/null
+++ b/src/pages/nftBridge/components/ContractSelect/index.tsx
@@ -0,0 +1,113 @@
+import { makeStyles } from "tss-react/mui"
+
+import { Autocomplete, Chip, Icon, Stack, TextField, Typography } from "@mui/material"
+
+import { ReactComponent as ArrowDownIcon } from "@/assets/svgs/arrow-down.svg"
+import Link from "@/components/Link"
+import { ChainId } from "@/constants"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+
+const useStyles = makeStyles()(theme => {
+ return {
+ AutocompleteRoot: {
+ width: "56rem",
+ color: "#7e7e7e",
+ },
+ listbox: {
+ padding: 0,
+ },
+ input: {
+ fontSize: "1.4rem",
+ height: "1.8rem",
+ color: "#7e7e7e",
+ userSelect: "none",
+ },
+ inputRoot: {
+ backgroundColor: "#e0e0e0",
+ "&.Mui-focused": {
+ backgroundColor: theme.palette.background.default,
+ },
+ },
+ option: {
+ paddingLeft: "0.6rem !important",
+ paddingRight: "0.6rem !important",
+ justifyContent: "space-between !important",
+
+ ".faucetLink": {
+ visibility: "hidden",
+ },
+
+ "&:hover": {
+ ".faucetLink": {
+ visibility: "visible",
+ },
+ },
+ },
+
+ ChipRoot: {
+ height: "auto",
+ padding: "0 4px",
+ borderRadius: "4px",
+ backgroundColor: "#e0e0e0",
+ },
+ label: {
+ fontSize: "1rem",
+ color: "#7e7e7e",
+ padding: 0,
+ },
+
+ clearIndicator: {
+ transform: "scale(0.6)",
+ },
+ }
+})
+
+const ContractSelect = props => {
+ const { value, data, onChange } = props
+ const { checkConnectedChainId } = useWeb3Context()
+ const { classes } = useStyles()
+ return (
+ }
+ size="small"
+ classes={{
+ root: classes.AutocompleteRoot,
+ listbox: classes.listbox,
+ input: classes.input,
+ inputRoot: classes.inputRoot,
+ option: classes.option,
+ clearIndicator: classes.clearIndicator,
+ }}
+ getOptionLabel={option => (checkConnectedChainId(ChainId.SCROLL_LAYER_1) ? option?.l1 : option?.l2) ?? ""}
+ renderInput={params => (
+
+ )}
+ renderOption={(innerProps: any, option, state) => (
+
+
+
+ {checkConnectedChainId(ChainId.SCROLL_LAYER_1) ? option.l1 : option.l2}
+
+ {checkConnectedChainId(ChainId.SCROLL_LAYER_1) && (
+
+ faucet
+
+ )}
+
+ )}
+ onChange={(event, newValue) => {
+ onChange(newValue)
+ }}
+ />
+ )
+}
+
+export default ContractSelect
diff --git a/src/pages/nftBridge/components/CornerTip/index.tsx b/src/pages/nftBridge/components/CornerTip/index.tsx
new file mode 100644
index 000000000..476086234
--- /dev/null
+++ b/src/pages/nftBridge/components/CornerTip/index.tsx
@@ -0,0 +1,24 @@
+import { makeStyles } from "tss-react/mui"
+
+import { Alert, Snackbar } from "@mui/material"
+
+const useStyles = makeStyles()(theme => ({
+ message: {
+ wordBreak: "break-all",
+ },
+}))
+
+const CornerTip = props => {
+ const { open, children, autoHideDuration = 6000, onClose, AlertProps, severity } = props
+
+ const { classes } = useStyles()
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export default CornerTip
diff --git a/src/pages/nftBridge/components/Gallery/SelectedItem.tsx b/src/pages/nftBridge/components/Gallery/SelectedItem.tsx
new file mode 100644
index 000000000..7ffd7299f
--- /dev/null
+++ b/src/pages/nftBridge/components/Gallery/SelectedItem.tsx
@@ -0,0 +1,81 @@
+import { Box, Card, CardContent, CardMedia, IconButton, InputBase, Stack, SvgIcon, Typography } from "@mui/material"
+
+import { ReactComponent as CloseIconSvg } from "@/assets/svgs/close.svg"
+import EmptyImg from "@/assets/svgs/empty-img.svg"
+import { TOEKN_TYPE } from "@/constants"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+
+const SelectedItem = props => {
+ const { id, image, name, amount, transferAmount } = props
+
+ const { contract, toggleSelectedList, updateSelectedList } = useNFTBridgeStore()
+
+ const handleRemoveSelectedId = () => {
+ toggleSelectedList(id)
+ }
+
+ const handleChangeTransferAmount = e => {
+ const { value } = e.target
+ let transferAmount
+ if (value) {
+ transferAmount = isNaN(+value) ? undefined : +value
+ } else {
+ transferAmount = undefined
+ }
+ updateSelectedList(id, { transferAmount })
+ }
+
+ return (
+
+
+
+ {`#${id}` || name}
+ {contract.type === TOEKN_TYPE[1155] && (
+
+
+ Amount:
+ `1px solid ${theme.palette.error.main}`,
+ },
+ }}
+ value={transferAmount}
+ onChange={handleChangeTransferAmount}
+ >
+ / {amount}
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+export default SelectedItem
diff --git a/src/pages/nftBridge/components/Gallery/ViewingItem.tsx b/src/pages/nftBridge/components/Gallery/ViewingItem.tsx
new file mode 100644
index 000000000..0c90ada0c
--- /dev/null
+++ b/src/pages/nftBridge/components/Gallery/ViewingItem.tsx
@@ -0,0 +1,71 @@
+import { Box, Card, CardContent, CardMedia, Stack, SvgIcon, Typography } from "@mui/material"
+
+import { ReactComponent as CheckIconSvg } from "@/assets/svgs/check.svg"
+import EmptyImg from "@/assets/svgs/empty-img.svg"
+import { TOEKN_TYPE } from "@/constants"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+
+const GalleryItem = props => {
+ const { id, image, name, amount } = props
+
+ const { contract, toggleSelectedList } = useNFTBridgeStore()
+ const selectedTokenIds = useNFTBridgeStore(state => state.selectedTokenIds())
+
+ const handleToggleSelect = () => {
+ toggleSelectedList(id)
+ }
+
+ return (
+
+
+
+ {`#${id}` || name}
+ {contract.type === TOEKN_TYPE[1155] && (
+
+
+ Amount:
+ {amount}
+
+
+ )}
+
+ {selectedTokenIds.includes(id) && (
+
+
+
+ )}
+
+ )
+}
+
+export default GalleryItem
diff --git a/src/pages/nftBridge/components/Gallery/index.tsx b/src/pages/nftBridge/components/Gallery/index.tsx
new file mode 100644
index 000000000..3dcf5abec
--- /dev/null
+++ b/src/pages/nftBridge/components/Gallery/index.tsx
@@ -0,0 +1,56 @@
+import { Box, Typography } from "@mui/material"
+import { styled } from "@mui/material/styles"
+
+import TextButton from "@/components/TextButton"
+import { useWeb3Context } from "@/contexts/Web3ContextProvider"
+import useNFTBridgeStore from "@/stores/nftBridgeStore"
+import { switchNetwork } from "@/utils"
+
+const Container = styled("div")(
+ ({ theme }) => `
+ display: grid;
+ width: 100%;
+ grid-column-gap: 1rem;
+ grid-row-gap: 1rem;
+ grid-auto-rows: min-content;
+`,
+)
+
+const SelectedGallery = props => {
+ const { column, emptyTip, sx, children, ...restProps } = props
+ const { walletCurrentAddress, connectWallet, chainId } = useWeb3Context()
+
+ const { fromNetwork } = useNFTBridgeStore()
+
+ const renderTip = () => {
+ if (!walletCurrentAddress) {
+ return (
+
+ Click here to connect wallet
+
+ )
+ } else if (chainId !== fromNetwork.chainId) {
+ return switchNetwork(fromNetwork.chainId)}>Click here to switch to {fromNetwork.name}.
+ }
+ return (
+
+ {emptyTip}
+
+ )
+ }
+ return (
+ <>
+ {children.length ? (
+
+ {children}
+
+ ) : (
+
+ {renderTip()}
+
+ )}
+ >
+ )
+}
+
+export default SelectedGallery
diff --git a/src/pages/nftBridge/components/LargeTextField/index.tsx b/src/pages/nftBridge/components/LargeTextField/index.tsx
new file mode 100644
index 000000000..7cb03a0a2
--- /dev/null
+++ b/src/pages/nftBridge/components/LargeTextField/index.tsx
@@ -0,0 +1,73 @@
+import { FC, ReactNode } from "react"
+import { makeStyles } from "tss-react/mui"
+
+import MuiTextField, { StandardTextFieldProps } from "@mui/material/TextField"
+
+type LargeTextFieldProps = {
+ units?: string | ReactNode
+ centerAlign?: boolean | undefined
+ leftAlign?: boolean | undefined
+ defaultShadow?: boolean | undefined
+ smallFontSize?: boolean
+} & StandardTextFieldProps
+
+const useStyles = makeStyles()(theme => {
+ return {
+ root: {
+ display: "flex",
+ width: "100%",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ },
+ adornment: {
+ width: "auto",
+ textAlign: "right",
+ [theme.breakpoints.down("sm")]: {
+ fontSize: theme.typography.subtitle2.fontSize,
+ },
+ },
+ }
+})
+
+const useInputStyles = makeStyles()((theme, { leftAlign, centerAlign }) => ({
+ root: {
+ transition: "all 0.15s ease-out",
+ width: "100%",
+ },
+ input: {
+ textAlign: leftAlign ? "left" : centerAlign ? "center" : "right",
+ fontSize: theme.typography.h4.fontSize,
+ fontWeight: theme.typography.h4.fontWeight,
+ color: theme.palette.text.primary,
+ textOverflow: "clip",
+ padding: "6px 4px",
+ [theme.breakpoints.down("sm")]: {
+ fontSize: "2.4rem",
+ },
+ },
+}))
+
+const LargeTextField: FC = props => {
+ const { className, units, leftAlign = false, centerAlign, sx, ...textFieldProps } = props
+ const { classes, cx } = useStyles()
+ const { classes: inputStyles } = useInputStyles({
+ leftAlign,
+ centerAlign,
+ })
+
+ return (
+
+ )
+}
+
+export default LargeTextField
diff --git a/src/pages/nftBridge/components/Modal/index.tsx b/src/pages/nftBridge/components/Modal/index.tsx
new file mode 100644
index 000000000..beaf5f5fa
--- /dev/null
+++ b/src/pages/nftBridge/components/Modal/index.tsx
@@ -0,0 +1,51 @@
+import CloseIcon from "@mui/icons-material/Close"
+import { CircularProgress, Dialog, DialogContent, DialogTitle, Icon, IconButton, Typography } from "@mui/material"
+
+import { ReactComponent as SuccessSvg } from "@/assets/svgs/success.svg"
+
+const Modal = props => {
+ const { open, title, variant, children, onClose } = props
+ const renderIndicator = () => {
+ if (variant === "loading") {
+ return (
+
+ )
+ } else if (variant === "success") {
+ return
+ }
+ return null
+ }
+
+ return (
+
+ )
+}
+
+export default Modal
diff --git a/src/pages/nftBridge/components/NetworkSelect/index.tsx b/src/pages/nftBridge/components/NetworkSelect/index.tsx
new file mode 100644
index 000000000..1246dfe13
--- /dev/null
+++ b/src/pages/nftBridge/components/NetworkSelect/index.tsx
@@ -0,0 +1,104 @@
+import { makeStyles } from "tss-react/mui"
+
+import { ListItemIcon, ListItemText, MenuItem, Select } from "@mui/material"
+
+import { ReactComponent as ArrowDownIcon } from "@/assets/svgs/arrow-down.svg"
+
+const useStyles = makeStyles()(theme => ({
+ networkSelect: {
+ width: "22rem",
+ height: "3rem",
+ border: "none",
+ borderRadius: "0.8rem",
+ backgroundColor: theme.palette.background.default,
+ boxShadow: theme.boxShadows.sharp,
+
+ ".MuiSelect-select": {
+ display: "flex",
+ alignItems: "center",
+ padding: "0 1rem",
+
+ "&:focus": {
+ backgroundColor: "unset",
+ },
+ "&.Mui-disabled": {
+ WebkitTextFillColor: theme.palette.text.primary,
+ },
+ },
+ ".MuiTypography-root": {
+ fontSize: "1.3rem",
+ fontWeight: 500,
+ },
+ ".MuiListItemIcon-root": {
+ minWidth: "unset",
+ },
+ ".MuiSelect-icon": {
+ top: "unset",
+ right: "1.2rem",
+ color: theme.palette.text.primary,
+ transform: "scale(0.7)",
+ },
+ ".MuiSelect-iconOpen": {
+ transform: "rotate(180deg) scale(0.7)",
+ },
+ },
+ networkIcon: {
+ display: "flex",
+ height: "1.6rem",
+ margin: "0.4rem",
+ },
+
+ networkMenuItem: {
+ padding: "0.2rem 1rem",
+ ".MuiTypography-root": {
+ fontSize: "1.2rem",
+ },
+ ".MuiListItemIcon-root": {
+ minWidth: "unset",
+ },
+ },
+ optionModal: {
+ borderRadius: "0 0 0.8rem 0.8rem",
+ },
+ optionList: {
+ padding: 0,
+ },
+ optionListText: {
+ cursor: "pointer",
+ },
+}))
+
+const NetworkSelect = props => {
+ const { options, value, onChange, ...extraProps } = props
+ const { classes } = useStyles()
+ const icon = options.length <= 1 ? () => null : ArrowDownIcon
+ return (
+
+ )
+}
+
+export default NetworkSelect
diff --git a/src/pages/nftBridge/components/SearchButton/index.tsx b/src/pages/nftBridge/components/SearchButton/index.tsx
new file mode 100644
index 000000000..19fd3affd
--- /dev/null
+++ b/src/pages/nftBridge/components/SearchButton/index.tsx
@@ -0,0 +1,20 @@
+import { styled } from "@mui/system"
+
+import LoadingButton from "@/components/LoadingButton"
+
+const SearchButton = styled(LoadingButton)(
+ ({ theme }: any) => `
+ height: 3.5rem;
+ font-size: 1.4rem;
+ box-shadow: ${theme.boxShadows.sharp};
+ background-color: ${theme.palette.background.default};
+ color: ${theme.palette.text.primary};
+ :hover{
+ color: ${theme.palette.primary.main};
+ background-color: ${theme.palette.background.default};
+ box-shadow: ${theme.boxShadows.sharp};
+ }
+`,
+)
+
+export default SearchButton
diff --git a/src/pages/nftBridge/components/TokenIdInput/index.tsx b/src/pages/nftBridge/components/TokenIdInput/index.tsx
new file mode 100644
index 000000000..9d66a63a9
--- /dev/null
+++ b/src/pages/nftBridge/components/TokenIdInput/index.tsx
@@ -0,0 +1,27 @@
+import React from "react"
+
+import SearchIcon from "@mui/icons-material/Search"
+import { Divider, IconButton, InputBase, Paper } from "@mui/material"
+
+const TokenIdInput = props => {
+ const { value, onChange, onEnsure } = props
+
+ return (
+
+ ) => {
+ onChange(event.target.value)
+ }}
+ />
+
+
+
+
+
+ )
+}
+
+export default TokenIdInput
diff --git a/src/pages/nftBridge/components/TxTable/index.tsx b/src/pages/nftBridge/components/TxTable/index.tsx
new file mode 100644
index 000000000..759d30ec9
--- /dev/null
+++ b/src/pages/nftBridge/components/TxTable/index.tsx
@@ -0,0 +1,245 @@
+import { useCallback, useMemo } from "react"
+import { makeStyles } from "tss-react/mui"
+
+import {
+ Chip,
+ CircularProgress,
+ Pagination,
+ Paper,
+ Skeleton,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography,
+} from "@mui/material"
+
+import Link from "@/components/Link"
+import { useApp } from "@/contexts/AppContextProvider"
+import { generateContractLink, generateTxLink, truncateAddress, truncateHash } from "@/utils"
+
+const useStyles = makeStyles()(theme => {
+ return {
+ tableContainer: {
+ whiteSpace: "nowrap",
+ [theme.breakpoints.down("sm")]: {
+ paddingBottom: "1.6rem",
+ overflowX: "scroll",
+ },
+ },
+ tableWrapper: {
+ boxShadow: "unset",
+ border: `1px solid ${theme.palette.border.main}`,
+ borderRadius: "1rem",
+ width: "82rem",
+ },
+ tableTitle: {
+ marginTop: "2.8rem",
+ marginBottom: "3rem",
+ [theme.breakpoints.down("sm")]: {
+ marginTop: "1.6rem",
+ marginBottom: "1.6rem",
+ },
+ },
+ tableHeader: {
+ backgroundColor: theme.palette.scaleBackground.primary,
+ ".MuiTableCell-head": {
+ borderBottom: "unset",
+ },
+ },
+ chip: {
+ width: "9rem",
+ height: "2.8rem",
+ fontSize: "1.2rem",
+ padding: 0,
+ fontWeight: 500,
+ ".MuiChip-label": {
+ paddingLeft: 0,
+ paddingRight: 0,
+ },
+ },
+ pendingChip: {
+ color: theme.palette.tagWarning.main,
+ backgroundColor: theme.palette.tagWarning.light,
+ },
+ successChip: {
+ color: theme.palette.tagSuccess.main,
+ backgroundColor: theme.palette.tagSuccess.light,
+ },
+ pagination: {
+ ".MuiPaginationItem-text": {
+ fontSize: "1.6rem",
+ },
+ ".MuiPaginationItem-root": {
+ color: theme.palette.text.secondary,
+ },
+ ".MuiPaginationItem-root.Mui-selected": {
+ fontWeight: 700,
+ backgroundColor: "unset",
+ },
+ ".MuiSvgIcon-root": {
+ fontSize: "2.4rem",
+ },
+ },
+ }
+})
+
+const TxTable = (props: any) => {
+ const { data, pagination, loading } = props
+
+ const { classes } = useStyles()
+
+ const handleChangePage = (e, newPage) => {
+ pagination?.onChange?.(newPage)
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Status
+ Contract Address
+ Type
+ Token IDs
+ Amount
+ Txn Hash
+
+
+
+ {loading ? (
+
+ ) : (
+ <>
+ {data?.map((tx: any) => (
+
+ ))}
+ >
+ )}
+
+
+
+
+ {pagination && (
+
+ )}
+ >
+ )
+}
+
+const TxRow = props => {
+ const { tx } = props
+
+ const {
+ txHistory: { blockNumbers },
+ } = useApp()
+
+ const { classes, cx } = useStyles()
+
+ const txStatus = useCallback(
+ (blockNumber, isL1, to) => {
+ if (!blockNumber || !blockNumbers) {
+ return "Pending"
+ }
+ if (blockNumbers[+!(isL1 ^ to)] >= blockNumber) {
+ return "Success"
+ }
+ return "Pending"
+ },
+ [blockNumbers],
+ )
+
+ const fromStatus = useMemo(() => {
+ return txStatus(tx.fromBlockNumber, tx.isL1, false)
+ }, [tx, txStatus])
+
+ const toStatus = useMemo(() => {
+ return txStatus(tx.toBlockNumber, tx.isL1, true)
+ }, [tx, txStatus])
+
+ return (
+
+
+
+ {blockNumbers ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+ {truncateAddress(tx.tokenAddress)}
+
+
+
+
+
+
+
+
+
+
+ {tx.tokenIds.map(item => (
+ {item}
+ ))}
+
+
+
+
+ {tx.amounts.map(item => (
+ {item}
+ ))}
+
+
+
+
+ From {tx.fromName}:
+
+
+ {truncateHash(tx.hash)}
+
+
+
+
+
+ To {tx.toName}:
+
+ {tx.toHash ? (
+
+ {truncateHash(tx.toHash)}
+
+ ) : (
+ -
+ )}
+
+
+
+
+ )
+}
+
+export default TxTable
diff --git a/src/pages/nftBridge/index.tsx b/src/pages/nftBridge/index.tsx
new file mode 100644
index 000000000..7d47a8e9b
--- /dev/null
+++ b/src/pages/nftBridge/index.tsx
@@ -0,0 +1,24 @@
+import { GlobalStyles } from "@mui/material"
+
+import AppProvider from "@/contexts/AppContextProvider"
+
+import Header from "./Header"
+import NFTPanel from "./NFTPanel"
+
+const NFTBridge = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+export default NFTBridge
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index c771a8121..0025b394b 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -2,6 +2,7 @@ import IframeEmbedding from "@/components/IframeEmbedding"
import Bridge from "@/pages/bridge"
import Ecosystem from "@/pages/ecosystem"
import Portal from "@/pages/home"
+import NFTBridge from "@/pages/nftBridge"
import RollupScanBatch from "@/pages/rollup/batch"
import RollupScanBlock from "@/pages/rollup/block"
import RollupScan from "@/pages/rollup/index"
@@ -52,6 +53,13 @@ const routes = [
fullPath: "/bridge",
element: ,
},
+
+ {
+ name: "NFTBridge",
+ path: "/nft-bridge",
+ fullPath: "/nft-bridge",
+ element: ,
+ },
{
name: "Ecosystem",
path: "/ecosystem",
diff --git a/src/stores/nftBridgeStore.ts b/src/stores/nftBridgeStore.ts
new file mode 100644
index 000000000..9be01c98f
--- /dev/null
+++ b/src/stores/nftBridgeStore.ts
@@ -0,0 +1,134 @@
+import produce from "immer"
+import create from "zustand"
+
+interface NFTToken {
+ id: number
+ amount: number
+ name?: string
+ image?: string
+ description?: string
+ transferAmount?: number
+}
+
+interface Contract {
+ type?: string
+ l1?: string
+ l2?: string
+ faucet?: string
+}
+
+interface NFTBridgeStore {
+ fromNetwork: any
+ toNetwork: any
+ contract: Contract
+ viewingList: NFTToken[]
+ selectedList: NFTToken[]
+ promptMessage: string
+ selectedTokenIds: () => number[]
+ selectedTokenAmount: () => number
+ changeContract: (value) => void
+ changeFromNetwork: (value) => void
+ changeToNetwork: (value) => void
+ addViewingList: (token) => void
+ removeViewingList: (id) => void
+ clearViewingList: () => void
+ clearSelectedList: () => void
+ toggleSelectedList: (id) => void
+ updateSelectedList: (id, params) => void
+ exciseSelected: () => void
+ updatePromptMessage: (value) => void
+}
+
+const useNFTBridgeStore = create()((set, get) => ({
+ contract: {},
+ fromNetwork: { chainId: 0 },
+ toNetwork: { chainId: 0 },
+ promptMessage: "",
+ viewingList: [],
+ selectedList: [],
+ selectedTokenIds: () => get().selectedList.map(item => item.id),
+
+ selectedTokenAmount: () => get().selectedList.reduce((pre, cur: any) => pre + (cur.transferAmount ?? 0), 0),
+
+ changeFromNetwork: value => {
+ set({
+ fromNetwork: value,
+ })
+ },
+ changeToNetwork: value => {
+ set({
+ toNetwork: value,
+ })
+ },
+ changeContract: contract => {
+ set({
+ contract: contract || {},
+ })
+ },
+
+ addViewingList: token => {
+ set({
+ viewingList: get().viewingList.concat(token),
+ })
+ },
+
+ removeViewingList: id => {
+ const curViewingList = get().viewingList
+ const nextViewingList = curViewingList.filter(item => item.id !== id)
+ set({
+ viewingList: nextViewingList,
+ })
+ },
+
+ clearViewingList: () => {
+ set({
+ viewingList: [],
+ })
+ },
+
+ clearSelectedList: () => {
+ set({
+ selectedList: [],
+ })
+ },
+
+ toggleSelectedList: id => {
+ const curSelectedList = get().selectedList
+ const tokenIndex = curSelectedList.findIndex(item => item.id === id)
+ if (tokenIndex > -1) {
+ const nextSelectedList = [...curSelectedList]
+ nextSelectedList.splice(tokenIndex, 1)
+ set({
+ selectedList: nextSelectedList,
+ })
+ } else {
+ const token = get().viewingList.find(item => item.id === id)
+ set({
+ selectedList: get().selectedList.concat(token as NFTToken),
+ })
+ }
+ },
+ exciseSelected: () => {
+ const selectedTokenIds = get().selectedTokenIds()
+ const nextViewingList = get().viewingList.filter(item => !selectedTokenIds.includes(item.id))
+ set({
+ viewingList: nextViewingList,
+ selectedList: [],
+ })
+ },
+ updateSelectedList: (id, params) => {
+ set(
+ produce(state => {
+ const curToken = state.selectedList.find(item => item.id === id)
+ for (let key of Object.keys(params)) {
+ curToken[key] = params[key]
+ }
+ }),
+ )
+ },
+ updatePromptMessage: promptMessage => {
+ set({ promptMessage })
+ },
+}))
+
+export default useNFTBridgeStore
diff --git a/src/stores/nftTxStore.ts b/src/stores/nftTxStore.ts
new file mode 100644
index 000000000..40df86561
--- /dev/null
+++ b/src/stores/nftTxStore.ts
@@ -0,0 +1,197 @@
+import produce from "immer"
+import create from "zustand"
+import { persist } from "zustand/middleware"
+
+import { fetchTxListUrl } from "@/apis/bridge"
+import { networks } from "@/constants"
+import { NFT_BRIDGE_TRANSACTIONS } from "@/utils/storageKey"
+
+interface NFTTxStore {
+ page: number
+ total: number
+ loading: boolean
+ frontTransactions: Transaction[]
+ transactions: Transaction[]
+ pageTransactions: Transaction[]
+ addNFTTransaction: (tx) => void
+ updateNFTTransaction: (hash, tx) => void
+ generateNFTTransactions: (transactions, safeBlockNumber) => void
+ comboPageNFTTransactions: (address, page, rowsPerPage, safeBlockNumber) => Promise
+ clearNFTTransactions: () => void
+}
+interface Transaction {
+ hash: string
+ toHash?: string
+ fromName: string
+ toName: string
+ fromExplore: string
+ toExplore?: string
+ fromBlockNumber?: number
+ toBlockNumber?: number
+ tokenType: string
+ tokenAdress: string
+ amounts: Array
+ tokenIds: Array
+ isL1: boolean
+}
+
+const formatBackTxList = (backList, safeBlockNumber) => {
+ if (!backList.length) {
+ return []
+ }
+ return backList.map(tx => {
+ const amount = tx.amount
+ const fromName = networks[+!tx.isL1].name
+ const fromExplore = networks[+!tx.isL1].explorer
+ const toName = networks[+tx.isL1].name
+ const toExplore = networks[+tx.isL1].explorer
+ const toHash = tx.finalizeTx?.hash
+ const fromEstimatedEndTime = tx.isL1 && tx.blockNumber > safeBlockNumber ? Date.now() + (tx.blockNumber - safeBlockNumber) * 12 * 1000 : undefined
+ const toEstimatedEndTime =
+ !tx.isL1 && tx.finalizeTx?.blockNumber && tx.finalizeTx.blockNumber > safeBlockNumber
+ ? Date.now() + (tx.finalizeTx.blockNumber - safeBlockNumber) * 12 * 1000
+ : undefined
+ return {
+ hash: tx.hash,
+ amount,
+ fromName,
+ fromExplore,
+ fromBlockNumber: tx.blockNumber,
+ fromEstimatedEndTime,
+ toHash,
+ toName,
+ toExplore,
+ toBlockNumber: tx.finalizeTx?.blockNumber,
+ isL1: tx.isL1,
+ symbolToken: tx.isL1 ? tx.l1Token : tx.l2Token,
+ toEstimatedEndTime,
+ }
+ })
+}
+
+const useNFTTxStore = create()(
+ persist(
+ (set, get) => ({
+ page: 1,
+ total: 0,
+ frontTransactions: [],
+ loading: false,
+ // frontTransactions + backendTransactions.slice(0, 2)
+ transactions: [],
+ pageTransactions: [],
+ // when user send a transaction
+ addNFTTransaction: newTx =>
+ set(state => ({
+ frontTransactions: [newTx, ...state.frontTransactions],
+ transactions: [newTx, ...state.transactions],
+ })),
+ // wait transaction success in from network
+ updateNFTTransaction: (hash, updateOpts) =>
+ set(
+ produce(state => {
+ const frontTx = state.frontTransactions.find(item => item.hash === hash)
+ if (frontTx) {
+ for (const key in updateOpts) {
+ frontTx[key] = updateOpts[key]
+ }
+ }
+ // for stay on "recent tx" page
+ const recentTx = state.transactions.find(item => item.hash === hash)
+ if (recentTx) {
+ for (const key in updateOpts) {
+ recentTx[key] = updateOpts[key]
+ }
+ }
+ // for keep "bridge history" open
+ const pageTx = state.pageTransactions.find(item => item.hash === hash)
+ if (pageTx) {
+ for (const key in updateOpts) {
+ pageTx[key] = updateOpts[key]
+ }
+ }
+ }),
+ ),
+ // polling transactions
+ // slim frontTransactions and keep the latest 3 backTransactions
+ generateNFTTransactions: (historyList, safeBlockNumber) => {
+ const realHistoryList = historyList.filter(item => item)
+ if (realHistoryList.length) {
+ const formattedHistoryList = formatBackTxList(realHistoryList, safeBlockNumber)
+ const formattedHistoryListHash = formattedHistoryList.map(item => item.hash)
+ const formattedHistoryListMap = Object.fromEntries(formattedHistoryList.map(item => [item.hash, item]))
+ const pendingFrontList = get().frontTransactions.filter(item => !formattedHistoryListHash.includes(item.hash))
+ const pendingFrontListHash = pendingFrontList.map(item => item.hash)
+ const syncList = formattedHistoryList.filter(item => !pendingFrontListHash.includes(item.hash))
+ const restList = get().transactions.filter(item => item.toHash)
+
+ const refreshPageTransaction = get().pageTransactions.map(item => {
+ if (formattedHistoryListMap[item.hash]) {
+ return formattedHistoryListMap[item.hash]
+ }
+ return item
+ })
+ set({
+ transactions: pendingFrontList.concat([...syncList, ...restList].slice(0, 2)),
+ frontTransactions: pendingFrontList,
+ pageTransactions: refreshPageTransaction,
+ })
+ }
+ },
+ clearNFTTransactions: () => {
+ set({
+ frontTransactions: [],
+ transactions: [],
+ pageTransactions: [],
+ page: 1,
+ total: 0,
+ })
+ },
+
+ // page transactions
+ comboPageNFTTransactions: async (address, page, rowsPerPage, safeBlockNumber) => {
+ const frontTransactions = get().frontTransactions
+ set({ loading: true })
+ const offset = (page - 1) * rowsPerPage
+ // const offset = gap > 0 ? gap : 0;
+ if (frontTransactions.length >= rowsPerPage + offset) {
+ set({
+ pageTransactions: frontTransactions.slice(offset, offset + rowsPerPage),
+ page,
+ loading: false,
+ })
+ return
+ }
+
+ const currentPageFrontTransactions = frontTransactions.slice((page - 1) * rowsPerPage)
+ const gap = (page - 1) * rowsPerPage - frontTransactions.length
+ const relativeOffset = gap > 0 ? gap : 0
+ const limit = rowsPerPage - currentPageFrontTransactions.length
+
+ return scrollRequest(`${fetchTxListUrl}?address=${address}&offset=${relativeOffset}&limit=${limit}`)
+ .then(data => {
+ set({
+ pageTransactions: [...frontTransactions, ...formatBackTxList(data.data.result, safeBlockNumber)],
+ total: data.data.total,
+ page,
+ loading: false,
+ })
+ if (page === 1) {
+ // keep transactions always frontList + the latest two history list
+ set({
+ transactions: [...frontTransactions, ...formatBackTxList(data.data.result, safeBlockNumber).slice(0, 2)],
+ })
+ }
+ })
+ .catch(error => {
+ set({ loading: false })
+ return Promise.reject(`${error.status}:${error.message}`)
+ })
+ },
+ }),
+ {
+ name: NFT_BRIDGE_TRANSACTIONS,
+ },
+ ),
+)
+
+export default useNFTTxStore
diff --git a/src/theme/light.tsx b/src/theme/light.tsx
index e09e852bb..730f58175 100644
--- a/src/theme/light.tsx
+++ b/src/theme/light.tsx
@@ -98,7 +98,7 @@ const lightTheme = createTheme({
borderColor: paletteOptions.primary.main,
color: paletteOptions.primary.main,
backgroundColor: paletteOptions.background.default,
- width: "21.5rem",
+ // width: "21.5rem",
"&:hover": {
backgroundColor: paletteOptions.background.default,
borderColor: paletteOptions.primary.main,
diff --git a/src/theme/options.ts b/src/theme/options.ts
index eec9db680..2a3b52385 100644
--- a/src/theme/options.ts
+++ b/src/theme/options.ts
@@ -172,11 +172,6 @@ export const typographyOptions = {
fontSize: "1.4rem",
lineHeight: "2.6rem",
},
- // button: {
- // fontSize: "1.8rem",
- // fontWeight: 700,
- // textTransform: "capitalize",
- // },
}
export const boxShadowOptions = {
diff --git a/src/utils/common.ts b/src/utils/common.ts
index 7a6b90865..56f89ddab 100644
--- a/src/utils/common.ts
+++ b/src/utils/common.ts
@@ -27,10 +27,14 @@ export function requireEnv(entry) {
}
}
-export const generateExploreLink = (explorer, hash) => {
+export const generateTxLink = (explorer, hash) => {
return `${explorer}/tx/${hash}`
}
+export const generateContractLink = (explorer, address) => {
+ return `${explorer}/address/${address}`
+}
+
export const isProduction = requireEnv("REACT_APP_SCROLL_ENVIRONMENT") === requireEnv("REACT_APP_MAIN_ENVIRONMENT")
export const isValidEmail = (email: string): boolean => {
diff --git a/src/utils/storageKey.ts b/src/utils/storageKey.ts
index 232d06866..eb287dec5 100644
--- a/src/utils/storageKey.ts
+++ b/src/utils/storageKey.ts
@@ -9,6 +9,8 @@ export const BRIDGE_TOKEN_SYMBOL = "bridgeTokenSymbol"
export const BRIDGE_TRANSACTIONS = "bridgeTransactions"
+export const NFT_BRIDGE_TRANSACTIONS = "nftBridgeTransactions"
+
export const APP_VERSION = "appVersion"
export const BLOCK_NUMBERS = "blockNumbers"
diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx
index b55abf747..8408be586 100644
--- a/src/views/home/index.tsx
+++ b/src/views/home/index.tsx
@@ -265,7 +265,7 @@ const Home = () => {
Scroll is a team of passionate contributors around the globe. We value ideas and execution above all else. Join us today!
-