Skip to content

Commit

Permalink
Merge pull request #17 from decentraland/feat/add-contract-validation
Browse files Browse the repository at this point in the history
feat: Add contract validation & remove new URN logic
  • Loading branch information
LautaroPetaccio authored Jul 22, 2024
2 parents bbfe9b7 + ba42870 commit 1b0493b
Show file tree
Hide file tree
Showing 21 changed files with 258 additions and 97 deletions.
73 changes: 63 additions & 10 deletions src/components/ContractsField/ContractsField.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
import { providers, Contract } from 'ethers'
import React, { useCallback, useMemo, useState } from 'react'
import { t } from 'decentraland-dapps/dist/modules/translation'
import { Button, DropdownProps, Field, InputOnChangeData, SelectField } from 'decentraland-ui'
import { Props } from './ContractsField.types'
import styles from './ContractsField.module.css'
import { ContractNetwork, LinkedContract, TEST_NETWORKS } from '../../modules/thirdParty/types'
import { isAddress } from '../../modules/thirdParty/utils'
import { debounce } from '../../lib/time'
import { isDevelopment } from '../../lib/environment'
import { RPC_URLS } from './utils'
import { Props } from './ContractsField.types'
import styles from './ContractsField.module.css'

export const ContractsField = (props: Props) => {
const { onChange, disabled } = props
const [address, setAddress] = useState<string>()
const [network, setNetwork] = useState(ContractNetwork.ETHEREUM_MAINNET)
const [contractError, setContractError] = useState<string>()
const [isCheckingContract, setIsCheckingContract] = useState(false)

const validateContract = useCallback(
debounce(async (contractAddress: string, network: ContractNetwork) => {
setIsCheckingContract(true)
setContractError(undefined)
const jsonRpcProvider = new providers.JsonRpcProvider(RPC_URLS[network])
const erc165Contract = new Contract(
contractAddress,
['function supportsInterface(bytes4) external view returns (bool)'],
jsonRpcProvider
)
const Erc721InterfaceId = '0x01ffc9a7'
const Erc1155InterfaceId = '0xd9b67a26'
try {
const supportsInterfaces = await Promise.all([
erc165Contract.supportsInterface(Erc721InterfaceId),
erc165Contract.supportsInterface(Erc1155InterfaceId)
])
if (!supportsInterfaces.some(supportsInterface => supportsInterface)) {
setContractError(
'The contract is not based on the ERC721 or the ERC1155 standard. Please, check if the network or the contract address is correct.'
)
}
} catch (error) {
setContractError('There was an error checking the contract. The contract might not exist or the connection might be down.')
console.error(error)
} finally {
setIsCheckingContract(false)
}
}, 1000),
[setIsCheckingContract, setContractError]
)

const networkOptions = useMemo(
() =>
Expand All @@ -22,34 +58,51 @@ export const ContractsField = (props: Props) => {
)

const handleAdd = useCallback(() => {
onChange({ address, network } as LinkedContract)
onChange({ address: address?.toLowerCase(), network } as LinkedContract)
setAddress('')
setNetwork(ContractNetwork.ETHEREUM_MAINNET)
}, [onChange, address, network])

const handleChangeNetwork = useCallback(
(_e: React.SyntheticEvent<HTMLElement, Event>, data: DropdownProps) => {
setNetwork(data.value as ContractNetwork)
if (address && isAddress(address)) {
validateContract(address, data.value)
}
},
[setNetwork]
[setNetwork, validateContract, address]
)

const handleChangeContract = useCallback(
(_e: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData) => {
setContractError(undefined)
setAddress(data.value)
if (!isAddress(data.value)) {
setContractError(t('linked_contracts.required_address'))
return setContractError(t('linked_contracts.required_address'))
}
validateContract(data.value, network)
},
[setAddress]
[setAddress, validateContract, network]
)

return (
<div className={styles.contractsField}>
<SelectField options={networkOptions} disabled={disabled} value={network} onChange={handleChangeNetwork} />
<Field value={address} disabled={disabled} onChange={handleChangeContract} maxLength={42} type="address" />
<Button disabled={!!contractError || !address || disabled} onClick={handleAdd}>
<SelectField
options={networkOptions}
disabled={disabled || isCheckingContract}
value={network}
loading={isCheckingContract}
onChange={handleChangeNetwork}
/>
<Field
value={address}
disabled={disabled || isCheckingContract}
onChange={handleChangeContract}
loading={isCheckingContract}
maxLength={42}
message={contractError}
error={!!contractError}
/>
<Button disabled={!!contractError || !address || disabled || isCheckingContract} onClick={handleAdd}>
{t('linked_contracts.add_contract')}
</Button>
</div>
Expand Down
8 changes: 8 additions & 0 deletions src/components/ContractsField/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ContractNetwork } from '../../modules/thirdParty/types'

export const RPC_URLS = {
[ContractNetwork.ETHEREUM_MAINNET]: 'https://rpc.decentraland.org/mainnet',
[ContractNetwork.MATIC_MAINNET]: 'https://rpc.decentraland.org/polygon',
[ContractNetwork.ETHEREUM_SEPOLIA]: 'https://rpc.decentraland.org/sepolia',
[ContractNetwork.MATIC_AMOY]: 'https://rpc.decentraland.org/amoy'
}
51 changes: 27 additions & 24 deletions src/components/ContractsTable/ContractsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
import React from 'react'
import { Table } from 'decentraland-ui'
import { Message, Table } from 'decentraland-ui'
import { Props } from './ContractsTable.types'
import { t } from 'decentraland-dapps/dist/modules/translation'

export const ContractsTable = ({ contracts }: Props) => {
export const ContractsTable = ({ contracts, message, error }: Props) => {
return (
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.HeaderCell></Table.HeaderCell>
<Table.HeaderCell>{t('linked_contracts.network')}</Table.HeaderCell>
<Table.HeaderCell>{t('linked_contracts.address')}</Table.HeaderCell>
</Table.Row>
</Table.Header>

<Table.Body>
{contracts.length === 0 ? (
<>
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.Cell>{t('linked_contracts.no_contracts')}</Table.Cell>
<Table.HeaderCell></Table.HeaderCell>
<Table.HeaderCell>{t('linked_contracts.network')}</Table.HeaderCell>
<Table.HeaderCell>{t('linked_contracts.address')}</Table.HeaderCell>
</Table.Row>
) : (
contracts.map(({ network, address }, index) => (
<Table.Row key={index}>
<Table.Cell>{index}</Table.Cell>
<Table.Cell>{network}</Table.Cell>
<Table.Cell>{address}</Table.Cell>
</Table.Header>

<Table.Body>
{contracts.length === 0 ? (
<Table.Row>
<Table.Cell>{t('linked_contracts.no_contracts')}</Table.Cell>
</Table.Row>
))
)}
</Table.Body>
</Table>
) : (
contracts.map(({ network, address }, index) => (
<Table.Row key={index}>
<Table.Cell>{index}</Table.Cell>
<Table.Cell>{network}</Table.Cell>
<Table.Cell>{address}</Table.Cell>
</Table.Row>
))
)}
</Table.Body>
</Table>
{message && error && <Message error={error} content={message} />}
</>
)
}
2 changes: 2 additions & 0 deletions src/components/ContractsTable/ContractsTable.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ import { LinkedContract } from '../../modules/thirdParty/types'

export type Props = {
contracts: LinkedContract[]
error?: boolean
message?: string
}
5 changes: 3 additions & 2 deletions src/components/CreateThirdParty/CreateThirdParty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Link } from 'react-router-dom'
import { Controller, useForm } from 'react-hook-form'
import Page from '../Page'
import { CreateThirdPartyFormData, Props } from './CreateThirdParty.types'
import { locations } from '../../modules/locations'
import { getUrn } from './utils'
import ManagersField from '../ManagersField'
import { CreateThirdPartyFormData, Props } from './CreateThirdParty.types'
import { getUrn } from './utils'
import './CreateThirdParty.css'

const CreateThirdParty = ({ chainId, isLoading, onSubmit }: Props) => {
Expand Down Expand Up @@ -105,6 +105,7 @@ const CreateThirdParty = ({ chainId, isLoading, onSubmit }: Props) => {
}}
render={({ field, fieldState }) => (
<ManagersField
onBlur={field.onBlur}
managers={field.value}
error={fieldState.error?.message}
onChange={managers => field.onChange({ target: { value: managers } })}
Expand Down
2 changes: 1 addition & 1 deletion src/components/CreateThirdParty/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const toCreateThirdParty = (data: CreateThirdPartyFormData, chainId: Chai
export const getUrn = (thirdPartyName: string, chainId: ChainId) => {
const protocol = getURNProtocol(chainId)
const name = formatThirdPartyName(thirdPartyName)
return `urn:decentraland:${protocol}:collections-linked-wearables:${name}`
return `urn:decentraland:${protocol}:collections-thirdparty:${name}`
}

export const formatThirdPartyName = (value: string) => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/ManagersField/ManagersField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { isAddress } from '../../modules/thirdParty/utils'
import { Props } from './ManagersField.types'
import './ManagersField.css'

const ManagersField = ({ managers, error: providedError, onChange }: Props) => {
const ManagersField = ({ managers, error: providedError, onChange, onBlur }: Props) => {
const [error, setError] = useState('')

return (
<div className="ManagersField">
<TagField
label={t('managers_field.label')}
value={managers}
onBlur={onBlur}
onChange={(_, props) => {
const values = props.value as string[]
if (values.length > 0) {
Expand Down
9 changes: 5 additions & 4 deletions src/components/ManagersField/ManagersField.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type Props = {
managers: string[];
error?: string;
onChange: (managers: string[]) => void;
};
managers: string[]
error?: string
onBlur?: () => void
onChange: (managers: string[]) => void
}
3 changes: 0 additions & 3 deletions src/components/ThirdParties/ThirdParties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import Page from '../Page'
import { Props } from './ThirdParties.types'
import { locations } from '../../modules/locations'
import './ThirdParties.css'
import { getThirdPartyVersion } from '../../modules/thirdParty/utils'

enum TABLE_FILTER {
ALL = 1,
Expand Down Expand Up @@ -58,7 +57,6 @@ const ThirdParties = ({ thirdParties, isLoading, isAggregator, userAddress }: Pr
<Table.HeaderCell>{t('third_parties.name')}</Table.HeaderCell>
<Table.HeaderCell>{t('third_parties.max_items')}</Table.HeaderCell>
<Table.HeaderCell>{t('third_parties.consumed_slots')}</Table.HeaderCell>
<Table.HeaderCell>{t('third_parties.version')}</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
Expand All @@ -68,7 +66,6 @@ const ThirdParties = ({ thirdParties, isLoading, isAggregator, userAddress }: Pr
<Table.Cell>{tp.metadata.name}</Table.Cell>
<Table.Cell>{tp.maxItems}</Table.Cell>
<Table.Cell>{tp.consumedSlots}</Table.Cell>
<Table.Cell>v{getThirdPartyVersion(tp)}</Table.Cell>
<Table.Cell textAlign="right">
<Popup
content={t('third_parties.details')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Dispatch } from 'redux'
import { RootState } from '../../../modules/reducer'
import { updateThirdPartyRequest, UPDATE_THIRD_PARTY_REQUEST } from '../../../modules/thirdParty/action'
import { getAggregatorAddress, getLoading } from '../../../modules/thirdParty/selectors'
import { getIsLinkedWearablesV2Enabled } from '../../../modules/features/selectors'
import UpdateThirdPartyForm from './UpdateThirdPartyForm'
import { MapDispatchProps, MapStateProps, OwnProps } from './UpdateThirdPartyForm.types'
import { toUpdateThirdParty } from './utils'
Expand All @@ -13,6 +14,7 @@ const mapState = (state: RootState, ownProps: OwnProps): MapStateProps => {
const userAddress = getAddress(state)
return {
isUpdating: isLoadingType(getLoading(state), UPDATE_THIRD_PARTY_REQUEST),
isThirdPartyV2Enabled: getIsLinkedWearablesV2Enabled(state),
canUpdate: Boolean(userAddress && (getAggregatorAddress(state) === userAddress || ownProps.thirdParty.managers.includes(userAddress)))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React from 'react'
import { Button, Field, Header } from 'decentraland-ui'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Controller, useForm } from 'react-hook-form'
Expand All @@ -7,11 +7,9 @@ import './UpdateThirdPartyForm.css'
import ManagersField from '../../ManagersField'
import { ContractsTable } from '../../ContractsTable'
import { ContractsField } from '../../ContractsField'
import { LinkedContract, ThirdPartyVersion } from '../../../modules/thirdParty/types'
import { getThirdPartyVersion } from '../../../modules/thirdParty/utils'
import { LinkedContract } from '../../../modules/thirdParty/types'

const UpdateThirdPartyForm = ({ thirdParty, canUpdate, isUpdating, onUpdateThirdParty }: Props) => {
const thirdPartyVersion = useMemo(() => getThirdPartyVersion(thirdParty), [thirdParty])
const UpdateThirdPartyForm = ({ thirdParty, canUpdate, isUpdating, isThirdPartyV2Enabled, onUpdateThirdParty }: Props) => {
const { control, handleSubmit } = useForm<UpdateThirdPartyFormData>({
disabled: isUpdating || !canUpdate,
defaultValues: {
Expand Down Expand Up @@ -67,14 +65,23 @@ const UpdateThirdPartyForm = ({ thirdParty, canUpdate, isUpdating, onUpdateThird
<Field label={t('update_third_party.description')} {...field} message={fieldState.error?.message} error={fieldState.invalid} />
)}
/>
{thirdPartyVersion === ThirdPartyVersion.V2 && (
{isThirdPartyV2Enabled && (
<Controller
name="contracts"
control={control}
render={({ field }) => (
rules={{
validate: (contracts: LinkedContract[]) => {
for (const contract of contracts) {
if (contracts.some(c => c.address === contract.address && c.network === contract.network)) {
return 'There are duplicated contracts in the list'
}
}
}
}}
render={({ field, fieldState }) => (
<>
<Header sub>{t('update_third_party.contracts')}</Header>
<ContractsTable contracts={field.value} />
<ContractsTable contracts={field.value} message={fieldState.error?.message} error={fieldState.invalid} />
<ContractsField
disabled={field.disabled}
onChange={(contract: LinkedContract) => field.onChange(field.value.concat(contract))}
Expand All @@ -83,22 +90,20 @@ const UpdateThirdPartyForm = ({ thirdParty, canUpdate, isUpdating, onUpdateThird
)}
/>
)}
{thirdPartyVersion === ThirdPartyVersion.V1 && (
<Controller
name="resolver"
control={control}
rules={{
validate: (value: string) => {
if (!/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_+.~#?&//=]*)/.test(value)) {
return t('update_third_party.required_valid_url')
}
<Controller
name="resolver"
control={control}
rules={{
validate: (value: string) => {
if (!/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_+.~#?&//=]*)/.test(value)) {
return t('update_third_party.required_valid_url')
}
}}
render={({ field, fieldState }) => (
<Field label={t('update_third_party.resolver')} {...field} message={fieldState.error?.message} error={fieldState.invalid} />
)}
/>
)}
}
}}
render={({ field, fieldState }) => (
<Field label={t('update_third_party.resolver')} {...field} message={fieldState.error?.message} error={fieldState.invalid} />
)}
/>
<Controller
name="slots"
control={control}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export type Props = {
thirdParty: ThirdParty
isUpdating: boolean
canUpdate: boolean
isThirdPartyV2Enabled: boolean
onUpdateThirdParty: (data: UpdateThirdPartyFormData) => void
}

export type MapStateProps = Pick<Props, 'isUpdating' | 'canUpdate'>
export type MapStateProps = Pick<Props, 'isUpdating' | 'canUpdate' | 'isThirdPartyV2Enabled'>
export type MapDispatchProps = Pick<Props, 'onUpdateThirdParty'>
export type OwnProps = Pick<Props, 'thirdParty'>
9 changes: 9 additions & 0 deletions src/lib/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const debounce = (fn: (...args: any[]) => void, ms: number) => {
let timeout: any = null
return (...args: any[]) => {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => fn(...args), ms)
}
}
Loading

0 comments on commit 1b0493b

Please sign in to comment.