Skip to content

Commit

Permalink
feat(bridge): ens support in custom recipient input (#1341)
Browse files Browse the repository at this point in the history
  • Loading branch information
yqrashawn authored Nov 5, 2024
1 parent a732855 commit 5e2804d
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
layout node

dotenv_if_exists .env
3 changes: 1 addition & 2 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ REACT_APP_NFT_VIEWER_URL="https://nft.scroll.io"
REACT_APP_NFT_API_URI="https://nft.scroll.io"
REACT_APP_OKX_URI="https://www.okx.com/web3/explorer/scroll?channelId=scroll"
REACT_APP_BLOCKSCOUT_URI="https://scroll.blockscout.com"
REACT_APP_ENS_API_URL="https://ens.scroll.cat"

[context.deploy-preview.environment]
REACT_APP_SCROLL_ENVIRONMENT = "Staging"
Expand Down Expand Up @@ -298,5 +299,3 @@ REACT_APP_EAS_EXPLORER_URL = "https://scroll.easscan.org"
REACT_APP_ETHEREUM_YEAR_BADGE_API_URI = "https://nft.scroll.io"
REACT_APP_BADGE_REGISTRY_URL="https://badge-registry.canvas.scroll.cat"
REACT_APP_BADGE_INDEXER_URL="https://canvas-indexer.scroll.cat"


2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scroll.io",
"version": "5.3.0",
"version": "5.4.0",
"private": false,
"license": "MIT",
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions src/apis/ens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { requireEnv } from "@/utils"

const ensBaseURL = requireEnv("REACT_APP_ENS_API_URL")
export function getEnsAddressURL(ens: string) {
return `${ensBaseURL}/name/${ens}/address`
}
131 changes: 131 additions & 0 deletions src/hooks/useInputAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { isAddress } from "ethers"
import { identity } from "lodash"
import { useEffect } from "react"
import { namehash, normalize } from "viem/ens"
import { create } from "zustand"

import { getEnsAddressURL } from "@/apis/ens"

/**
* Fetches Ethereum address for an ENS name
* @param ens ENS name to resolve
* @returns Resolved Ethereum address or null if not found
*/
async function getEnsAddress(ens: string): Promise<string | null> {
return await scrollRequest(getEnsAddressURL(ens)).then(data => data?.address)
}

interface InputAddressStore {
inputValue: string
isValidInput: boolean
address: string | null
isValidAddress: boolean
ens: string | null
isValidEns: boolean
resolvingEns: boolean
ensServerError: string | null
validateAddress: () => void
validateEnsFormat: () => void
resolveEnsAddress: () => Promise<void>
validateEns: () => Promise<void>
setInputValue: (v: string) => void
setValidationStatus: () => void
}

const useInputAddressStore = create<InputAddressStore>((set, get) => ({
inputValue: "",
isValidInput: true,
address: null,
isValidAddress: false,
ens: null,
isValidEns: false,
resolvingEns: false,
ensServerError: null,
validateAddress() {
const addr = get().inputValue.trim()
if (isAddress(addr)) set({ address: addr })
else set({ address: null })
},
validateEnsFormat() {
set({ ens: null })
const ens = get().inputValue.trim()
if (!ens.endsWith(".eth")) return
try {
namehash(normalize(ens))
} catch (err) {
return
}
set({ ens })
},
async resolveEnsAddress() {
const { ens } = get()
if (!ens) return
set({ resolvingEns: true })
const address = await getEnsAddress(ens).catch(() => set({ ensServerError: "Failed to fetch ENS address" }))
set({ resolvingEns: false })
if (address !== undefined) return set({ ens, address, ensServerError: null })
},
async validateEns() {
set({ ensServerError: null, resolvingEns: false })

get().validateEnsFormat()
await get().resolveEnsAddress()
},
async setInputValue(v) {
if (v === get().inputValue) return
set({ inputValue: v || "" })

get().validateAddress()
await get().validateEns()
get().setValidationStatus()
},
setValidationStatus() {
const { inputValue, address, ens } = get()
// Empty input is valid
if (!inputValue) return set({ isValidAddress: false, isValidEns: false, isValidInput: true })
// Valid address + ENS is valid
if (address && ens) return set({ isValidAddress: true, isValidEns: true, isValidInput: true })
// Valid address but invalid ENS
if (address && !ens) return set({ isValidAddress: true, isValidEns: false, isValidInput: true })
return set({ isValidAddress: false, isValidEns: false, isValidInput: false })
},
}))

/**
* Hook for managing and validating Ethereum address/ENS input
* @param options.onValidAddress Callback when valid address is resolved
* @param options.onValidEnsName Callback when valid ENS name is resolved
* @returns Input address store state and methods
*/
export default function useInputAddress({
onValidAddress = identity,
onValidEnsName = identity,
onAddressChange = identity,
onEnsChange = identity,
} = {}) {
const store = useInputAddressStore()

useEffect(() => {
try {
if (store.address) {
onValidAddress(store.address)
onAddressChange(store.address)
} else {
onAddressChange(null)
}
} catch (err) {}
}, [store.address])

useEffect(() => {
try {
if (store.ens) {
onValidEnsName(store.ens)
onEnsChange(store.ens)
} else {
onEnsChange(null)
}
} catch (err) {}
}, [store.ens])

return store
}
67 changes: 35 additions & 32 deletions src/pages/bridge/Send/SendTransaction/CustomiseRecipient.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isAddress } from "ethers"
import { useEffect, useMemo, useState } from "react"
import { makeStyles } from "tss-react/mui"

