← 返回 MegaWallet 工作文档

MEGAWALLET · WORKING DOCUMENT 06

useAddressValidation

原始文件 · my-history/6.UseValidation.md

一个面向“Recipient 地址输入”流程的小型校验工具集。


快速上手(先看这个)

最简单的用法:把 debouncedSetValue 绑到输入框(用户打字),等 isValidating=falseisValid=true 时,直接使用 address 作为最终可用地址。

import { useAddressValidation, type AddressValidationScope } from '@megaeth-labs/wallet-hooks'

export function RecipientInput({ scopes }: { scopes: AddressValidationScope[] }) {
  const { value, debouncedSetValue, isValid, isValidating, error, address, validate } = useAddressValidation({
    scopes,
    validateOnChange: true,
  })

  const onSubmit = async () => {
    // 常见场景:实时校验已完成,直接用 address 就够
    if (isValidating || !isValid || !address) return
    console.log(address)

    // 如果你希望“点击提交时强制以当前输入做一次立即校验”
    //(例如不禁用按钮、或者 validateOnChange=false),可以改成:
    // const result = await validate()
    // if (result.isValid) console.log(result.address)
  }

  // <Input value={value} onChangeText={debouncedSetValue} />
  // <Button disabled={isValidating || !isValid} onPress={onSubmit} />
  // {(!isValidating && error) ? <Text>{error}</Text> : null}
  return null
}

想加更多规则(禁止转给自己、域名解析、合约地址提示、通讯录校验等),直接看后面的 Recipes真实 UI 示例

概念

scopes

scopes 表示当前允许的链/网络范围(多链校验:任一 scope 校验通过即可),每个 scope 可用两种形式:

AddressValidationResult

类型上它是一个可判别联合(discriminated union),推荐这样用:

const result = await validate()
if (result.isValid) {
  result.address // string
} else {
  result.errorCode // AddressValidationErrorCode
}

AddressValidationStepLevel

有两档:

UI 层面的样式区分(如黄色警告 vs 灰色提示)由调用方根据 validator name 或自定义属性决定。

ADDRESS_VALIDATION_ERROR_CODE

错误码常量,类型 AddressValidationErrorCode 由此推导:

import { ADDRESS_VALIDATION_ERROR_CODE } from '@megaeth-labs/wallet-hooks'

ADDRESS_VALIDATION_ERROR_CODE.EMPTY              // 空输入
ADDRESS_VALIDATION_ERROR_CODE.SKIPPED            // 非空输入但该校验器选择跳过(如:不是域名 / 未配置错误提示的列表校验未命中)
ADDRESS_VALIDATION_ERROR_CODE.INVALID_FORMAT     // 地址格式错误
ADDRESS_VALIDATION_ERROR_CODE.NOT_IN_LIST        // 不在列表中
ADDRESS_VALIDATION_ERROR_CODE.SELF_TRANSFER      // 发送给自己
ADDRESS_VALIDATION_ERROR_CODE.IS_CONTRACT        // 是合约地址
ADDRESS_VALIDATION_ERROR_CODE.NOT_CONTRACT       // 不是合约地址
ADDRESS_VALIDATION_ERROR_CODE.DOMAIN_UNSUPPORTED // 域名不支持当前网络
ADDRESS_VALIDATION_ERROR_CODE.DOMAIN_RESOLVE_FAILED // 域名解析失败
ADDRESS_VALIDATION_ERROR_CODE.DOMAIN_INVALID_RESULT // 域名解析结果无效
ADDRESS_VALIDATION_ERROR_CODE.RPC_ERROR          // RPC 调用错误
ADDRESS_VALIDATION_ERROR_CODE.UNKNOWN            // 未知错误

AddressValidatorAddressValidatorEntry

为什么要分成两种:

示例:

// 纯 AddressValidator(自动命名 validator1,level 默认 error)
const v1 = createAddressFormatValidator(scope)

