Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ethereum address mapping #3015

Merged
merged 6 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
600 changes: 515 additions & 85 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"@types/uuid": "^3.4.4",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"canvas": "^2.11.2",
"concurrently": "^7.2.2",
"decentraland-rpc": "^3.1.8",
"eslint": "^7.28.0",
Expand Down
4 changes: 3 additions & 1 deletion src/components/ENSListPage/ENSListPage.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FETCH_ENS_LIST_REQUEST } from 'modules/ens/actions'
import { getLands, getLoading as getLandsLoading, getError as getLandsError } from 'modules/land/selectors'
import { FETCH_LANDS_REQUEST } from 'modules/land/actions'
import { getAvatar, getName } from 'modules/profile/selectors'
import { getIsEnsAddressEnabled } from 'modules/features/selectors'
import { openModal } from 'modules/modal/actions'
import { MapStateProps, MapDispatchProps, MapDispatch } from './ENSListPage.types'
import ENSListPage from './ENSListPage'
Expand All @@ -24,7 +25,8 @@ const mapState = (state: RootState): MapStateProps => ({
isLoadingType(getLandsLoading(state), FETCH_LANDS_REQUEST) ||
isLoadingType(getLoading(state), FETCH_ENS_LIST_REQUEST) ||
isLoggingIn(state),
isLoggedIn: isLoggedIn(state)
isLoggedIn: isLoggedIn(state),
isEnsAddressEnabled: getIsEnsAddressEnabled(state)
})

const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
Expand Down
85 changes: 85 additions & 0 deletions src/components/ENSListPage/ENSListPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,88 @@
.ENSListPage .ui.dropdown {
margin-right: 20px;
}

.ENSListPage .ens-list-btn.ui.button {
display: flex;
gap: 5px;
align-items: center;
text-transform: none;
}

.ENSListPage .ens-list-btn .Icon.pin {
width: 15px;
height: 15px;
}

.ENSListPage .ens-list-owner {
display: flex;
align-items: center;
gap: 5px;
}

.ENSListPage .ens-list-land {
display: flex;
align-items: center;
gap: 5px;
}

.ENSListPage .ens-list-land-coord {
display: flex;
height: 34px;
align-items: center;
padding: 0 10px;
background: var(--secondary);
border-radius: 6px;
}

.ENSListPage .ens-list-land-redirect-icon {
filter: grayscale(1) brightness(5);
}

.ENSListPage .ens-list-land-redirect {
width: fit-content;
min-width: unset;
height: 34px;
padding: 0 6px;
display: flex;
align-items: center;
}

.ENSListPage .ens-list-address {
display: flex;
gap: 5px;
align-items: center;
}

.ENSListPage .ens-list-address-icon {
width: 25px;
}

.ENSListPage .ens-address-copy {
background: var(--secondary);
border-radius: 50%;
width: 25px;
height: 25px;
font-size: 12px;
align-items: center;
display: flex;
justify-content: center;
margin-left: 10px;
cursor: pointer;
}

.ENSListPage .ens-address-copy:hover {
background-color: var(--secondary-hover);
}

.ENSListPage .ens-page-content {
display: flex;
flex-direction: column;
gap: 20px;
}

.ENSListPage .ens-page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
167 changes: 163 additions & 4 deletions src/components/ENSListPage/ENSListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { config } from 'config'
import { Popup, Button, Table, Row, Column, Header, Section, Container, Pagination, Dropdown, Empty } from 'decentraland-ui'
import {
Popup,
Button,
Table,
Row,
Column,
Header,
Section,
Container,
Pagination,
Dropdown,
Empty,
Icon as DCLIcon
} from 'decentraland-ui'
import { T, t } from 'decentraland-dapps/dist/modules/translation/utils'
import { locations } from 'routing/locations'
import { isCoords } from 'modules/land/utils'
import { ENS } from 'modules/ens/types'
import Icon from 'components/Icon'
import CopyToClipboard from 'components/CopyToClipboard/CopyToClipboard'
import { NavigationTab } from 'components/Navigation/Navigation.types'
import LoggedInDetailPage from 'components/LoggedInDetailPage'
import ethereumImg from '../../icons/ethereum.svg'
import { getCroppedAddress } from './utils'
import { Props, State, SortBy } from './ENSListPage.types'
import './ENSListPage.css'

