import { ArchWalletErrorException, ArchWalletErrorType, } from '../error/arch-wallet.error'; import { Address, NETWORK } from '@scure/btc-signer'; export interface ValidationRule { isValid: (value: T) => boolean; getException: (fieldName: string, value: T) => Error; } const base64Regex = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}(?:==)|[A-Za-z0-9+\/]{3}=)?$/; // X-only pubkey format const xOnlyPubkeyInputRegex = /^(?=.{64}$)[0-9a-fA-F]+$/; // Bitcoin pubkey format between 64 and 66 characters const pubkeyInputRegex = /^(?=.{64,66}$)[0-9a-fA-F]+$/; export const isIntegerPositiveNonZeroValue = (value: bigint) => value > 0n; export const isPositiveNonZeroValue = (value: number) => value > 0; export const isPubkeyInValidXOnlyFormat = (pubkey: string) => xOnlyPubkeyInputRegex.test(pubkey); export const isPubkeyInValidHexFormat = (pubkey: string) => pubkeyInputRegex.test(pubkey); export const isBase64String = (value: string) => base64Regex.test(value); const validateAddress = (address: string | null, network: typeof NETWORK) => { if (address === null) { return true; } try { const addressInfo = Address(network).decode(address); if (addressInfo.type === 'unknown') { return false; } return true; } catch (error) { return false; } }; const positiveBigIntRule: ValidationRule = { isValid: isIntegerPositiveNonZeroValue, getException: (fieldName: string, value: bigint) => new ArchWalletErrorException({ type: ArchWalletErrorType.InvalidFeeRate, message: `Field "${fieldName}" must be a positive non-zero bigint, but got ${value.toString()}.`, minFeeRate: 0, }), }; const xOnlyPubkeyRule: ValidationRule = { isValid: isPubkeyInValidXOnlyFormat, getException: (fieldName: string, value: string) => new ArchWalletErrorException({ type: ArchWalletErrorType.InvalidPubkey, message: `Field "${fieldName}" must be in x-only format with exactly 64 hex characters, but got ${value}.`, pubkey: value, }), }; const pubkeyRule: ValidationRule = { isValid: isPubkeyInValidHexFormat, getException: (fieldName: string, value: string) => new ArchWalletErrorException({ type: ArchWalletErrorType.InvalidPubkey, message: `Field "${fieldName}" must be in bitcoin pubkey format with exactly 66 hex characters, but got ${value}.`, pubkey: value, }), }; const createAddressValidationRule = ( network: typeof NETWORK, ): ValidationRule => { return { isValid: (address: string | null) => validateAddress(address, network), getException: (_: string, value: number) => new ArchWalletErrorException({ message: `Invalid address: ${value}`, type: ArchWalletErrorType.InvalidAddress, }), }; }; const poolSdkValidation = { feeRate: positiveBigIntRule, feeRateOracleAccount: xOnlyPubkeyRule, mempoolInfoOracleAccount: xOnlyPubkeyRule, programAccount: xOnlyPubkeyRule, amount: positiveBigIntRule, paymentPublicKey: pubkeyRule, runePublicKey: pubkeyRule, feePayerPublicKey: pubkeyRule, }; export const createArchWalletConfigValidationWithAddressRule = ( network: typeof NETWORK, ): Record => { const addressValidationRule = createAddressValidationRule(network); return { ...poolSdkValidation, address: addressValidationRule, runeAddress: addressValidationRule, paymentAddress: addressValidationRule, }; }; export function validateArchWalletConfigData( request: ArchWalletSdkConfig, ): ArchWalletSdkConfig { return runValidation(request, poolSdkValidation); } export function validateArchWalletData( request: T, network: typeof NETWORK, ): T { const poolSdkValidation = createArchWalletConfigValidationWithAddressRule(network); return runValidation(request, poolSdkValidation); } function runValidation( request: T, validationRules: Record, ): T { for (const key in request) { if (Object.hasOwn(validationRules, key)) { const value = request[key as keyof T]; if (value !== undefined && !validationRules[key].isValid(value)) { throw validationRules[key].getException(key, value); } } } return request; }