diff --git a/packages/app/.env.example b/packages/app/.env.example index 506b3d57..651f50cc 100644 --- a/packages/app/.env.example +++ b/packages/app/.env.example @@ -8,6 +8,8 @@ REACT_APP_SUBGRAPH_SEPOLIA=auryn-macmillan/tabula-sepolia REACT_APP_SUBGRAPH_POLYGON=auryn-macmillan/tabula-polygon REACT_APP_SUBGRAPH_ARBITRUM=auryn-macmillan/tabula-arbitrum REACT_APP_SUBGRAPH_OPTIMISM=auryn-macmillan/tabula-optimism +REACT_APP_ENS_SUBGRAPH_MAINNET=ensdomains/ens +REACT_APP_ENS_SUBGRAPH_GOERLI=ensdomains/ensgoerli REACT_APP_IPFS_GATEWAY=https://ipfs.io/ipfs REACT_APP_SUBGRAPH_OPTIMISM_ON_GNOSIS_CHAIN=auryn-macmillan/tabula-optimism-on-gnosis-chain REACT_APP_INFURA_API_KEY= diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 551066e0..61ee945d 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -18,6 +18,7 @@ import { PosterProvider } from "./services/poster/context" import { WalletProvider } from "./connectors/WalletProvider" import { RedirectOldRoute } from "./components/commons/RedicrectOldRoute" import PreviewArticleView from "./components/views/publication/PreviewArticleView" +import { EnsProvider } from "./services/ens/context" const App: React.FC = () => { // the chainId should be from the publication if its present @@ -44,39 +45,41 @@ const App: React.FC = () => { - - - - - - {" "} - } /> - } /> - } /> - } /> - {/* Redirect old routes to new routes */} - } /> - } /> - } /> - } /> - } /> - } /> - {/* New routes */} - - } /> + + + + + + + {" "} + } /> + } /> + } /> + } /> + {/* Redirect old routes to new routes */} + } /> + } /> + } /> + } /> + } /> + } /> + {/* New routes */} + + } /> - } /> + } /> - } /> + } /> - } /> + } /> - } /> - - - - - + } /> + + + + + + diff --git a/packages/app/src/components/commons/NetworkModal.tsx b/packages/app/src/components/commons/NetworkModal.tsx new file mode 100644 index 00000000..66612f80 --- /dev/null +++ b/packages/app/src/components/commons/NetworkModal.tsx @@ -0,0 +1,112 @@ +import { Button, CircularProgress, Grid, Modal, ModalProps, Typography, styled } from "@mui/material" +import React, { useRef, useState } from "react" +import { palette, typography } from "../../theme" +import { ViewContainer } from "./ViewContainer" +import CloseIcon from "@mui/icons-material/Close" +import { useWeb3React } from "@web3-react/core" +import { useNotification } from "../../hooks/useNotification" + +type NetworkError = { + code: number + message: string + stack: string +} + +interface NetworkModalProps extends Omit {} +const ModalContainer = styled(ViewContainer)({ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + borderRadius: 8, + width: 648, + background: palette.whites[1000], + padding: 24, +}) + +const NetworkModal: React.FC = ({ open, onClose }) => { + const ref = useRef(null) + const openNotification = useNotification() + const { library } = useWeb3React() + const [loading, setLoading] = useState(false) + + const handleSwitch = async () => { + if (library) { + try { + setLoading(true) + await library.send("wallet_switchEthereumChain", [{ chainId: "0x1" }]) + onClose && onClose({}, "escapeKeyDown") + } catch (switchError: unknown) { + const error = switchError as NetworkError + setLoading(false) + openNotification({ + message: error.message, + variant: "error", + }) + if (error && error.code === 4902) { + setLoading(false) + try { + await library.send("wallet_addEthereumChain", [ + { + chainId: "0x1", + }, + ]) + } catch (addError) { + console.error(addError) + setLoading(false) + } + } + } + } + } + return ( + + + + + + + + Network Switch Required + + + + { + !loading && onClose && onClose({}, "escapeKeyDown") + }} + /> + + + + + + + +

+ You are currently connected to a network other than mainnet. To proceed with transactions involving + ENS domains, please switch to mainnet. +

