1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-06-23 01:36:47 +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/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",

View File

@ -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",

View File

@ -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 />}

View File

@ -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>

View File

@ -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>

View File

@ -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>
)

View File

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

View File

@ -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>

View File

@ -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

View File

@ -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
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
}