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/components/views/publication/components/EnsModal.tsx b/packages/app/src/components/views/publication/components/EnsModal.tsx index acdaa64a..b101e6ef 100644 --- a/packages/app/src/components/views/publication/components/EnsModal.tsx +++ b/packages/app/src/components/views/publication/components/EnsModal.tsx @@ -1,11 +1,12 @@ import { Button, CircularProgress, Grid, Modal, ModalProps, Typography, styled } from "@mui/material" -import React, { useEffect, useRef } from "react" +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 @@ -26,7 +27,8 @@ const EnsModal: React.FC = ({ publicationId, ...props }) => { const { generateTextRecord, setRecordMulticall, loading, transactionCompleted } = useENS() const { connector, chainId } = useWeb3React() const ref = useRef(null) - const { ensName } = useEnsContext() + const { ensNameList } = useEnsContext() + const [ensNameSelected, setEnsNameSelected] = useState("") useEffect(() => { if (transactionCompleted) { @@ -36,8 +38,8 @@ const EnsModal: React.FC = ({ publicationId, ...props }) => { const handleEnsRecord = async () => { const provider = await connector?.getProvider() - if (provider !== null && ensName && chainId) { - const textData = generateTextRecord(provider, publicationId) + if (provider !== null && ensNameSelected && chainId) { + const textData = generateTextRecord(provider, publicationId, ensNameSelected) textData && (await setRecordMulticall(provider, textData, chainId)) } } @@ -78,6 +80,16 @@ const EnsModal: React.FC = ({ publicationId, ...props }) => { + + { + setEnsNameSelected(e.value) + }} + /> + + @@ -90,7 +102,7 @@ const EnsModal: React.FC = ({ publicationId, ...props }) => { cursor: "pointer", }} > - tabula.gg/#/{ensName} + tabula.gg/#/{ensNameSelected ?? "yourENS"} . @@ -98,7 +110,7 @@ const EnsModal: React.FC = ({ publicationId, ...props }) => { - diff --git a/packages/app/src/components/views/publication/components/SettingSection.tsx b/packages/app/src/components/views/publication/components/SettingSection.tsx index 0683f931..f2eac736 100644 --- a/packages/app/src/components/views/publication/components/SettingSection.tsx +++ b/packages/app/src/components/views/publication/components/SettingSection.tsx @@ -26,6 +26,7 @@ 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" type Post = { title: string @@ -47,7 +48,6 @@ const publicationSchema = yup.object().shape({ export const SettingSection: React.FC = ({ couldDelete, couldEdit }) => { const { publicationSlug } = useParams<{ publicationSlug: string }>() const navigate = useNavigate() - const [pinning] = useLocalStorage("pinning", undefined) const [tags, setTags] = useState([]) const [loading, setLoading] = useState(false) @@ -61,7 +61,8 @@ export const SettingSection: React.FC = ({ couldDelete, co removePublicationImage, setRemovePublicationImage, } = usePublicationContext() - const { ensName } = useEnsContext() + const { ensNameList } = useEnsContext() + const { fetchNames } = useENS() const { executePublication, deletePublication } = usePoster() const { indexing: updateIndexing, @@ -80,6 +81,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 @@ -98,6 +104,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") @@ -241,7 +256,7 @@ export const SettingSection: React.FC = ({ couldDelete, co errorMsg={tags.length && tags.length >= 6 ? "Add up to 5 tags for your publication" : undefined} /> - {ensName && ( + {ensNameList && ( )} - {ensName && ( + {ensNameList && ( setOpenENSModal(false)} diff --git a/packages/app/src/services/ens/context/ens.context.tsx b/packages/app/src/services/ens/context/ens.context.tsx index d956103b..58eff157 100644 --- a/packages/app/src/services/ens/context/ens.context.tsx +++ b/packages/app/src/services/ens/context/ens.context.tsx @@ -1,18 +1,21 @@ 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} diff --git a/packages/app/src/services/ens/context/ens.types.ts b/packages/app/src/services/ens/context/ens.types.ts index f0ef5504..fed55bed 100644 --- a/packages/app/src/services/ens/context/ens.types.ts +++ b/packages/app/src/services/ens/context/ens.types.ts @@ -1,8 +1,11 @@ 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 = { diff --git a/packages/app/src/services/ens/hooks/useENS.ts b/packages/app/src/services/ens/hooks/useENS.ts index fd6ba219..c91d77db 100644 --- a/packages/app/src/services/ens/hooks/useENS.ts +++ b/packages/app/src/services/ens/hooks/useENS.ts @@ -5,6 +5,11 @@ 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 @@ -19,10 +24,28 @@ const ensImplementation = "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" // ENS: B 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] }, []) @@ -58,16 +81,19 @@ export const useENS = () => { } }, []) - const generateTextRecord = useCallback((provider: ethers.providers.ExternalProvider, publicationId: string) => { - try { - const web3Provider = new ethers.providers.Web3Provider(provider) - const contract = new ethers.Contract(ensRegistry, abiPublicResolver, web3Provider) - const node = "0x32a03d3aa475a2eac9dddc2da7fb8e45544e77a3e5657aa40fdf6b506f9ff896" // Default Data Node - return contract.interface.encodeFunctionData("setText", [node, "tabula", publicationId]) - } catch (e) { - console.log("ENS is not supported on this network") - } - }, []) + const generateTextRecord = useCallback( + (provider: ethers.providers.ExternalProvider, publicationId: string, ensName: string) => { + 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 + return contract.interface.encodeFunctionData("setText", [namehash, "tabula", publicationId]) + } catch (e) { + console.log("ENS is not supported on this network") + } + }, + [], + ) const setRecordMulticall = useCallback( async (provider: ethers.providers.ExternalProvider, textRecord: string, chainId: SupportedChainId) => { @@ -165,6 +191,7 @@ export const useENS = () => { checkIfIsController, checkIfIsOwner, setTextRecord, + fetchNames, loading, transactionCompleted, } 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..eaacb89c 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,17 @@ const getUrl = (chainId?: number) => { } } +const getENSUrl = (chainId?: number) => { + switch (chainId) { + case SupportedChainId.MAINNET: + return BASE_SUBGRAPH_URL + ENS_SUBGRAPH_MAINNET + case SupportedChainId.GOERLI: + return BASE_SUBGRAPH_URL + ENS_SUBGRAPH_GOERLI + default: + return BASE_SUBGRAPH_URL + ENS_SUBGRAPH_GOERLI + } +} + export const subgraphClient = (chainId?: number) => createClient({ url: getUrl(chainId), @@ -70,3 +89,12 @@ export const subgraphClient = (chainId?: number) => cache: "no-cache", }, }) + +export const ensSubgraphClient = (chainId?: number) => + createClient({ + url: getENSUrl(chainId), + exchanges: [...defaultExchanges], + fetchOptions: { + cache: "no-cache", + }, + })