1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-06-30 05:41:41 +02: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:
Matthias Kretschmann 2021-09-20 13:47:15 +02:00 committed by GitHub
parent 15a29bcb01
commit 212865110e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 196 additions and 84 deletions

20
package-lock.json generated
View File

@ -86,7 +86,6 @@
"@testing-library/jest-dom": "^5.12.0", "@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@types/chart.js": "^2.9.32", "@types/chart.js": "^2.9.32",
"@types/classnames": "^2.3.1",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.1", "@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3", "@types/lodash.debounce": "^4.0.3",
@ -10515,16 +10514,6 @@
"moment": "^2.10.2" "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": { "node_modules/@types/clipboard": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz",
@ -67020,15 +67009,6 @@
"moment": "^2.10.2" "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": { "@types/clipboard": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz",

View File

@ -101,7 +101,6 @@
"@testing-library/jest-dom": "^5.12.0", "@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7", "@testing-library/react": "^11.2.7",
"@types/chart.js": "^2.9.32", "@types/chart.js": "^2.9.32",
"@types/classnames": "^2.3.1",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.1", "@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3", "@types/lodash.debounce": "^4.0.3",

View File

@ -8,6 +8,7 @@ import { accountTruncate } from '../../../utils/web3'
import axios from 'axios' import axios from 'axios'
import Add from './Add' import Add from './Add'
import { useWeb3 } from '../../../providers/Web3' import { useWeb3 } from '../../../providers/Web3'
import { getEnsName } from '../../../utils/ens'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -20,27 +21,34 @@ export default function Publisher({
minimal?: boolean minimal?: boolean
className?: string className?: string
}): ReactElement { }): ReactElement {
const { networkId, accountId } = useWeb3() const { accountId } = useWeb3()
const [profile, setProfile] = useState<Profile>() 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 const showAdd = account === accountId && !profile
useEffect(() => { useEffect(() => {
if (!account) return if (!account) return
setName(accountTruncate(account))
const source = axios.CancelToken.source() 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) const profile = await get3BoxProfile(account, source.token)
if (!profile) return if (!profile) return
setProfile(profile) setProfile(profile)
const { name, emoji } = profile const { name, emoji } = profile
name && setName(`${emoji || ''} ${name}`) name && setName(`${emoji || ''} ${name}`)
} }
get3Box() getExternalName()
return () => { return () => {
source.cancel() source.cancel()
@ -58,7 +66,10 @@ export default function Publisher({
name name
) : ( ) : (
<> <>
<Link to={`/profile/${account}`} title="Show profile page."> <Link
to={`/profile/${accountEns || account}`}
title="Show profile page."
>
{name} {name}
</Link> </Link>
{showAdd && <Add />} {showAdd && <Add />}

View File

@ -9,7 +9,7 @@ import Blockies from '../../atoms/Blockies'
// Forward ref for Tippy.js // Forward ref for Tippy.js
// eslint-disable-next-line // eslint-disable-next-line
const Account = React.forwardRef((props, ref: any) => { const Account = React.forwardRef((props, ref: any) => {
const { accountId, web3Modal, connect } = useWeb3() const { accountId, accountEns, web3Modal, connect } = useWeb3()
async function handleActivation(e: FormEvent<HTMLButtonElement>) { async function handleActivation(e: FormEvent<HTMLButtonElement>) {
// prevent accidentially submitting a form the button might be in // prevent accidentially submitting a form the button might be in
@ -32,7 +32,7 @@ const Account = React.forwardRef((props, ref: any) => {
> >
<Blockies accountId={accountId} /> <Blockies accountId={accountId} />
<span className={styles.address} title={accountId}> <span className={styles.address} title={accountId}>
{accountTruncate(accountId)} {accountTruncate(accountEns || accountId)}
</span> </span>
<Caret aria-hidden="true" className={styles.caret} /> <Caret aria-hidden="true" className={styles.caret} />
</button> </button>

View File

@ -1,6 +1,5 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../providers/UserPreferences'
import { accountTruncate } from '../../../../utils/web3'
import ExplorerLink from '../../../atoms/ExplorerLink' import ExplorerLink from '../../../atoms/ExplorerLink'
import NetworkName from '../../../atoms/NetworkName' import NetworkName from '../../../atoms/NetworkName'
import jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg' import jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
@ -40,12 +39,13 @@ export default function Account({
</figure> </figure>
<div> <div>
<h3 className={styles.name}> <h3 className={styles.name}>{profile?.name}</h3>
{profile?.name || accountTruncate(accountId)}
</h3>
{accountId && ( {accountId && (
<code className={styles.accountId}> <code
{accountId} <Copy text={accountId} /> className={styles.accountId}
title={profile?.accountEns ? accountId : null}
>
{profile?.accountEns || accountId} <Copy text={accountId} />
</code> </code>
)} )}
<p> <p>

View File

@ -11,13 +11,9 @@ const isDescriptionTextClamped = () => {
if (el) return el.scrollHeight > el.clientHeight if (el) return el.scrollHeight > el.clientHeight
} }
const Link3Box = ({ accountId, text }: { accountId: string; text: string }) => { const LinkExternal = ({ url, text }: { url: string; text: string }) => {
return ( return (
<a <a href={url} target="_blank" rel="noreferrer">
href={`https://www.3box.io/${accountId}`}
target="_blank"
rel="noreferrer"
>
{text} {text}
</a> </a>
) )
@ -46,7 +42,10 @@ export default function AccountHeader({
<Markdown text={profile?.description} className={styles.description} /> <Markdown text={profile?.description} className={styles.description} />
{isDescriptionTextClamped() ? ( {isDescriptionTextClamped() ? (
<span className={styles.more} onClick={toogleShowMore}> <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> </span>
) : ( ) : (
'' ''
@ -56,7 +55,20 @@ export default function AccountHeader({
)} )}
</div> </div>
<div className={styles.meta}> <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>
</div> </div>
) )

View File

@ -6,6 +6,7 @@ export interface ProfileLink {
export interface Profile { export interface Profile {
did?: string did?: string
name?: string name?: string
accountEns?: string
description?: string description?: string
emoji?: string emoji?: string
image?: string image?: string

View File

@ -1,25 +1,63 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import Page from '../../components/templates/Page' import Page from '../../components/templates/Page'
import { graphql, PageProps } from 'gatsby' import { graphql, PageProps, navigate } from 'gatsby'
import ProfilePage from '../../components/pages/Profile' import ProfilePage from '../../components/pages/Profile'
import { accountTruncate } from '../../utils/web3' import { accountTruncate } from '../../utils/web3'
import { useWeb3 } from '../../providers/Web3' import { useWeb3 } from '../../providers/Web3'
import ProfileProvider from '../../providers/Profile' import ProfileProvider from '../../providers/Profile'
import { getEnsAddress, getEnsName } from '../../utils/ens'
import ethereumAddress from 'ethereum-address'
export default function PageGatsbyProfile(props: PageProps): ReactElement { export default function PageGatsbyProfile(props: PageProps): ReactElement {
const { accountId } = useWeb3() const { accountId, accountEns } = useWeb3()
const [finalAccountId, setFinalAccountId] = useState<string>() const [finalAccountId, setFinalAccountId] = useState<string>()
const [finalAccountEns, setFinalAccountEns] = useState<string>()
// Have accountId in path take over, if not present fall back to web3 // Have accountId in path take over, if not present fall back to web3
useEffect(() => { useEffect(() => {
const pathAccountId = props.location.pathname.split('/')[2] async function init() {
const finalAccountId = pathAccountId || accountId 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) setFinalAccountId(finalAccountId)
}, [props.location.pathname, accountId])
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 ( return (
<Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader> <Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader>
<ProfileProvider accountId={finalAccountId}> <ProfileProvider accountId={finalAccountId} accountEns={finalAccountEns}>
<ProfilePage accountId={finalAccountId} /> <ProfilePage accountId={finalAccountId} />
</ProfileProvider> </ProfileProvider>
</Page> </Page>

View File

@ -42,9 +42,11 @@ const refreshInterval = 10000 // 10 sec.
function ProfileProvider({ function ProfileProvider({
accountId, accountId,
accountEns,
children children
}: { }: {
accountId: string accountId: string
accountEns: string
children: ReactNode children: ReactNode
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
@ -62,18 +64,19 @@ function ProfileProvider({
}, [accountId]) }, [accountId])
// //
// 3Box // User profile: ENS + 3Box
// //
const [profile, setProfile] = useState<Profile>({ const [profile, setProfile] = useState<Profile>()
name: accountTruncate(accountId),
image: null, useEffect(() => {
description: null, if (!accountEns) return
links: null Logger.log(`[profile] ENS name found for ${accountId}:`, accountEns)
}) }, [accountId, accountEns])
useEffect(() => { useEffect(() => {
const clearedProfile: Profile = { const clearedProfile: Profile = {
name: null, name: null,
accountEns: null,
image: null, image: null,
description: null, description: null,
links: null links: null
@ -86,7 +89,9 @@ function ProfileProvider({
const cancelTokenSource = axios.CancelToken.source() const cancelTokenSource = axios.CancelToken.source()
async function getInfoFrom3Box() { async function getInfo() {
setProfile({ name: accountEns || accountTruncate(accountId), accountEns })
const profile3Box = await get3BoxProfile( const profile3Box = await get3BoxProfile(
accountId, accountId,
cancelTokenSource.token cancelTokenSource.token
@ -100,19 +105,22 @@ function ProfileProvider({
description, description,
links links
} }
setProfile(newProfile) setProfile((prevState) => ({
...prevState,
...newProfile
}))
Logger.log('[profile] Found and set 3box profile.', newProfile) Logger.log('[profile] Found and set 3box profile.', newProfile)
} else { } else {
setProfile(clearedProfile) // setProfile(clearedProfile)
Logger.log('[profile] No 3box profile found.') Logger.log('[profile] No 3box profile found.')
} }
} }
getInfoFrom3Box() getInfo()
return () => { return () => {
cancelTokenSource.cancel() cancelTokenSource.cancel()
} }
}, [accountId, isEthAddress]) }, [accountId, accountEns, isEthAddress])
// //
// POOL SHARES // POOL SHARES

View File

@ -18,7 +18,7 @@ import {
getNetworkDataById, getNetworkDataById,
getNetworkDisplayName getNetworkDisplayName
} from '../utils/web3' } from '../utils/web3'
import { graphql } from 'gatsby' import { getEnsName } from '../utils/ens'
import { UserBalance } from '../@types/TokenBalance' import { UserBalance } from '../@types/TokenBalance'
import { getOceanBalance } from '../utils/ocean' import { getOceanBalance } from '../utils/ocean'
import useNetworkMetadata from '../hooks/useNetworkMetadata' import useNetworkMetadata from '../hooks/useNetworkMetadata'
@ -29,6 +29,7 @@ interface Web3ProviderValue {
web3Modal: Web3Modal web3Modal: Web3Modal
web3ProviderInfo: IProviderInfo web3ProviderInfo: IProviderInfo
accountId: string accountId: string
accountEns: string
balance: UserBalance balance: UserBalance
networkId: number networkId: number
chainId: number chainId: number
@ -84,26 +85,6 @@ export const web3ModalOpts = {
const refreshInterval = 20000 // 20 sec. 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) const Web3Context = createContext({} as Web3ProviderValue)
function Web3Provider({ children }: { children: ReactNode }): ReactElement { function Web3Provider({ children }: { children: ReactNode }): ReactElement {
@ -120,6 +101,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
const [block, setBlock] = useState<number>() const [block, setBlock] = useState<number>()
const [isTestnet, setIsTestnet] = useState<boolean>() const [isTestnet, setIsTestnet] = useState<boolean>()
const [accountId, setAccountId] = useState<string>() const [accountId, setAccountId] = useState<string>()
const [accountEns, setAccountEns] = useState<string>()
const [web3Loading, setWeb3Loading] = useState<boolean>(true) const [web3Loading, setWeb3Loading] = useState<boolean>(true)
const [balance, setBalance] = useState<UserBalance>({ const [balance, setBalance] = useState<UserBalance>({
eth: '0', eth: '0',
@ -181,6 +163,27 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
} }
}, [accountId, networkId, web3]) }, [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 // Create initial Web3Modal instance
// ----------------------------------- // -----------------------------------
@ -229,6 +232,13 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
} }
}, [getUserBalance]) }, [getUserBalance])
// -----------------------------------
// Get and set user ENS name
// -----------------------------------
useEffect(() => {
getUserEnsName()
}, [getUserEnsName])
// ----------------------------------- // -----------------------------------
// Get and set network metadata // Get and set network metadata
// ----------------------------------- // -----------------------------------
@ -333,6 +343,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
web3Modal, web3Modal,
web3ProviderInfo, web3ProviderInfo,
accountId, accountId,
accountEns,
balance, balance,
networkId, networkId,
chainId, chainId,

52
src/utils/ens.ts Normal file
View 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
}