Expand All @@ -27,6 +43,15 @@ export default class ENSListPage extends React.PureComponent<Props, State> {
this.props.onNavigate(locations.ensSelectLand(ens.subdomain))
}

handleUseAsAlias = (name: string) => {
this.handleOpenModal(name)
}

handleAssignENSAddress = (ens: ENS) => {
const { onOpenModal } = this.props
onOpenModal('EnsMapAddressModal', { ens })
}

handleOpenModal = (newName: string) => {
const { onOpenModal } = this.props
onOpenModal('UseAsAliasModal', { newName })
Expand Down Expand Up @@ -99,14 +124,61 @@ export default class ENSListPage extends React.PureComponent<Props, State> {
)
}

renderLandLinkInfo(ens: ENS) {
if (!ens.landId) {
return (
<Button compact className="ens-list-btn" onClick={this.handleAssignENS.bind(null, ens)}>
<Icon name="pin" />
{t('ens_list_page.button.assign_to_land')}
</Button>
)
}
if (isCoords(ens.landId)) {
return (
<div className="ens-list-land">
<span className="ens-list-land-coord">
<Icon name="pin" />
{ens.landId}
</span>
<Button
compact
className="ens-list-land-redirect"
target="_blank"
href={`https://${ens.subdomain}.${config.get('ENS_GATEWAY')}`}
rel="noopener noreferrer"
>
<Icon name="right-round-arrow" className="ens-list-land-redirect-icon" />
</Button>
</div>
)
} else {
return (
<div className="ens-list-land">
<span className="ens-list-land-coord">
<Icon name="pin" />
{`Estate (${ens.landId})`}
</span>
<Button
compact
className="ens-list-land-redirect"
target="_blank"
href={`https://${ens.subdomain}.${config.get('ENS_GATEWAY')}`}
rel="noopener noreferrer"
>
<Icon name="right-round-arrow" className="ens-list-land-redirect-icon" />
</Button>
</div>
)
}
}

