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

fix: Estate fingerprint calculation #2047

Merged
merged 5 commits into from
Nov 14, 2023
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
15 changes: 14 additions & 1 deletion webapp/src/components/AssetImage/AssetImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const AssetImage = (props: Props) => {
wearableController,
isTryingOn,
isPlayingEmote,
showUpdatedDateWarning,
onSetIsTryingOn,
onSetWearablePreviewController,
onPlaySmartWearableVideoShowcase,
Expand Down Expand Up @@ -186,7 +187,13 @@ const AssetImage = (props: Props) => {
setHasSound(sound)
})
}
}, [wearableController, asset.category, isDraggable, hasSound, isLoadingWearablePreview])
}, [
wearableController,
asset.category,
isDraggable,
hasSound,
isLoadingWearablePreview
])

const estateSelection = useMemo(() => (estate ? getSelection(estate) : []), [
estate
Expand Down Expand Up @@ -257,6 +264,9 @@ const AssetImage = (props: Props) => {
showForRent={false}
showOnSale={false}
showOwned={false}
lastUpdated={
showUpdatedDateWarning ? new Date(asset.updatedAt) : undefined
}
>
{hasBadges && children}
</Atlas>
Expand All @@ -278,6 +288,9 @@ const AssetImage = (props: Props) => {
showOnSale={false}
showOwned={false}
isEstate
lastUpdated={
showUpdatedDateWarning ? new Date(asset.updatedAt) : undefined
}
>
{hasBadges && children}
</Atlas>
Expand Down
1 change: 1 addition & 0 deletions webapp/src/components/AssetImage/AssetImage.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type Props = {
className?: string
isDraggable?: boolean
withNavigation?: boolean
showUpdatedDateWarning?: boolean
hasPopup?: boolean
zoom?: number
isSmall?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const EstateDetail = ({ nft, order, rental }: Props) => {
isDraggable
withNavigation
hasPopup
showUpdatedDateWarning
/>
{estate.size === 0 && (
<div className="dissolved-wrapper">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ const ParcelDetail = ({ nft, order, rental }: Props) => {
asset={nft}
rental={rental ?? undefined}
assetImage={
<AssetImage asset={nft} isDraggable withNavigation hasPopup />
<AssetImage
asset={nft}
isDraggable
withNavigation
hasPopup
showUpdatedDateWarning
/>
}
showDetails={isLand(nft)}
isOnSale={!!nft.activeOrderId}
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/components/Atlas/Atlas.container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { connect } from 'react-redux'
import { push } from 'connected-react-router'
import { getTiles, getTilesByEstateId } from '../../modules/tile/selectors'
import { getLastModifiedDate, getTiles, getTilesByEstateId } from '../../modules/tile/selectors'
import { getOnRentNFTsByLessor } from '../../modules/ui/browse/selectors'
import { RootState } from '../../modules/reducer'
import { getWalletNFTs } from '../../modules/nft/selectors'
Expand All @@ -19,6 +19,7 @@ const mapState = (state: RootState): MapStateProps => {
nfts: getWalletNFTs(state),
nftsOnRent,
tilesByEstateId: getTilesByEstateId(state),
lastAtlasModifiedDate: getLastModifiedDate(state),
getContract: (query: Partial<Contract>) => getContract(state, query)
}
}
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/components/Atlas/Atlas.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
outline: 1px solid white;
}

.atlas-wrapper .atlas-warning-banner {
position: absolute;
bottom: 0;
width: 100%;
}

.atlas-wrapper .atlas-info-button .info-icon {
background-image: url('../../images/info.svg');
width: 16px;
Expand Down
15 changes: 14 additions & 1 deletion webapp/src/components/Atlas/Atlas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { nftAPI } from '../../modules/vendor/decentraland/nft/api'
import { Props, Tile } from './Atlas.types'
import { VendorName } from '../../modules/vendor'
import { NFT } from '../../modules/nft/types'
import ErrorBanner from '../ErrorBanner'
import Popup from './Popup'
import './Atlas.css'

Expand All @@ -35,8 +36,10 @@ const Atlas: React.FC<Props> = (props: Props) => {
tilesByEstateId,
withMapColorsInfo,
withZoomControls,
lastAtlasModifiedDate,
getContract,
children
children,
lastUpdated
} = props

const [showPopup, setShowPopup] = useState(false)
Expand Down Expand Up @@ -375,6 +378,16 @@ const Atlas: React.FC<Props> = (props: Props) => {
layers={layers}
withZoomControls={withZoomControls}
/>
{lastAtlasModifiedDate &&
lastUpdated &&
lastUpdated > lastAtlasModifiedDate ? (
<ErrorBanner
className="atlas-warning-banner"
info={t('atlas_updated_warning.info', {
strong: (text: string) => <strong>{text}</strong>
})}
/>
) : null}
{hoveredTile ? (
<Popup
x={x}
Expand Down
4 changes: 3 additions & 1 deletion webapp/src/components/Atlas/Atlas.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ export type Props = Partial<AtlasProps> & {
showOwned?: boolean
withMapColorsInfo?: boolean
withZoomControls?: boolean
lastUpdated?: Date
lastAtlasModifiedDate: Date | null
getContract: (query: Partial<Contract>) => ReturnType<typeof getContract>
onNavigate: (path: string) => void
children?: React.ReactNode
}

export type MapStateProps = Pick<
Props,
'tiles' | 'nfts' | 'nftsOnRent' | 'tilesByEstateId' | 'getContract'
'tiles' | 'nfts' | 'nftsOnRent' | 'tilesByEstateId' | 'getContract' | 'lastAtlasModifiedDate'
>
export type MapDispatchProps = Pick<Props, 'onNavigate'>
export type MapDispatch = Dispatch<CallHistoryMethodAction>
16 changes: 11 additions & 5 deletions webapp/src/components/BidPage/BidModal/BidModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useCallback } from 'react'
import { ethers } from 'ethers'
import { Contract } from '@dcl/schemas'
import { Contract, NFTCategory } from '@dcl/schemas'
import { Header, Form, Field, Button } from 'decentraland-ui'
import { t, T } from 'decentraland-dapps/dist/modules/translation/utils'
import { withAuthorizedAction } from 'decentraland-dapps/dist/containers'
Expand All @@ -26,6 +26,7 @@ import { getBidStatus, getError } from '../../../modules/bid/selectors'
import { ManaField } from '../../ManaField'
import { ConfirmInputValueModal } from '../../ConfirmInputValueModal'
import { Mana } from '../../Mana'
import ErrorBanner from '../../ErrorBanner'
import { Props } from './BidModal.types'
import './BidModal.css'

Expand All @@ -44,7 +45,7 @@ const BidModal = (props: Props) => {
const [price, setPrice] = useState('')
const [expiresAt, setExpiresAt] = useState(getDefaultExpirationDate())

const [fingerprint, isLoading] = useFingerprint(nft)
const [fingerprint, isLoadingFingerprint, contractFingerprint] = useFingerprint(nft)

const [showConfirmationModal, setShowConfirmationModal] = useState(false)

Expand Down Expand Up @@ -105,8 +106,10 @@ const BidModal = (props: Props) => {
isInvalidPrice ||
isInvalidDate ||
hasInsufficientMANA ||
isLoading ||
isPlacingBid
isLoadingFingerprint ||
isPlacingBid ||
(!fingerprint && nft.category === NFTCategory.ESTATE) ||
contractFingerprint !== fingerprint

return (
<AssetAction asset={nft}>
Expand Down Expand Up @@ -161,11 +164,14 @@ const BidModal = (props: Props) => {
error={isInvalidDate}
message={isInvalidDate ? t('bid_page.invalid_date') : undefined}
/>
{!isLoadingFingerprint && contractFingerprint !== fingerprint ? (
<ErrorBanner info={t('atlas_updated_warning.fingerprint_missmatch')} />
) : null}
</div>
<div className="buttons">
<Button
as="div"
disabled={isLoading || isPlacingBid}
disabled={isLoadingFingerprint || isPlacingBid}
onClick={() =>
onNavigate(locations.nft(nft.contractAddress, nft.tokenId))
}
Expand Down
22 changes: 13 additions & 9 deletions webapp/src/components/BuyPage/BuyNFTModal/BuyNFTModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ import { getContractNames } from '../../../modules/vendor'
import * as events from '../../../utils/events'
import { AssetType } from '../../../modules/asset/types'
import { Contract as DCLContract } from '../../../modules/vendor/services'
import { getBuyItemStatus, getError } from '../../../modules/order/selectors'
import { AssetAction } from '../../AssetAction'
import { Network as NetworkSubtitle } from '../../Network'
import PriceSubtitle from '../../Price'
import { AssetProviderPage } from '../../AssetProviderPage'
import { getBuyItemStatus, getError } from '../../../modules/order/selectors'
import ErrorBanner from '../../ErrorBanner'
import { PriceTooLow } from '../PriceTooLow'
import { Name } from '../Name'
import { Price } from '../Price'
Expand All @@ -51,7 +52,11 @@ const BuyNFTModal = (props: Props) => {
onClearOrderErrors
} = props

const [fingerprint, isFingerprintLoading] = useFingerprint(nft)
const [
fingerprint,
isFingerprintLoading,
contractFingerprint
] = useFingerprint(nft)
const analytics = getAnalytics()

const handleExecuteOrder = useCallback(
Expand Down Expand Up @@ -121,7 +126,9 @@ const BuyNFTModal = (props: Props) => {
!order ||
isOwner ||
(hasInsufficientMANA && !isBuyWithCardPage) ||
(!fingerprint && nft.category === NFTCategory.ESTATE)
(!fingerprint && nft.category === NFTCategory.ESTATE) ||
isFingerprintLoading ||
contractFingerprint !== fingerprint

const name = <Name asset={nft} />

Expand All @@ -140,12 +147,6 @@ const BuyNFTModal = (props: Props) => {
subtitle = (
<T id={`${translationPageDescriptorId}.not_for_sale`} values={{ name }} />
)
} else if (
!fingerprint &&
nft.category === NFTCategory.ESTATE &&
!isFingerprintLoading
) {
subtitle = <T id={`${translationPageDescriptorId}.no_fingerprint`} />
} else if (isOwner) {
subtitle = (
<T id={`${translationPageDescriptorId}.is_owner`} values={{ name }} />
Expand Down Expand Up @@ -202,6 +203,9 @@ const BuyNFTModal = (props: Props) => {
<PriceTooLow chainId={nft.chainId} network={nft.network} />
) : null}
<PartiallySupportedNetworkCard asset={nft} />
{!isFingerprintLoading && contractFingerprint !== fingerprint ? (
<ErrorBanner info={t('atlas_updated_warning.fingerprint_missmatch')} />
) : null}
<div
className={classNames('buttons', isWearableOrEmote(nft) && 'with-mana')}
>
Expand Down
28 changes: 28 additions & 0 deletions webapp/src/components/ErrorBanner/ErrorBanner.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.container {
background: var(--dropdown);
display: flex;
padding: 20px;
gap: 10px;
}

.icon {
background: var(--oj-not-simpson);
border-radius: 50%;
min-width: 25px;
height: 25px;
text-align: center;
margin: 0;
}

.icon :global(.icon) {
position: relative;
top: 2px;
margin: 0;
}

.info {
display: flex;
flex-direction: column;
gap: 5px;
text-align: left;
}
17 changes: 17 additions & 0 deletions webapp/src/components/ErrorBanner/ErrorBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import classNames from 'classnames'
import { Icon } from 'decentraland-ui'
import { Props } from './ErrorBanner.types'
import styles from './ErrorBanner.module.css'

export default function ErrorBanner({ info, className }: Props) {
return (
<div className={classNames(styles.container, className)}>
<div className={styles.icon}>
<Icon name="exclamation triangle" />
</div>
<span className={styles.info}>
{info}
</span>
</div>
)
}
4 changes: 4 additions & 0 deletions webapp/src/components/ErrorBanner/ErrorBanner.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Props = {
info: string;
className?: string;
}
2 changes: 2 additions & 0 deletions webapp/src/components/ErrorBanner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ErrorBanner from "./ErrorBanner";
export default ErrorBanner
51 changes: 50 additions & 1 deletion webapp/src/modules/nft/estate/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
getSigner,
getConnectedProvider
getConnectedProvider,
getNetworkProvider
} from 'decentraland-dapps/dist/lib/eth'
import { EstateRegistry__factory } from '../../../contracts'
import { Contract } from '../../vendor/services'
import { NFT } from '../types'
import { ethers } from 'ethers'

export const getSelection = (estate: NFT['data']['estate']) => {
return estate!.parcels.map(pair => ({
Expand All @@ -21,6 +23,53 @@ export const getCenter = (selection: { x: number; y: number }[]) => {
return [x, y]
}

export async function generateFingerprint(
estateId: string,
parcels: { x: number; y: number }[],
landContract: Contract
) {
const provider = await getNetworkProvider(landContract.chainId)
const contract = new ethers.Contract(
landContract.address,
[
{
constant: true,
inputs: [
{ name: 'x', type: 'int256' },
{ name: 'y', type: 'int256' }
],
name: 'encodeTokenId',
outputs: [{ name: '', type: 'uint256' }],
payable: false,
stateMutability: 'pure',
type: 'function'
}
],
new ethers.providers.Web3Provider(provider)
)

const estateTokenIds = []

for (const parcel of parcels) {
estateTokenIds.push(await contract.encodeTokenId(parcel.x, parcel.y))
}

let fingerprint = BigInt(
ethers.utils.solidityKeccak256(
['string', 'uint256'],
['estateId', estateId]
)
)

for (const tokenId of estateTokenIds) {
fingerprint ^= BigInt(
ethers.utils.solidityKeccak256(['uint256'], [tokenId])
)
}

return ethers.utils.hexlify(fingerprint)
}

export async function getFingerprint(
estateId: string,
estateContract: Contract
Expand Down
Loading
Loading