mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-13 12:33:26 +02:00
113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
import { getAddressInfo, Network, validate as isValidBitcoinAddress } from 'bitcoin-address-validation'
|
|
import * as yup from 'yup'
|
|
import type { AddressSummary } from '@/context/JamWalletInfoContext'
|
|
import type { BitcoinAddress, BlockHeight, JarIndex } from '@/types/global'
|
|
import { isValidInteger } from './utils'
|
|
|
|
/**
|
|
* Shared bitcoin-address predicates used across form schemas (send, sweep, ...).
|
|
* Keeping these in one place ensures every form validates addresses the same way.
|
|
*/
|
|
export const isValidAddress = (value: unknown): value is BitcoinAddress =>
|
|
typeof value === 'string' && isValidBitcoinAddress(value)
|
|
|
|
// Legacy (base58) addresses share the same version bytes on testnet and regtest, so
|
|
// bitcoin-address-validation can't tell them apart and always labels them "testnet" -
|
|
// only bech32 addresses carry a distinct "bcrt1" prefix. Treat the two as interchangeable
|
|
// so a regtest wallet doesn't reject its own legacy-style addresses as "wrong network".
|
|
const AMBIGUOUS_TESTNET_REGTEST_NETWORKS: ReadonlySet<Network> = new Set([Network.testnet, Network.regtest])
|
|
|
|
export const isAddressOnNetwork = (value: string, network: Network): boolean => {
|
|
try {
|
|
const addressNetwork = getAddressInfo(value).network
|
|
if (addressNetwork === network) return true
|
|
return AMBIGUOUS_TESTNET_REGTEST_NETWORKS.has(addressNetwork) && AMBIGUOUS_TESTNET_REGTEST_NETWORKS.has(network)
|
|
} catch (_ignoredOnPurpose) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export const isReusedAddress = (value: string, addressSummary: AddressSummary): boolean =>
|
|
addressSummary[value]?.used === true
|
|
|
|
/**
|
|
* Shared "source jar" (`fromJar`) field validator.
|
|
*
|
|
* Callers provide the feedback message and the predicate that decides whether a jar index is
|
|
* selectable, e.g. "the jar exists" (receive / fidelity bond) or "the jar has spendable balance"
|
|
* (send). This keeps the yup rules for the common `fromJar` field aligned across forms.
|
|
*/
|
|
export const sourceJarField = (message: string, isSelectableJar: (jarIndex: JarIndex) => boolean) =>
|
|
yup
|
|
.number<JarIndex>()
|
|
.integer(message)
|
|
.required(message)
|
|
.test('valid-source-jar-index-test', message, (value) => typeof value === 'number' && isSelectableJar(value))
|
|
|
|
export type DestinationAddressMessages = {
|
|
invalid: string
|
|
networkMismatch: string
|
|
reused: string
|
|
}
|
|
|
|
/**
|
|
* Shared destination bitcoin-address field validator: valid address, matching network and not
|
|
* previously used. Forms that validate a single destination address (e.g. send) can reuse this
|
|
* instead of re-declaring the same chain of `.test(...)` rules.
|
|
*/
|
|
export const destinationAddressField = ({
|
|
network,
|
|
addressSummary,
|
|
messages,
|
|
}: {
|
|
network: Network
|
|
addressSummary: AddressSummary
|
|
messages: DestinationAddressMessages
|
|
}) =>
|
|
yup
|
|
.string()
|
|
.trim()
|
|
.required(messages.invalid)
|
|
.test('valid-address-test', messages.invalid, (value) => isValidAddress(value))
|
|
// The network and reuse checks only run once the address itself is valid, so an invalid
|
|
// address surfaces a single, correct error instead of also reporting a network mismatch.
|
|
.test(
|
|
'network-mismatch-test',
|
|
messages.networkMismatch,
|
|
(value) => !isValidAddress(value) || isAddressOnNetwork(value, network),
|
|
)
|
|
.test(
|
|
'reused-address-test',
|
|
messages.reused,
|
|
(value) =>
|
|
!isValidAddress(value) || !isAddressOnNetwork(value, network) || !isReusedAddress(value, addressSummary),
|
|
)
|
|
|
|
export type BlockHeightMessages = {
|
|
invalid: string
|
|
}
|
|
|
|
export const INPUT_BLOCK_HEIGHT_MIN = 0
|
|
const INPUT_BLOCK_HEIGHT_MAX = Number.MAX_SAFE_INTEGER
|
|
|
|
export const blockHeightField = ({
|
|
currentBlockHeight,
|
|
messages: { invalid },
|
|
}: {
|
|
currentBlockHeight: BlockHeight | undefined
|
|
messages: {
|
|
invalid: ({ min, max }: { min: BlockHeight; max: BlockHeight }) => string
|
|
}
|
|
}) => {
|
|
const minBlockHeight = Math.min(INPUT_BLOCK_HEIGHT_MIN, currentBlockHeight || INPUT_BLOCK_HEIGHT_MIN)
|
|
const maxBlockheight = Math.max(minBlockHeight, currentBlockHeight || INPUT_BLOCK_HEIGHT_MAX)
|
|
const invalidBlockheightMessage = invalid({ min: minBlockHeight, max: maxBlockheight })
|
|
|
|
return yup
|
|
.number()
|
|
.transform((value) => (isValidInteger(value) ? value : null))
|
|
.integer(invalidBlockheightMessage)
|
|
.min(minBlockHeight, invalidBlockheightMessage)
|
|
.max(maxBlockheight, invalidBlockheightMessage)
|
|
.required(invalidBlockheightMessage)
|
|
}
|