renderEnsList() {
const { ensList, hasProfileCreated } = this.props
const { page } = this.state

const total = ensList.length
const totalPages = Math.ceil(total / PAGE_SIZE)
const paginatedItems = this.paginate()

return (
<>
<div className="filters">
Expand Down Expand Up @@ -238,8 +310,95 @@ export default class ENSListPage extends React.PureComponent<Props, State> {
)
}

renderNewEnsList() {
const { hasProfileCreated, ensList } = this.props
const { page } = this.state

const total = ensList.length
const totalPages = Math.ceil(total / PAGE_SIZE)
const paginatedItems = this.paginate()
return (
<div className="ens-page-content">
<div className="ens-page-header">
<div>
<h1>NAMEs</h1>
{`${ensList.length} results`}
</div>
<Button href={`${MARKETPLACE_WEB_URL}/names/claim`} target="_blank" primary>
Mint Name
</Button>
</div>
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.HeaderCell width="2">{t('ens_list_page.table.name')}</Table.HeaderCell>
<Table.HeaderCell width="1">{t('ens_list_page.table.alias')}</Table.HeaderCell>
<Table.HeaderCell width="2">{t('ens_list_page.table.address')}</Table.HeaderCell>
<Table.HeaderCell width="2">{t('ens_list_page.table.land')}</Table.HeaderCell>
<Table.HeaderCell width="1">{t('ens_list_page.table.actions')}</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{paginatedItems.map((ens: ENS, index) => {
return (
<Table.Row className="TableRow" key={index}>
<Table.Cell>{ens.subdomain}</Table.Cell>
<Table.Cell>
{this.isAlias(ens) ? (
<span className="ens-list-owner">
<Icon name="profile" />
{t('ens_list_page.table.owner')}
</span>
) : (
<Button
compact
className="ens-list-btn"
onClick={this.handleUseAsAlias.bind(null, ens.name)}
disabled={!hasProfileCreated}
>
<Icon name="add" />
{t('ens_list_page.button.add_to_avatar')}
</Button>
)}
</Table.Cell>
<Table.Cell>
{ens.ensAddressRecord ? (
<span className="ens-list-address">
<img className="ens-list-address-icon" src={ethereumImg} alt="Ethereum" />
{getCroppedAddress(ens.ensAddressRecord)}
<CopyToClipboard role="button" text={ens.ensAddressRecord} showPopup={true}>
<DCLIcon aria-label="Copy urn" aria-hidden="false" className="ens-address-copy" name="copy outline" />
</CopyToClipboard>
</span>
) : (
<Button compact className="ens-list-btn" onClick={this.handleAssignENSAddress.bind(null, ens)}>
<Icon name="add" />
{t('ens_list_page.button.link_to_address')}
</Button>
)}
</Table.Cell>
<Table.Cell>{this.renderLandLinkInfo(ens)}</Table.Cell>
<Table.Cell>TBD</Table.Cell>
LautaroPetaccio marked this conversation as resolved.
Show resolved Hide resolved
</Table.Row>
)
})}
</Table.Body>
</Table>
{totalPages > 1 && (
<Pagination
firstItem={null}
lastItem={null}
totalPages={totalPages}
activePage={page}
onPageChange={(_event, props) => this.setState({ page: +props.activePage! })}
/>
)}
</div>
)
}

render() {
const { isLoading, error } = this.props
const { isLoading, isEnsAddressEnabled, error } = this.props
return (
<LoggedInDetailPage
className="ENSListPage view"
Expand All @@ -248,7 +407,7 @@ export default class ENSListPage extends React.PureComponent<Props, State> {
isLoading={isLoading}
isPageFullscreen={true}
>
{this.renderEnsList()}
{isEnsAddressEnabled ? this.renderNewEnsList() : this.renderEnsList()}
</LoggedInDetailPage>
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/ENSListPage/ENSListPage.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type Props = {
hasProfileCreated: boolean
isLoggedIn: boolean
isLoading: boolean
isEnsAddressEnabled: boolean
onNavigate: (path: string) => void
onOpenModal: typeof openModal
}
Expand All @@ -28,7 +29,7 @@ export type State = {

export type MapStateProps = Pick<
Props,
'address' | 'alias' | 'ensList' | 'lands' | 'hasProfileCreated' | 'isLoading' | 'error' | 'isLoggedIn'
'address' | 'alias' | 'ensList' | 'lands' | 'hasProfileCreated' | 'isLoading' | 'error' | 'isLoggedIn' | 'isEnsAddressEnabled'
>
export type MapDispatchProps = Pick<Props, 'onNavigate' | 'onOpenModal'>
export type MapDispatch = Dispatch
21 changes: 21 additions & 0 deletions src/components/ENSListPage/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getCroppedAddress } from './utils'

describe('getCroppedAddress', () => {
describe('when address is a 42 character string', () => {
it('should return only the first 5 and last 6 characters of address', () => {
expect(getCroppedAddress('0xA4f689625F6F51AdF691988D38772BE8509087d2')).toEqual('0xA4f...9087d2')
})
})

describe('when address is undefined', () => {
it('should return empty string', () => {
expect(getCroppedAddress(undefined)).toEqual('')
})
})

describe('when address is not a 42 character string', () => {
it('should return empty string', () => {
expect(getCroppedAddress('0xA4f689625F6F51AdF691988D38772')).toEqual('')
})
})
})
6 changes: 6 additions & 0 deletions src/components/ENSListPage/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function getCroppedAddress(address?: string) {
if (!address || address.length < 42) {
return ''
}
return `${address.slice(0, 5)}...${address.slice(-6)}`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { connect } from 'react-redux'
import { SET_ENS_ADDRESS_REQUEST, setENSAddressRequest } from 'modules/ens/actions'
import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors'
import { getError, getLoading, isWaitingTxSetAddress } from 'modules/ens/selectors'
import { RootState } from 'modules/common/types'
import { MapDispatch, MapDispatchProps, OwnProps } from './ENSMapAddressModal.types'
import EnsMapAddressModal from './ENSMapAddressModal'

const mapState = (state: RootState) => {
const error = getError(state)
return {
isLoading: isLoadingType(getLoading(state), SET_ENS_ADDRESS_REQUEST) || isWaitingTxSetAddress(state),
error: error ? error.message : null
}
}

const mapDispatch = (dispatch: MapDispatch, ownProps: OwnProps): MapDispatchProps => ({
onSave: ((address: string) => dispatch(setENSAddressRequest(ownProps.metadata.ens, address))) as any
})

export default connect(mapState, mapDispatch)(EnsMapAddressModal)
Loading
Loading