mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
ENS names (#860)
* prototype getting ENS names * get ENS name with subgraph * ENS name for publisher line * inject ENS name in profile page * refactor to cover all use cases for profile URLs * fixes for switching between own and other profiles * remove testing ENS libraries * more cleanup * any solves everything * build fix * more profile switching tweaks * link publisher line to ens name * another profile switching fix * show ENS link in meta line
This commit is contained in:
parent
15a29bcb01
commit
212865110e
20
package-lock.json
generated
20
package-lock.json
generated
@ -86,7 +86,6 @@
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/loadable__component": "^5.13.1",
|
||||
"@types/lodash.debounce": "^4.0.3",
|
||||
@ -10515,16 +10514,6 @@
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/classnames": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
|
||||
"deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"classnames": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/clipboard": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz",
|
||||
@ -67020,15 +67009,6 @@
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"@types/classnames": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
|
||||
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"classnames": "*"
|
||||
}
|
||||
},
|
||||
"@types/clipboard": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz",
|
||||
|
@ -101,7 +101,6 @@
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@types/chart.js": "^2.9.32",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/loadable__component": "^5.13.1",
|
||||
"@types/lodash.debounce": "^4.0.3",
|
||||
|
@ -8,6 +8,7 @@ import { accountTruncate } from '../../../utils/web3'
|
||||
import axios from 'axios'
|
||||
import Add from './Add'
|
||||
import { useWeb3 } from '../../../providers/Web3'
|
||||
import { getEnsName } from '../../../utils/ens'
|
||||
|
||||
const cx = classNames.bind(styles)
|
||||
|
||||
@ -20,27 +21,34 @@ export default function Publisher({
|
||||
minimal?: boolean
|
||||
className?: string
|
||||
}): ReactElement {
|
||||
const { networkId, accountId } = useWeb3()
|
||||
const { accountId } = useWeb3()
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
const [name, setName] = useState<string>()
|
||||
const [name, setName] = useState(accountTruncate(account))
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
|
||||
const showAdd = account === accountId && !profile
|
||||
|
||||
useEffect(() => {
|
||||
if (!account) return
|
||||
|
||||
setName(accountTruncate(account))
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
async function get3Box() {
|
||||
async function getExternalName() {
|
||||
// ENS
|
||||
const accountEns = await getEnsName(account)
|
||||
if (accountEns) {
|
||||
setAccountEns(accountEns)
|
||||
setName(accountEns)
|
||||
}
|
||||
|
||||
// 3box
|
||||
const profile = await get3BoxProfile(account, source.token)
|
||||
if (!profile) return
|
||||
|
||||
setProfile(profile)
|
||||
const { name, emoji } = profile
|
||||
name && setName(`${emoji || ''} ${name}`)
|
||||
}
|
||||
get3Box()
|
||||
getExternalName()
|
||||
|
||||
return () => {
|
||||
source.cancel()
|
||||
@ -58,7 +66,10 @@ export default function Publisher({
|
||||
name
|
||||
) : (
|
||||
<>
|
||||
<Link to={`/profile/${account}`} title="Show profile page.">
|
||||
<Link
|
||||
to={`/profile/${accountEns || account}`}
|
||||
title="Show profile page."
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
{showAdd && <Add />}
|
||||
|
@ -9,7 +9,7 @@ import Blockies from '../../atoms/Blockies'
|
||||
// Forward ref for Tippy.js
|
||||
// eslint-disable-next-line
|
||||
const Account = React.forwardRef((props, ref: any) => {
|
||||
const { accountId, web3Modal, connect } = useWeb3()
|
||||
const { accountId, accountEns, web3Modal, connect } = useWeb3()
|
||||
|
||||
async function handleActivation(e: FormEvent<HTMLButtonElement>) {
|
||||
// prevent accidentially submitting a form the button might be in
|
||||
@ -32,7 +32,7 @@ const Account = React.forwardRef((props, ref: any) => {
|
||||
>
|
||||
<Blockies accountId={accountId} />
|
||||
<span className={styles.address} title={accountId}>
|
||||
{accountTruncate(accountId)}
|
||||
{accountTruncate(accountEns || accountId)}
|
||||
</span>
|
||||
<Caret aria-hidden="true" className={styles.caret} />
|
||||
</button>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
import { accountTruncate } from '../../../../utils/web3'
|
||||
import ExplorerLink from '../../../atoms/ExplorerLink'
|
||||
import NetworkName from '../../../atoms/NetworkName'
|
||||
import jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
|
||||
@ -40,12 +39,13 @@ export default function Account({
|
||||
</figure>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.name}>
|
||||
{profile?.name || accountTruncate(accountId)}
|
||||
</h3>
|
||||
<h3 className={styles.name}>{profile?.name}</h3>
|
||||
{accountId && (
|
||||
<code className={styles.accountId}>
|
||||
{accountId} <Copy text={accountId} />
|
||||
<code
|
||||
className={styles.accountId}
|
||||
title={profile?.accountEns ? accountId : null}
|
||||
>
|
||||
{profile?.accountEns || accountId} <Copy text={accountId} />
|
||||
</code>
|
||||
)}
|
||||
<p>
|
||||
|
@ -11,13 +11,9 @@ const isDescriptionTextClamped = () => {
|
||||
if (el) return el.scrollHeight > el.clientHeight
|
||||
}
|
||||
|
||||
const Link3Box = ({ accountId, text }: { accountId: string; text: string }) => {
|
||||
const LinkExternal = ({ url, text }: { url: string; text: string }) => {
|
||||
return (
|
||||
<a
|
||||
href={`https://www.3box.io/${accountId}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
{text}
|
||||
</a>
|
||||
)
|
||||
@ -46,7 +42,10 @@ export default function AccountHeader({
|
||||
<Markdown text={profile?.description} className={styles.description} />
|
||||
{isDescriptionTextClamped() ? (
|
||||
<span className={styles.more} onClick={toogleShowMore}>
|
||||
<Link3Box accountId={accountId} text="Read more on 3box" />
|
||||
<LinkExternal
|
||||
url={`https://www.3box.io/${accountId}`}
|
||||
text="Read more on 3box"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
@ -56,7 +55,20 @@ export default function AccountHeader({
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
Profile data from <Link3Box accountId={accountId} text="3Box Hub" />
|
||||
Profile data from{' '}
|
||||
{profile?.accountEns && (
|
||||
<>
|
||||
<LinkExternal
|
||||
url={`https://app.ens.domains/name/${profile.accountEns}`}
|
||||
text="ENS"
|
||||
/>{' '}
|
||||
&{' '}
|
||||
</>
|
||||
)}
|
||||
<LinkExternal
|
||||
url={`https://www.3box.io/${accountId}`}
|
||||
text="3Box Hub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -6,6 +6,7 @@ export interface ProfileLink {
|
||||
export interface Profile {
|
||||
did?: string
|
||||
name?: string
|
||||
accountEns?: string
|
||||
description?: string
|
||||
emoji?: string
|
||||
image?: string
|
||||
|
@ -1,25 +1,63 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import Page from '../../components/templates/Page'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
import { graphql, PageProps, navigate } from 'gatsby'
|
||||
import ProfilePage from '../../components/pages/Profile'
|
||||
import { accountTruncate } from '../../utils/web3'
|
||||
import { useWeb3 } from '../../providers/Web3'
|
||||
import ProfileProvider from '../../providers/Profile'
|
||||
import { getEnsAddress, getEnsName } from '../../utils/ens'
|
||||
import ethereumAddress from 'ethereum-address'
|
||||
|
||||
export default function PageGatsbyProfile(props: PageProps): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
const { accountId, accountEns } = useWeb3()
|
||||
const [finalAccountId, setFinalAccountId] = useState<string>()
|
||||
const [finalAccountEns, setFinalAccountEns] = useState<string>()
|
||||
|
||||
// Have accountId in path take over, if not present fall back to web3
|
||||
useEffect(() => {
|
||||
const pathAccountId = props.location.pathname.split('/')[2]
|
||||
const finalAccountId = pathAccountId || accountId
|
||||
setFinalAccountId(finalAccountId)
|
||||
}, [props.location.pathname, accountId])
|
||||
async function init() {
|
||||
if (!props?.location?.pathname) return
|
||||
|
||||
// Path is root /profile, have web3 take over
|
||||
if (props.location.pathname === '/profile') {
|
||||
setFinalAccountEns(accountEns)
|
||||
setFinalAccountId(accountId)
|
||||
return
|
||||
}
|
||||
|
||||
const pathAccount = props.location.pathname.split('/')[2]
|
||||
|
||||
// Path has ETH addreess
|
||||
if (ethereumAddress.isAddress(pathAccount)) {
|
||||
const finalAccountId = pathAccount || accountId
|
||||
setFinalAccountId(finalAccountId)
|
||||
|
||||
const accountEns = await getEnsName(finalAccountId)
|
||||
if (!accountEns) return
|
||||
setFinalAccountEns(accountEns)
|
||||
} else {
|
||||
// Path has ENS name
|
||||
setFinalAccountEns(pathAccount)
|
||||
const resolvedAccountId = await getEnsAddress(pathAccount)
|
||||
setFinalAccountId(resolvedAccountId)
|
||||
}
|
||||
}
|
||||
init()
|
||||
}, [props.location.pathname, accountId, accountEns])
|
||||
|
||||
// Replace pathname with ENS name if present
|
||||
useEffect(() => {
|
||||
if (!finalAccountEns || props.location.pathname === '/profile') return
|
||||
|
||||
const newProfilePath = `/profile/${finalAccountEns}`
|
||||
// make sure we only replace path once
|
||||
if (newProfilePath !== props.location.pathname)
|
||||
navigate(newProfilePath, { replace: true })
|
||||
}, [props.location, finalAccountEns, accountId])
|
||||
|
||||
return (
|
||||
<Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader>
|
||||
<ProfileProvider accountId={finalAccountId}>
|
||||
<ProfileProvider accountId={finalAccountId} accountEns={finalAccountEns}>
|
||||
<ProfilePage accountId={finalAccountId} />
|
||||
</ProfileProvider>
|
||||
</Page>
|
||||
|
@ -42,9 +42,11 @@ const refreshInterval = 10000 // 10 sec.
|
||||
|
||||
function ProfileProvider({
|
||||
accountId,
|
||||
accountEns,
|
||||
children
|
||||
}: {
|
||||
accountId: string
|
||||
accountEns: string
|
||||
children: ReactNode
|
||||
}): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
@ -62,18 +64,19 @@ function ProfileProvider({
|
||||
}, [accountId])
|
||||
|
||||
//
|
||||
// 3Box
|
||||
// User profile: ENS + 3Box
|
||||
//
|
||||
const [profile, setProfile] = useState<Profile>({
|
||||
name: accountTruncate(accountId),
|
||||
image: null,
|
||||
description: null,
|
||||
links: null
|
||||
})
|
||||
const [profile, setProfile] = useState<Profile>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountEns) return
|
||||
Logger.log(`[profile] ENS name found for ${accountId}:`, accountEns)
|
||||
}, [accountId, accountEns])
|
||||
|
||||
useEffect(() => {
|
||||
const clearedProfile: Profile = {
|
||||
name: null,
|
||||
accountEns: null,
|
||||
image: null,
|
||||
description: null,
|
||||
links: null
|
||||
@ -86,7 +89,9 @@ function ProfileProvider({
|
||||
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
async function getInfoFrom3Box() {
|
||||
async function getInfo() {
|
||||
setProfile({ name: accountEns || accountTruncate(accountId), accountEns })
|
||||
|
||||
const profile3Box = await get3BoxProfile(
|
||||
accountId,
|
||||
cancelTokenSource.token
|
||||
@ -100,19 +105,22 @@ function ProfileProvider({
|
||||
description,
|
||||
links
|
||||
}
|
||||
setProfile(newProfile)
|
||||
setProfile((prevState) => ({
|
||||
...prevState,
|
||||
...newProfile
|
||||
}))
|
||||
Logger.log('[profile] Found and set 3box profile.', newProfile)
|
||||
} else {
|
||||
setProfile(clearedProfile)
|
||||
// setProfile(clearedProfile)
|
||||
Logger.log('[profile] No 3box profile found.')
|
||||
}
|
||||
}
|
||||
getInfoFrom3Box()
|
||||
getInfo()
|
||||
|
||||
return () => {
|
||||
cancelTokenSource.cancel()
|
||||
}
|
||||
}, [accountId, isEthAddress])
|
||||
}, [accountId, accountEns, isEthAddress])
|
||||
|
||||
//
|
||||
// POOL SHARES
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
getNetworkDataById,
|
||||
getNetworkDisplayName
|
||||
} from '../utils/web3'
|
||||
import { graphql } from 'gatsby'
|
||||
import { getEnsName } from '../utils/ens'
|
||||
import { UserBalance } from '../@types/TokenBalance'
|
||||
import { getOceanBalance } from '../utils/ocean'
|
||||
import useNetworkMetadata from '../hooks/useNetworkMetadata'
|
||||
@ -29,6 +29,7 @@ interface Web3ProviderValue {
|
||||
web3Modal: Web3Modal
|
||||
web3ProviderInfo: IProviderInfo
|
||||
accountId: string
|
||||
accountEns: string
|
||||
balance: UserBalance
|
||||
networkId: number
|
||||
chainId: number
|
||||
@ -84,26 +85,6 @@ export const web3ModalOpts = {
|
||||
|
||||
const refreshInterval = 20000 // 20 sec.
|
||||
|
||||
const networksQuery = graphql`
|
||||
query {
|
||||
allNetworksMetadataJson {
|
||||
edges {
|
||||
node {
|
||||
chain
|
||||
network
|
||||
networkId
|
||||
chainId
|
||||
nativeCurrency {
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Web3Context = createContext({} as Web3ProviderValue)
|
||||
|
||||
function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
@ -120,6 +101,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
const [block, setBlock] = useState<number>()
|
||||
const [isTestnet, setIsTestnet] = useState<boolean>()
|
||||
const [accountId, setAccountId] = useState<string>()
|
||||
const [accountEns, setAccountEns] = useState<string>()
|
||||
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
|
||||
const [balance, setBalance] = useState<UserBalance>({
|
||||
eth: '0',
|
||||
@ -181,6 +163,27 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
}
|
||||
}, [accountId, networkId, web3])
|
||||
|
||||
// -----------------------------------
|
||||
// Helper: Get user ENS name
|
||||
// -----------------------------------
|
||||
const getUserEnsName = useCallback(async () => {
|
||||
if (!accountId) return
|
||||
|
||||
try {
|
||||
// const accountEns = await getEnsNameWithWeb3(
|
||||
// accountId,
|
||||
// web3Provider,
|
||||
// `${networkId}`
|
||||
// )
|
||||
const accountEns = await getEnsName(accountId)
|
||||
setAccountEns(accountEns)
|
||||
accountEns &&
|
||||
Logger.log(`[web3] ENS name found for ${accountId}:`, accountEns)
|
||||
} catch (error) {
|
||||
Logger.error('[web3] Error: ', error.message)
|
||||
}
|
||||
}, [accountId])
|
||||
|
||||
// -----------------------------------
|
||||
// Create initial Web3Modal instance
|
||||
// -----------------------------------
|
||||
@ -229,6 +232,13 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
}
|
||||
}, [getUserBalance])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set user ENS name
|
||||
// -----------------------------------
|
||||
useEffect(() => {
|
||||
getUserEnsName()
|
||||
}, [getUserEnsName])
|
||||
|
||||
// -----------------------------------
|
||||
// Get and set network metadata
|
||||
// -----------------------------------
|
||||
@ -333,6 +343,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
|
||||
web3Modal,
|
||||
web3ProviderInfo,
|
||||
accountId,
|
||||
accountEns,
|
||||
balance,
|
||||
networkId,
|
||||
chainId,
|
||||
|
52
src/utils/ens.ts
Normal file
52
src/utils/ens.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { gql, OperationContext, OperationResult } from 'urql'
|
||||
import { fetchData } from './subgraph'
|
||||
|
||||
// make sure to only query for domains owned by account, so domains
|
||||
// solely set by 3rd parties like *.gitcoin.eth won't show up
|
||||
const UserEnsNames = gql<any>`
|
||||
query UserEnsDomains($accountId: String!) {
|
||||
domains(where: { resolvedAddress: $accountId, owner: $accountId }) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const UserEnsAddress = gql<any>`
|
||||
query UserEnsDomainsAddress($name: String!) {
|
||||
domains(where: { name: $name }) {
|
||||
resolvedAddress {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ensSubgraphQueryContext: OperationContext = {
|
||||
url: `https://api.thegraph.com/subgraphs/name/ensdomains/ens`,
|
||||
requestPolicy: 'cache-and-network'
|
||||
}
|
||||
|
||||
export async function getEnsName(accountId: string): Promise<string> {
|
||||
const response: OperationResult<any> = await fetchData(
|
||||
UserEnsNames,
|
||||
{ accountId: accountId.toLowerCase() },
|
||||
ensSubgraphQueryContext
|
||||
)
|
||||
if (!response?.data?.domains?.length) return
|
||||
|
||||
// Default order of response.data.domains seems to be by creation time, from oldest to newest.
|
||||
// Pick the last one as that is what direct web3 calls do.
|
||||
const { name } = response.data.domains.slice(-1)[0]
|
||||
return name
|
||||
}
|
||||
|
||||
export async function getEnsAddress(ensName: string): Promise<string> {
|
||||
const response: OperationResult<any> = await fetchData(
|
||||
UserEnsAddress,
|
||||
{ name: ensName },
|
||||
ensSubgraphQueryContext
|
||||
)
|
||||
if (!response?.data?.domains?.length) return
|
||||
const { id } = response.data.domains[0].resolvedAddress
|
||||
return id
|
||||
}
|
Loading…
Reference in New Issue
Block a user