Expand All @@ -8,6 +7,7 @@ import { ReactComponent as EditSvg } from "@/assets/svgs/bridge/edit.svg"
import { ReactComponent as RemoveSvg } from "@/assets/svgs/bridge/remove.svg"
import { ReactComponent as WarningSvg } from "@/assets/svgs/bridge/warning.svg"
import TextButton from "@/components/TextButton"
import useInputAddress from "@/hooks/useInputAddress"

const useStyles = makeStyles()(theme => ({
title: {
Expand All @@ -33,44 +33,47 @@ const useStyles = makeStyles()(theme => ({
const CustomiseRecipient = props => {
const { handleChangeRecipient, bridgeWarning, disabled, readOnly } = props
const { classes, cx } = useStyles()

const [isEditing, setIsEditing] = useState(false)

const [recipient, setRecipient] = useState("")
const [isValidate, setIsValidate] = useState(false)

const handleChangeAddress = recipient => {
setRecipient(recipient.trim())
}
const [enableCustomRecipient, setEnableCustomRecipient] = useState(false)
const {
ens,
inputValue: addressInputValue,
isValidInput,
isValidEns,
isValidAddress,
resolvingEns,
ensServerError,
setInputValue: setAddressInputValue,
} = useInputAddress({
onAddressChange: handleChangeRecipient,
})

useEffect(() => {
if (isAddress(recipient)) {
handleChangeRecipient(recipient)
setIsValidate(true)
} else {
handleChangeRecipient(null)
setIsValidate(false)
}
}, [recipient])
if (!enableCustomRecipient) setAddressInputValue("")
}, [enableCustomRecipient])

useEffect(() => {
if (!isEditing) {
setRecipient("")
}
}, [isEditing])
const invalidUseInputAddressErrorMessage = useMemo(() => {
if (isValidInput) return
if (resolvingEns) return "Resolving ENS..."
if (ensServerError) return ensServerError
if (ens && !isValidEns) return "Invalid ENS name"
if (!isValidAddress && !isValidEns) return "Invalid wallet address or ENS name"
if (!isValidAddress) return "Invalid wallet address"
if (!isValidEns) return "Invalid ENS name"
}, [resolvingEns, isValidInput, isValidAddress, isValidEns, ensServerError])

const showErrorMessage = useMemo(() => {
return recipient && !isValidate && !(!!bridgeWarning && bridgeWarning !== ">0")
}, [isValidate, recipient, bridgeWarning])
return resolvingEns || (!isValidInput && !(!!bridgeWarning && bridgeWarning !== ">0"))
}, [isValidInput, bridgeWarning, resolvingEns])

return (
<Box sx={{ width: "100%", opacity: !!bridgeWarning && bridgeWarning !== ">0" ? "0.3" : 1 }}>
{isEditing ? (
{enableCustomRecipient ? (
<Box>
<Stack direction="row" justifyContent="space-between" sx={{ mb: "0.4rem" }}>
<Typography className={classes.title} variant="h5">
Customise recipient
</Typography>
<Typography onClick={() => setIsEditing(false)} className={classes.title}>
<Typography onClick={() => setEnableCustomRecipient(false)} className={classes.title}>
<SvgIcon sx={{ fontSize: "1.6rem", marginRight: "0.4rem" }} component={RemoveSvg} inheritViewBox />
<span style={{ color: "#FF684B" }}>Remove</span>
</Typography>
Expand All @@ -88,9 +91,9 @@ const CustomiseRecipient = props => {
borderRadius: "1rem",
}}
disabled={disabled}
onChange={v => handleChangeAddress(v.target.value)}
placeholder="Enter a different wallet address"
value={recipient}
onChange={v => setAddressInputValue(v.target.value)}
placeholder="Enter a different wallet address or ENS name"
value={addressInputValue}
/>
</Stack>
</Box>
Expand All @@ -99,7 +102,7 @@ const CustomiseRecipient = props => {
className={cx(disabled && classes.disabledButton, readOnly && classes.readOnlyButton, classes.title)}
disabled={disabled}
readOnly={readOnly}
onClick={() => setIsEditing(true)}
onClick={() => setEnableCustomRecipient(true)}
>
Customise recipient
<SvgIcon sx={{ fontSize: "1.6rem", marginLeft: "0.4rem" }} component={EditSvg} inheritViewBox></SvgIcon>
Expand All @@ -117,7 +120,7 @@ const CustomiseRecipient = props => {
direction="row"
style={{ fontSize: "1.6rem", display: "inline-flex", verticalAlign: "middle", alignItems: "center", color: "#FF684B" }}
>
Invalid wallet address
{invalidUseInputAddressErrorMessage}
</Stack>
</Box>
) : null}
Expand Down

0 comments on commit 5e2804d

Please sign in to comment.