// 带元信息的 Entry(命名步骤 + 非阻塞 info)
const v2 = {
  name: 'recent',
  level: 'info',
  validator: createInListValidator({ list: recentList }),
}

// 组合器接收 AddressValidatorEntry,并返回一个 AddressValidator
const validator = createAllOfValidator(v1, v2)

API

Hook

useAddressValidation(options)

useAddressValidation({
  initialValue?: string
  scopes?: AddressValidationScope[]
  validator?: AddressValidator
  validateOnChange?: boolean  // 默认 true
  debounceMs?: number         // 防抖延迟,默认 300ms
  settleMs?: number           // UI 软提示延迟,默认 600ms
  mode?: 'address' | 'addressOrDomain' // 默认 'address'
  resolveDomain?: (domain: string, scopeId: Caip2ChainId) => Promise<string | null>
  queryOptions?: {
    validatorKey?: string     // 区分不同 validator 的缓存 key
    staleTime?: number        // 缓存过期时间,默认 5 分钟
    gcTime?: number           // 缓存保留时间,默认 10 分钟
    retry?: boolean | number  // 是否重试,默认 false
    refetchOnWindowFocus?: boolean  // 默认 false
    refetchOnMount?: boolean  // 默认 false
  }
})

validateOnChange

queryOptions

基于 @tanstack/react-query 的缓存配置。缓存 key 由 [scopesKey, validatorKey, input] 组成(其中 scopesKey 是 scopes 里所有 scopeId 去重+排序后用 | 拼接)。

mode

重要:validator 必须用 useMemo 稳定引用

缓存 key 不包含 validator 函数引用。如果每次渲染都传入新的 validator 函数,缓存会复用但 validator 逻辑可能已变,导致结果不一致。

// ✅ 正确:用 useMemo 稳定 validator
const validator = useMemo(
  () => createAllOfValidator(createAddressFormatValidator(scope), createNoSelfTransferValidator({ currentAddress })),
  [scope, currentAddress]
)
const { isValid } = useAddressValidation({
  validator,
  queryOptions: { validatorKey: 'send-recipient' }
})

// ❌ 错误:每次渲染都创建新函数
const { isValid } = useAddressValidation({
  validator: createAllOfValidator(createAddressFormatValidator(scope), createNoSelfTransferValidator({ currentAddress })), // 每次都是新引用
  queryOptions: { validatorKey: 'send-recipient' }
})

返回:

{
  value: string
  isValid: boolean
  isValidating: boolean
  isSettled: boolean
  looksComplete: boolean
  looksLikeAddress: boolean
  looksLikeDomain: boolean
  error: string
  errorCode?: AddressValidationErrorCode
  address?: string
  steps?: AddressValidationResult['steps']
  setValue(value: string): void
  debouncedSetValue(value: string): void
  validate(): Promise<AddressValidationResult>
  reset(): void
}

setValue vs debouncedSetValue

validate()

isValidating

scopes 变化自动重新校验

scopes 变化时(如用户切换网络/允许链集合变化),hook 会自动用当前 value 重新触发校验,无需手动 useEffect

受控 / 非受控输入说明

useAddressValidation 内部维护自己的 value 状态,并且 validate() 永远以当前 value 为输入(没有 validate(rawValue) 这种 API)。

react-hook-form 集成

如果你在表单里使用,建议明确单一数据源,避免“RHF 一份值 + hook 一份值”的双状态漂移:

const validator = useMemo(() => createAddressFormatValidator(scopeId), [scopeId])

<Controller
  control={control}
  name="recipient"
  rules={{
    validate: async (v) => {
      const r = await validator(v)
      return r.isValid || r.error || 'Invalid address'
    },
  }}
  render={({ field: { value, onChange, onBlur }, fieldState: { error } }) => (
    <>
      <Input value={value} onChangeText={onChange} onBlur={onBlur} />
      {error?.message ? <Text>{error.message}</Text> : null}
    </>
  )}
/>