+
+
+
+ + + + +
+
+
+ ) +} + +export default NetworkModal diff --git a/packages/app/src/components/commons/WalletBadge.tsx b/packages/app/src/components/commons/WalletBadge.tsx index 19cc0156..6ee75b64 100644 --- a/packages/app/src/components/commons/WalletBadge.tsx +++ b/packages/app/src/components/commons/WalletBadge.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useState } from "react" +import React, { useEffect } from "react" import { Avatar } from "@mui/material" import * as blockies from "blockies-ts" import { useNotification } from "../../hooks/useNotification" import { useWeb3React } from "@web3-react/core" -import { lookupAddress } from "../../services/ens" +import { useEnsContext } from "../../services/ens/context" +import useENS from "../../services/ens/hooks/useENS" type WalletBadgeProps = { copyable?: boolean @@ -14,10 +15,11 @@ type WalletBadgeProps = { } export const WalletBadge: React.FC = ({ address, hover, copyable }) => { + const { lookupAddress } = useENS() const avatarSrc = blockies.create({ seed: address.toLowerCase() }).toDataURL() const { connector, active, chainId } = useWeb3React() + const { setEnsName } = useEnsContext() - const [ensName, setEnsName] = useState() const openNotification = useNotification() useEffect(() => { @@ -25,14 +27,14 @@ export const WalletBadge: React.FC = ({ address, hover, copyab if (address && active) { const provider = await connector?.getProvider() if (provider != null) { - const ensName = await lookupAddress(provider, address) - setEnsName(ensName ?? undefined) + const ens = await lookupAddress(provider, address) + setEnsName(ens) } } } fetchData().catch(console.error) - }, [active, address, connector, ensName, chainId]) + }, [active, address, connector, chainId, setEnsName, lookupAddress]) const handleAddressClick = async () => { if (copyable) { diff --git a/packages/app/src/components/views/publication/components/EnsModal.tsx b/packages/app/src/components/views/publication/components/EnsModal.tsx new file mode 100644 index 00000000..aa0b7b65 --- /dev/null +++ b/packages/app/src/components/views/publication/components/EnsModal.tsx @@ -0,0 +1,123 @@ +import { Button, CircularProgress, Grid, Modal, ModalProps, Typography, styled } from "@mui/material" +import React, { useEffect, useRef, useState } from "react" +import { palette, typography } from "../../../../theme" +import { ViewContainer } from "../../../commons/ViewContainer" +import CloseIcon from "@mui/icons-material/Close" +import { useEnsContext } from "../../../../services/ens/context" +import { useWeb3React } from "@web3-react/core" +import useENS from "../../../../services/ens/hooks/useENS" +import { Dropdown } from "../../../commons/Dropdown" + +interface EnsModalProps extends Omit { + publicationId: string +} + +const ModalContainer = styled(ViewContainer)({ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + borderRadius: 8, + width: 648, + background: palette.whites[1000], + padding: 24, +}) + +const EnsModal: React.FC = ({ publicationId, ...props }) => { + const { setTextRecord, loading, transactionCompleted } = useENS() + const { connector, chainId } = useWeb3React() + const ref = useRef(null) + const { ensNameList } = useEnsContext() + const [ensNameSelected, setEnsNameSelected] = useState("") + + useEffect(() => { + if (transactionCompleted) { + props.onClose && props.onClose({}, "backdropClick") + } + }, [props, transactionCompleted]) + + const handleEnsRecord = async () => { + const provider = await connector?.getProvider() + if (provider !== null && ensNameSelected && chainId) { + await setTextRecord(provider, publicationId, ensNameSelected, chainId) + } + } + + return ( + { + if (!loading) { + props.onClose && props.onClose({}, "backdropClick") + } + }} + > + + + + + + + Linking Your Publication to Your ENS Name + + + + { + if (!loading) { + props.onClose && props.onClose({}, "escapeKeyDown") + } + }} + /> + + + + + + { + setEnsNameSelected(e.value) + }} + /> + + + + + + We're registering a record to your ENS name with the publication ID. This allows direct access to your + publication via{" "} + + tabula.gg/#/{ensNameSelected ?? "yourENS"} + + . + + + + + + + + + + + ) +} + +export default EnsModal diff --git a/packages/app/src/components/views/publication/components/SettingSection.tsx b/packages/app/src/components/views/publication/components/SettingSection.tsx index 7d709665..0db54ca5 100644 --- a/packages/app/src/components/views/publication/components/SettingSection.tsx +++ b/packages/app/src/components/views/publication/components/SettingSection.tsx @@ -7,7 +7,6 @@ import { Grid, InputLabel, TextField, - // Typography, } from "@mui/material" import React, { useEffect, useState } from "react" import { useForm, Controller } from "react-hook-form" @@ -24,6 +23,12 @@ import { CreatableSelect } from "../../../commons/CreatableSelect" import { CreateSelectOption } from "../../../../models/dropdown" import useLocalStorage from "../../../../hooks/useLocalStorage" import { Pinning, PinningService } from "../../../../models/pinning" +import { useEnsContext } from "../../../../services/ens/context" +import EnsModal from "./EnsModal" +import useENS from "../../../../services/ens/hooks/useENS" +import { useWeb3React } from "@web3-react/core" +import NetworkModal from "../../../commons/NetworkModal" +import { SupportedChainId } from "../../../../constants/chain" type Post = { title: string @@ -45,9 +50,12 @@ const publicationSchema = yup.object().shape({ export const SettingSection: React.FC = ({ couldDelete, couldEdit }) => { const { publicationSlug } = useParams<{ publicationSlug: string }>() const navigate = useNavigate() + const { chainId } = useWeb3React() const [pinning] = useLocalStorage("pinning", undefined) const [tags, setTags] = useState([]) const [loading, setLoading] = useState(false) + const [openENSModal, setOpenENSModal] = useState(false) + const [openNetworkModal, setOpenNetworkModal] = useState(false) const [deleteLoading, setDeleteLoading] = useState(false) const { publication, @@ -57,6 +65,8 @@ export const SettingSection: React.FC = ({ couldDelete, co removePublicationImage, setRemovePublicationImage, } = usePublicationContext() + const { ensNameList } = useEnsContext() + const { fetchNames } = useENS() const { executePublication, deletePublication } = usePoster() const { indexing: updateIndexing, @@ -75,6 +85,11 @@ export const SettingSection: React.FC = ({ couldDelete, co resolver: yupResolver(publicationSchema), }) + useEffect(() => { + fetchNames() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + useEffect(() => { saveIsEditingPublication(true) // returned function will be called on component unmount @@ -93,6 +108,15 @@ export const SettingSection: React.FC = ({ couldDelete, co } }, [loading, publication, setCurrentTimestamp, setValue]) + useEffect(() => { + if (publication && !loading && publication.lastUpdated) { + setValue("title", publication.title) + setValue("description", publication.description || "") + setTags(publication.tags || []) + setCurrentTimestamp(parseInt(publication.lastUpdated)) + } + }, [loading, publication, setCurrentTimestamp, setValue]) + useEffect(() => { if (redirect) { navigate("../publications") @@ -173,6 +197,15 @@ export const SettingSection: React.FC = ({ couldDelete, co } } + const handleEns = () => { + if (chainId) { + if ([SupportedChainId.MAINNET, SupportedChainId.GOERLI, SupportedChainId.SEPOLIA].includes(chainId)) { + return setOpenENSModal(true) + } + return setOpenNetworkModal(true) + } + } + return ( @@ -236,6 +269,28 @@ export const SettingSection: React.FC = ({ couldDelete, co errorMsg={tags.length && tags.length >= 6 ? "Add up to 5 tags for your publication" : undefined} /> + {ensNameList && ( + + + + )} + setOpenNetworkModal(false)} /> + {ensNameList && ( + setOpenENSModal(false)} + publicationId={publication?.id ?? ""} + /> + )} {couldDelete && ( diff --git a/packages/app/src/services/ens.ts b/packages/app/src/services/ens.ts deleted file mode 100644 index ad23c35e..00000000 --- a/packages/app/src/services/ens.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ethers } from "ethers" -import { INFURA_KEY } from "../connectors" - -const ensRegistry = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" // ENS: Registry with Fallback (singleton same address on different chains) -const ensImplementation = "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" // ENS: Base Registrar Implementation (singleton same address on different chains) - -const abiPublicResolver = ["function setText(bytes32 node, string calldata key, string calldata value) external"] - -const abiRegistry = [ - "function owner(bytes32 node) external view returns (address)", - "function resolver(bytes32 node) external view returns (address)", -] - -const abiImplementation = ["function ownerOf(uint256 tokenId) public view returns (address owner)"] - -const getTextRecordContentInfura = async (ensName: string, textRecordKey: string) => { - const provider = new ethers.providers.InfuraProvider("mainnet", INFURA_KEY) - const resolver = await provider.getResolver(ensName) - return resolver?.getText(textRecordKey) -} - -export const getTextRecordContent = async ( - ensName: string, - textRecordKey: string, - provider?: ethers.providers.BaseProvider, -) => { - // no connected wallet - if (provider == null) { - return getTextRecordContentInfura(ensName, textRecordKey) - } - - try { - // try to use connected provider - const resolver = await provider.getResolver(ensName) - return resolver?.getText(textRecordKey) - } catch (e) { - // fallback to infura - // we are here if ENS is not supported on the current selected network - return getTextRecordContentInfura(ensName, textRecordKey) - } -} - -export const lookupAddress = async (provider: any, address: string) => { - try { - const web3Provider = new ethers.providers.Web3Provider(provider) - return await web3Provider.lookupAddress(address) - } catch (e) { - console.log("ENS is not supported on this network") - } -} - -/** - * This only works for ENS names using a resolver that conforms to the `abiPublicResolver` (like the PublicResolver). - */ -export const setTextRecord = async ( - provider: ethers.providers.BaseProvider, - ensName: string, - key: string, - content: string, -): Promise => { - const ensRegistryContract = new ethers.Contract(ensRegistry, abiRegistry, provider) - const nameHash = ethers.utils.namehash(ensName) - const ensResolver = await ensRegistryContract.resolver(nameHash) - const ensResolverContract = new ethers.Contract(ensResolver, abiPublicResolver, provider) - return await ensResolverContract.setText(nameHash, key, content) -} - -// the owner of the NFT -export const checkIfIsOwner = async (provider: ethers.providers.Provider, ensName: string, address: string) => { - const BigNumber = ethers.BigNumber - const name = ensName.split(".")[0] // only supports toplevel - const labelHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(name)) - const tokenId = BigNumber.from(labelHash).toString() - const ensImplementationContract = new ethers.Contract(ensImplementation, abiImplementation, provider) - const nftOwner = await ensImplementationContract.ownerOf(tokenId) - return ethers.utils.getAddress(nftOwner) === ethers.utils.getAddress(address) -} - -export const checkIfIsController = async (provider: ethers.providers.Provider, ensName: string, address: string) => { - const ensRegistryContract = new ethers.Contract(ensRegistry, abiRegistry, provider) - const nameHash = ethers.utils.namehash(ensName) - const owner = await ensRegistryContract.owner(nameHash) - - return ethers.utils.getAddress(address) === ethers.utils.getAddress(owner) -} diff --git a/packages/app/src/services/ens/context/ens.context.tsx b/packages/app/src/services/ens/context/ens.context.tsx new file mode 100644 index 00000000..58eff157 --- /dev/null +++ b/packages/app/src/services/ens/context/ens.context.tsx @@ -0,0 +1,26 @@ +import { useState } from "react" +import { createGenericContext } from "../../../utils/create-generic-context" +import { EnsContextType, EnsProviderProps } from "./ens.types" +import { DropdownOption } from "../../../models/dropdown" + +const [useEnsContext, EnsContextProvider] = createGenericContext() + +const EnsProvider = ({ children }: EnsProviderProps) => { + const [ensName, setEnsName] = useState(undefined) + const [ensNameList, setEnsNameList] = useState([]) + + return ( + + {children} + + ) +} + +export { useEnsContext, EnsProvider } diff --git a/packages/app/src/services/ens/context/ens.types.ts b/packages/app/src/services/ens/context/ens.types.ts new file mode 100644 index 00000000..fed55bed --- /dev/null +++ b/packages/app/src/services/ens/context/ens.types.ts @@ -0,0 +1,13 @@ +import { ReactNode } from "react" +import { DropdownOption } from "../../../models/dropdown" + +export type EnsContextType = { + ensName: string | undefined | null + setEnsName: (value: string | undefined | null) => void + ensNameList: DropdownOption[] + setEnsNameList: (value: DropdownOption[]) => void +} + +export type EnsProviderProps = { + children: ReactNode +} diff --git a/packages/app/src/services/ens/context/index.ts b/packages/app/src/services/ens/context/index.ts new file mode 100644 index 00000000..ea83870c --- /dev/null +++ b/packages/app/src/services/ens/context/index.ts @@ -0,0 +1,2 @@ +export * from "./ens.context" +export * from "./ens.types" diff --git a/packages/app/src/services/ens/contracts/abi.ts b/packages/app/src/services/ens/contracts/abi.ts new file mode 100644 index 00000000..4512877f --- /dev/null +++ b/packages/app/src/services/ens/contracts/abi.ts @@ -0,0 +1,11 @@ +export const abiPublicResolver = [ + "function setText(bytes32 node, string calldata key, string calldata value) external", + "function multicall(bytes[] calldata data) external", +] + +export const abiRegistry = [ + "function owner(bytes32 node) external view returns (address)", + "function resolver(bytes32 node) external view returns (address)", +] + +export const abiImplementation = ["function ownerOf(uint256 tokenId) public view returns (address owner)"] diff --git a/packages/app/src/services/ens/hooks/useENS.ts b/packages/app/src/services/ens/hooks/useENS.ts new file mode 100644 index 00000000..dfdc997b --- /dev/null +++ b/packages/app/src/services/ens/hooks/useENS.ts @@ -0,0 +1,176 @@ +import { useState, useCallback } from "react" +import { ethers } from "ethers" +import { SupportedChainId, chainParameters } from "../../../constants/chain" +import { INFURA_KEY } from "../../../connectors" +import { abiImplementation, abiPublicResolver, abiRegistry } from "../contracts/abi" +import { useNotification } from "../../../hooks/useNotification" +import { TransactionReceipt } from "@ethersproject/providers" +import { GET_ENS_NAMES_QUERY } from "../queries" +import { ensSubgraphClient } from "../../graphql" +import { useWeb3React } from "@web3-react/core" +import { DropdownOption } from "../../../models/dropdown" +import { useEnsContext } from "../context" + +// Addresses obtained from: +// https://discuss.ens.domains/t/namewrapper-updates-including-testnet-deployment-addresses/14505 +const publicResolvers: { [key in SupportedChainId]?: string } = { + [SupportedChainId.SEPOLIA]: "0x8FADE66B79cC9f707aB26799354482EB93a5B7dD", + [SupportedChainId.MAINNET]: "0x231b0ee14048e9dccd1d247744d114a4eb5e8e63", + [SupportedChainId.GOERLI]: "0xd7a4F6473f32aC2Af804B3686AE8F1932bC35750", +} + +const ensRegistry = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" // ENS: Registry with Fallback (singleton, same address on different chains) +const ensImplementation = "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" // ENS: Base Registrar Implementation (singleton, same address on different chains) + +export const useENS = () => { + const openNotification = useNotification() + const { chainId, account } = useWeb3React() + const { setEnsNameList } = useEnsContext() + + const client = ensSubgraphClient(chainId) + const [loading, setLoading] = useState(false) + const [transactionCompleted, setTransactionCompleted] = useState(false) + + const fetchNames = async () => { + client + .query(GET_ENS_NAMES_QUERY, { id: account?.toLowerCase() }) + .toPromise() + .then((result) => { + const data = result.data + if (data.account && data.account.wrappedDomains.length) { + const list: DropdownOption[] = data.account.wrappedDomains.map((ens: { domain: { name: string } }) => { + return { label: ens.domain.name, value: ens.domain.name } + }) + setEnsNameList(list) + } + }) + } + + const getPublicResolverAddress = useCallback((chainId: SupportedChainId): string | undefined => { + return publicResolvers[chainId] + }, []) + + const getTextRecordContentInfura = useCallback(async (ensName: string, textRecordKey: string) => { + const provider = new ethers.providers.InfuraProvider("mainnet", INFURA_KEY) + const resolver = await provider.getResolver(ensName) + return resolver?.getText(textRecordKey) + }, []) + + const getTextRecordContent = useCallback( + async (ensName: string, textRecordKey: string, provider?: ethers.providers.BaseProvider) => { + if (!provider) { + return getTextRecordContentInfura(ensName, textRecordKey) + } + + try { + const resolver = await provider.getResolver(ensName) + return resolver?.getText(textRecordKey) + } catch (e) { + return getTextRecordContentInfura(ensName, textRecordKey) + } + }, + [getTextRecordContentInfura], + ) + + const lookupAddress = useCallback(async (provider: ethers.providers.ExternalProvider, address: string) => { + try { + const web3Provider = new ethers.providers.Web3Provider(provider) + return await web3Provider.lookupAddress(address) + } catch (e) { + console.log("ENS is not supported on this network") + } + }, []) + + const setTextRecord = useCallback( + async ( + provider: ethers.providers.ExternalProvider, + publicationId: string, + ensName: string, + chainId: SupportedChainId, + ) => { + const parameters = chainParameters(chainId) + const URL = parameters ? parameters.blockExplorerUrls[0] : "https://goerli.etherscan.io/tx/" + setLoading(true) + const publicResolver = getPublicResolverAddress(chainId) + if (!publicResolver) { + openNotification({ + message: "Public resolver not found for the selected chain.", + variant: "error", + }) + setLoading(false) + return + } + try { + const web3Provider = new ethers.providers.Web3Provider(provider) + const contract = new ethers.Contract(ensRegistry, abiPublicResolver, web3Provider) + const namehash = ethers.utils.namehash(ensName) // Calculate namehash of the ENS name + const signer = web3Provider.getSigner() + const data = contract.interface.encodeFunctionData("setText", [namehash, "tabula", publicationId]) + if (!data) { + openNotification({ + message: "Failed to encode data for setText.", + variant: "error", + }) + setLoading(false) + return + } + const tx = await signer.sendTransaction({ + to: publicResolver, + data: data, + }) + const receipt: TransactionReceipt = await tx.wait() + + openNotification({ + message: "Transaction completed successfully!", + variant: "success", + detailsLink: `${URL}tx/${receipt.transactionHash}`, + }) + setTransactionCompleted(true) + } catch (e) { + console.log("error", e) + setLoading(false) + + openNotification({ + message: "ENS is not supported on this network or an error occurred.", + variant: "error", + }) + } finally { + setLoading(false) + } + }, + [getPublicResolverAddress, openNotification], + ) + + const checkIfIsOwner = useCallback(async (provider: ethers.providers.Provider, ensName: string, address: string) => { + const BigNumber = ethers.BigNumber + const name = ensName.split(".")[0] + const labelHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(name)) + const tokenId = BigNumber.from(labelHash).toString() + const ensImplementationContract = new ethers.Contract(ensImplementation, abiImplementation, provider) + const nftOwner = await ensImplementationContract.ownerOf(tokenId) + return ethers.utils.getAddress(nftOwner) === ethers.utils.getAddress(address) + }, []) + + const checkIfIsController = useCallback( + async (provider: ethers.providers.Provider, ensName: string, address: string) => { + const ensRegistryContract = new ethers.Contract(ensRegistry, abiRegistry, provider) + const nameHash = ethers.utils.namehash(ensName) + const owner = await ensRegistryContract.owner(nameHash) + return ethers.utils.getAddress(address) === ethers.utils.getAddress(owner) + }, + [], + ) + + return { + getTextRecordContent, + lookupAddress, + checkIfIsController, + checkIfIsOwner, + setTextRecord, + fetchNames, + loading, + transactionCompleted, + } +} + +export default useENS diff --git a/packages/app/src/services/ens/queries.ts b/packages/app/src/services/ens/queries.ts new file mode 100644 index 00000000..80391377 --- /dev/null +++ b/packages/app/src/services/ens/queries.ts @@ -0,0 +1,23 @@ +import { gql } from "urql" + +export const GET_ENS_NAMES_QUERY = gql` + query getNames($id: String!) { + account(id: $id) { + wrappedDomains(first: 1000) { + expiryDate + fuses + domain { + id + labelName + labelhash + name + isMigrated + parent { + name + id + } + } + } + } + } +` diff --git a/packages/app/src/services/graphql.ts b/packages/app/src/services/graphql.ts index 6980372c..34ae7423 100644 --- a/packages/app/src/services/graphql.ts +++ b/packages/app/src/services/graphql.ts @@ -28,6 +28,12 @@ if (!process.env.REACT_APP_SUBGRAPH_OPTIMISM) { if (!process.env.REACT_APP_SUBGRAPH_OPTIMISM_ON_GNOSIS_CHAIN) { throw new Error("REACT_APP_SUBGRAPH_OPTIMISM_ON_GNOSIS_CHAIN is not set") } +if (!process.env.REACT_APP_ENS_SUBGRAPH_MAINNET) { + throw new Error("REACT_APP_ENS_SUBGRAPH_MAINNET is not set") +} +if (!process.env.REACT_APP_ENS_SUBGRAPH_GOERLI) { + throw new Error("REACT_APP_ENS_SUBGRAPH_GOERLI is not set") +} const BASE_SUBGRAPH_URL = process.env.REACT_APP_SUBGRAPH_BASE_URL const SUBGRAPH_GNOSIS_CHAIN = process.env.REACT_APP_SUBGRAPH_GNOSIS_CHAIN @@ -38,6 +44,8 @@ const SUBGRAPH_POLYGON = process.env.REACT_APP_SUBGRAPH_POLYGON const SUBGRAPH_ARBITRUM = process.env.REACT_APP_SUBGRAPH_ARBITRUM const SUBGRAPH_OPTIMISM = process.env.REACT_APP_SUBGRAPH_OPTIMISM const SUBGRAPH_OPTIMISM_ON_GNOSIS_CHAIN = process.env.REACT_APP_SUBGRAPH_OPTIMISM_ON_GNOSIS_CHAIN +const ENS_SUBGRAPH_MAINNET = process.env.REACT_APP_ENS_SUBGRAPH_MAINNET +const ENS_SUBGRAPH_GOERLI = process.env.REACT_APP_ENS_SUBGRAPH_GOERLI const getUrl = (chainId?: number) => { switch (chainId) { @@ -62,6 +70,22 @@ const getUrl = (chainId?: number) => { } } +const getENSUrl = (chainId?: number) => { + switch (chainId) { + case SupportedChainId.MAINNET: + case SupportedChainId.GNOSIS_CHAIN: + case SupportedChainId.POLYGON: + case SupportedChainId.OPTIMISM: + case SupportedChainId.OPTIMISM_ON_GNOSIS_CHAIN: + case SupportedChainId.ARBITRUM: + return BASE_SUBGRAPH_URL + ENS_SUBGRAPH_MAINNET + case SupportedChainId.GOERLI: + return BASE_SUBGRAPH_URL + ENS_SUBGRAPH_GOERLI + default: + return BASE_SUBGRAPH_URL + ENS_SUBGRAPH_MAINNET + } +} + export const subgraphClient = (chainId?: number) => createClient({ url: getUrl(chainId), @@ -70,3 +94,12 @@ export const subgraphClient = (chainId?: number) => cache: "no-cache", }, }) + +export const ensSubgraphClient = (chainId?: number) => + createClient({ + url: getENSUrl(chainId), + exchanges: [...defaultExchanges], + fetchOptions: { + cache: "no-cache", + }, + }) diff --git a/packages/app/src/services/publications/contexts/publication.context.tsx b/packages/app/src/services/publications/contexts/publication.context.tsx index d0ccd40c..6a78e91b 100644 --- a/packages/app/src/services/publications/contexts/publication.context.tsx +++ b/packages/app/src/services/publications/contexts/publication.context.tsx @@ -2,13 +2,13 @@ import { ethers } from "ethers" import { useState } from "react" import { Permission, Publication } from "../../../models/publication" import { createGenericContext } from "../../../utils/create-generic-context" -import { getTextRecordContent } from "../../ens" import { PublicationContextType, PublicationProviderProps } from "./publication.types" - +import useENS from "../../ens/hooks/useENS" const [usePublicationContext, PublicationContextProvider] = createGenericContext() const PublicationProvider = ({ children }: PublicationProviderProps) => { + const { getTextRecordContent } = useENS() const [currentPath, setCurrentPath] = useState(undefined) const [publications, setPublications] = useState(undefined) const [publication, setPublication] = useState(undefined) @@ -17,7 +17,6 @@ const PublicationProvider = ({ children }: PublicationProviderProps) => { const [draftPublicationImage, setDraftPublicationImage] = useState(undefined) const [loading, setLoading] = useState(false) const [ipfsLoading, setIpfsLoading] = useState(false) - const [slugToPublicationId, setSlugToPublicationId] = useState<{ [key: string]: string }>({}) const [publicationAvatar, setPublicationAvatar] = useState<{ publicationId: string; uri: string } | undefined>( undefined,