核心 validators(validators.ts

辅助函数(core.ts

用于自定义 validator:

import {
  emptyAddressValidationResult,
  skippedAddressValidationResult,
  invalidAddressValidationResult,
  validAddressValidationResult,
  ADDRESS_VALIDATION_ERROR_CODE,
} from '@megaeth-labs/wallet-hooks'

emptyAddressValidationResult                                    // { isValid: false, error: '', errorCode: 'EMPTY' }
skippedAddressValidationResult                                  // { isValid: false, error: '', errorCode: 'SKIPPED' }
invalidAddressValidationResult('error message', ADDRESS_VALIDATION_ERROR_CODE.UNKNOWN)  // { isValid: false, error, errorCode }
validAddressValidationResult(address)                           // { isValid: true, error: '', address }

可选 validators(enhance/validators.ts

组合器(combinators)

不使用 hook(非 React 场景)

如果你在 React 之外(例如 service / util / test)需要校验,直接调用组合后的 validator

const validator = createAllOfValidator(
  { name: 'address', validator: createAddressFormatValidator(scope) },
  { name: 'recent', level: 'info', validator: createInListValidator({ list: recentList }) },
)

const result = await validator(input)

常见组合(Recipes)

1) 仅校验地址(快,同步)

const { setValue, validate } = useAddressValidation({ scopes: [scope] })
// 默认 validator 是 createAddressFormatValidator(scope) 的多链版(scopes 任一通过即可)

2) 地址 或 域名(域名必须能 resolve 成可用地址)

import { addressUtils } from '@megaeth-labs/wallet-utils'

const addressOrDomain = createBranchValidator({
  when: (s) => addressUtils.isValidWeb3Domain(s),
  // 你可以传入自定义 resolve(支持 SNS/UD 等),或者不传用内置 ENS(.eth) 解析:
  // - createDomainToAddressValidator({ scope })
  onMatch: { name: 'domain', validator: createDomainToAddressValidator({ scope, resolve }) }, // 或者 { scope }
  onMismatch: { name: 'address', validator: createAddressFormatValidator(scope) },
})

3) 必须在通讯录中(阻塞)

const validator = createAllOfValidator(
  { name: 'address', validator: createAddressFormatValidator(scope) },
  { name: 'addressBook', validator: createInListValidator({ list: addressBook, error: 'Not in address book' }) },
)

4) 是否最近收款人(仅 info,不阻塞)

const validator = createAllOfValidator(
  { name: 'address', validator: createAddressFormatValidator(scope) },
  { name: 'recent', level: 'info', validator: createInListValidator({ list: recentList }) },
)

5) 合约地址检查(info,不阻塞)

const validator = createAllOfValidator(
  { name: 'address', validator: createAddressFormatValidator(scope) },
  { name: 'notContract', level: 'info', validator: createEvmContractAddressValidator({ scope, mode: 'notContract' }) },
)

6) 禁止发送给自己

const validator = createAllOfValidator(
  { name: 'address', validator: createAddressFormatValidator(scope) },
  { name: 'notSelf', validator: createNoSelfTransferValidator({ currentAddress: myAddress }) },
)

7) 获取所有步骤结果(UI 用)

const result = await validate()

result.isValid
result.address
result.errorCode  // 用于国际化
result.steps?.forEach((s) => {
  // s.level: error/info
  // s.isValid: 该检查是否通过
  // s.errorCode: 用于国际化
})

在真实 UI 中使用 useAddressValidation

展示“阻塞错误 + 非阻塞提示 + loading”

import {
  createAllOfValidator,
  createAddressFormatValidator,
  createNoSelfTransferValidator,
  createAddressBookValidatorEntry,
  createRecentSendValidatorEntry,
  useAddressValidation,
  ADDRESS_VALIDATION_ERROR_CODE,
} from '@megaeth-labs/wallet-hooks'
import { useMemo } from 'react'
import { Text, View, ActivityIndicator } from 'react-native'

export function RecipientField({ scope, currentAddress }: { scope: string; currentAddress: string }) {
  const validator = useMemo(
    () =>
      createAllOfValidator(
        { name: 'address', validator: createAddressFormatValidator(scope) },
        // 禁止发送给自己
        { name: 'notSelf', validator: createNoSelfTransferValidator({ currentAddress }) },
        // 不在通讯录则提示,但不阻塞
        createAddressBookValidatorEntry({
          scope,
          level: 'info',
          error: 'Not in address book',
        }),
        // 是最近收款人则显示徽标
        createRecentSendValidatorEntry({ scope, level: 'info' }),
      ),
    [scope, currentAddress],
  )

  const {
    value,
    setValue,
    debouncedSetValue,
    isValid,
    isValidating,
    error,
    errorCode,
    steps,
    validate,
  } = useAddressValidation({
    scopes: [scope],
    validator,
    validateOnChange: true,
    debounceMs: 300,
    queryOptions: {
      validatorKey: 'recipientField',
      staleTime: 5 * 60 * 1000, // 5 分钟缓存
    },
  })

  // 非阻塞提示(根据 name 区分样式)
  const hints = steps?.filter((s) => s.level === 'info' && !s.isValid && !!s.error) ?? []
  const badges = steps?.filter((s) => s.level === 'info' && s.isValid).map((s) => s.name) ?? []

  return (
    <View>
      {/* 你的 Input:打字用 debouncedSetValue,粘贴用 setValue */}
      {/* <Input value={value} onChangeText={debouncedSetValue} onPaste={(e) => setValue(e.text)} /> */}

      {/* loading 状态 */}
      {isValidating && <ActivityIndicator />}

      {/* 阻塞错误(通常只显示一个) */}
      {!isValid && error ? <Text style={{ color: 'red' }}>{error}</Text> : null}

      {/* 非阻塞提示(调用方根据 name 决定样式) */}
      {hints.map((h) => (
        <Text key={h.name} style={{ color: h.name === 'notContract' ? 'orange' : 'gray' }}>
          {h.error}
        </Text>
      ))}

      {/* badges */}
      {badges.map((name) => (
        <Text key={name}>{name}</Text>
      ))}

      {/* 提交 */}
      {/* <Button onPress={async () => { const r = await validate(); if (r.isValid) onNext(r.address!) }} /> */}
    </View>
  )
}

说明:


Service 增强(通讯录 / 最近发送)

这些能力在 enhance/service.ts 中提供,属于wallet-service 强耦合的纯函数(不是 React hooks):

示例:

const addressBook = createAddressBookValidatorEntry({ scope, level: 'error', error: 'Not in address book' })
const recent = createRecentSendValidatorEntry({ scope, level: 'info' })

const validator = createAllOfValidator(
  { name: 'address', validator: createAddressFormatValidator(scope) },
  addressBook,
  recent,
)

注意:


调用方自定义

自定义黑名单地址(如零地址)

import {
  emptyAddressValidationResult,
  invalidAddressValidationResult,
  validAddressValidationResult,
  ADDRESS_VALIDATION_ERROR_CODE,
  type AddressValidator,
} from '@megaeth-labs/wallet-hooks'

const zeroAddressValidator = (): AddressValidator => {
  const blacklist = new Set([
    '0x0000000000000000000000000000000000000000',
    '0x000000000000000000000000000000000000dead',
  ])
  return (address) => {
    const input = address.trim().toLowerCase()
    if (!input) return emptyAddressValidationResult
    if (blacklist.has(input)) {
      return invalidAddressValidationResult('Cannot send to zero/burn address', ADDRESS_VALIDATION_ERROR_CODE.UNKNOWN)
    }
    return validAddressValidationResult(address.trim())
  }
}

const validator = createAllOfValidator(
  createAddressFormatValidator(scope),
  { name: 'notZero', validator: zeroAddressValidator() },
)