diff --git a/pages/services/account-settings.js b/pages/services/account-settings.js index c1809a37..1ef72d1a 100644 --- a/pages/services/account-settings.js +++ b/pages/services/account-settings.js @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react' import Link from 'next/link' import { useTranslation } from 'next-i18next' import axios from 'axios' -import { xahauNetwork, explorerName, nativeCurrency, isAddressValid, encode, isEmailValid, md5 } from '../../utils' +import { xahauNetwork, explorerName, nativeCurrency, isAddressValid, encode, isEmailValid, md5, isHexString } from '../../utils' import { multiply, subtract } from '../../utils/calc' import SEO from '../../components/SEO' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' @@ -73,6 +73,133 @@ export default function AccountSettings({ const [currentTickSize, setCurrentTickSize] = useState(null) const [walletLocatorInput, setWalletLocatorInput] = useState('') const [currentWalletLocator, setCurrentWalletLocator] = useState('') + + // Validation states + const [messageKeyValidation, setMessageKeyValidation] = useState({ isValid: true, message: '' }) + const [walletLocatorValidation, setWalletLocatorValidation] = useState({ isValid: true, message: '' }) + const [tickSizeValidation, setTickSizeValidation] = useState({ isValid: true, message: '' }) + + const validateInput = (value, options = {}) => { + const { + allowEmpty = true, + evenLength = true, + minChars, + exactChars, + successMessage = 'Valid input' + } = options + + const trimmed = value.trim() + + if (!trimmed) { + return { isValid: allowEmpty, message: '' } + } + + if (!isHexString(trimmed.toUpperCase())) { + return { + isValid: false, + message: 'Must contain only hexadecimal characters (0-9, a-f, A-F)' + } + } + + if (evenLength && trimmed.length % 2 !== 0) { + return { + isValid: false, + message: 'Must have an even number of characters (pairs of hex digits)' + } + } + + if (typeof exactChars === 'number') { + if (trimmed.length !== exactChars) { + return { + isValid: false, + message: `Must be exactly ${exactChars} characters (current: ${trimmed.length})` + } + } + } else if (typeof minChars === 'number') { + if (trimmed.length < minChars) { + return { + isValid: false, + message: `Must be at least ${minChars} characters (${minChars / 2} bytes)` + } + } + } + + return { isValid: true, message: successMessage } + } + + // Validation functions + const validateMessageKey = (value) => { + const trimmed = value.trim() + // Check the first byte for valid key type prefixes + const firstByte = trimmed.substring(0, 2).toUpperCase() + const validPrefixes = ['02', '03', 'ED'] + + if (!validPrefixes.includes(firstByte)) { + return { + isValid: false, + message: `First byte must be 02 or 03 for secp256k1 keys, or ED for Ed25519 keys. Current: 0x${firstByte}` + } + } + return validateInput(value, { + allowEmpty: true, + evenLength: true, + exactChars: 66, + successMessage: 'Valid 66-character hex string' + }) + } + + const validateWalletLocator = (value) => { + return validateInput(value, { + allowEmpty: true, + evenLength: true, + exactChars: 64, + successMessage: 'Valid 64-character hex string' + }) + } + + const validateTickSize = (value) => { + const trimmed = value.trim() + + if (!trimmed) { + return { isValid: true, message: '' } // Empty is valid (will be cleared) + } + + const numValue = Number(trimmed) + + if (isNaN(numValue)) { + return { + isValid: false, + message: 'Must be a valid number' + } + } + + if (!Number.isInteger(numValue)) { + return { + isValid: false, + message: 'Must be a whole number (integer)' + } + } + + if (numValue < 0) { + return { + isValid: false, + message: 'Must be 0 or positive' + } + } + + if (numValue === 0) { + return { isValid: true, message: 'Valid (will clear tick size)' } + } + + if (numValue < 3 || numValue > 15) { + return { + isValid: false, + message: 'Must be between 3 and 15 (or 0 to clear)' + } + } + + return { isValid: true, message: 'Valid tick size' } + } // TF flags state const [tfFlags, setTfFlags] = useState(null) @@ -553,9 +680,14 @@ export default function AccountSettings({ const handleSetMessageKey = () => { const value = messageKeyInput.trim() - const isHex = /^[0-9a-fA-F]+$/.test(value) - if (!value || !isHex || value.length % 2 !== 0) { - setErrorMessage('Please enter a valid hex-encoded MessageKey.') + if (!value) { + setErrorMessage('MessageKey cannot be empty. Please enter a hex-encoded public key.') + return + } + + const validation = validateMessageKey(value) + if (!validation.isValid) { + setErrorMessage(`MessageKey ${validation.message}`) return } const tx = { @@ -699,9 +831,14 @@ export default function AccountSettings({ const handleSetWalletLocator = () => { const value = walletLocatorInput.trim() - const isValid = /^[0-9a-fA-F]{64}$/.test(value) - if (!isValid) { - setErrorMessage('Please enter a valid WalletLocator (64 hexadecimal characters).') + if (!value) { + setErrorMessage('WalletLocator cannot be empty. Please enter a 64-character hexadecimal string.') + return + } + + const validation = validateWalletLocator(value) + if (!validation.isValid) { + setErrorMessage(`WalletLocator ${validation.message}`) return } const tx = { @@ -1109,21 +1246,35 @@ export default function AccountSettings({ Clear )} -
setMessageKeyInput(e.target.value)} + onChange={(e) => { + const value = e.target.value + setMessageKeyInput(value) + setMessageKeyValidation(validateMessageKey(value)) + }} type="text" disabled={!account?.address} + maxLength={66} /> - Provide a hex-encoded public key; clearing removes it from the ledger. + Provide a hex-encoded public key (exactly 66 characters/33 bytes). First byte must be 0x02 or 0x03 for secp256k1 keys, or 0xED for Ed25519 keys. Used for encrypted messaging. + {messageKeyInput && messageKeyValidation.message && ( +
+ {messageKeyValidation.message} +
+ )}
@@ -1177,22 +1328,35 @@ export default function AccountSettings({ )}
-
setTickSizeInput(e.target.value)} + onChange={(e) => { + const value = e.target.value + setTickSizeInput(value) + setTickSizeValidation(validateTickSize(value)) + }} type="text" inputMode="numeric" disabled={!account?.address} /> Controls significant digits for order book prices. 0 clears. + {tickSizeInput && tickSizeValidation.message && ( +
+ {tickSizeValidation.message} +
+ )}
@@ -1217,7 +1381,7 @@ export default function AccountSettings({ @@ -1225,15 +1389,24 @@ export default function AccountSettings({
setWalletLocatorInput(e.target.value)} + onChange={(e) => { + const value = e.target.value + setWalletLocatorInput(value) + setWalletLocatorValidation(validateWalletLocator(value)) + }} type="text" disabled={!account?.address} maxLength={64} /> - Optional hash locator for your wallet application. + Optional 64-character hexadecimal hash locator for your wallet application. + {walletLocatorInput && walletLocatorValidation.message && ( +
+ {walletLocatorValidation.message} +
+ )}
diff --git a/styles/pages/account-settings.module.scss b/styles/pages/account-settings.module.scss index 3bf51ccc..a9e7fdb7 100644 --- a/styles/pages/account-settings.module.scss +++ b/styles/pages/account-settings.module.scss @@ -141,6 +141,35 @@ opacity: 0.7; } } + + .input-error { + border-color: #dc3545 !important; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important; + } + + .input-valid { + border-color: #28a745 !important; + box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25) !important; + } + + .validation-message { + margin-top: 0.5rem; + font-size: 12px; + padding: 4px 8px; + border-radius: 4px; + + &.validation-error { + color: #dc3545; + background-color: rgba(220, 53, 69, 0.1); + border: 1px solid rgba(220, 53, 69, 0.2); + } + + &.validation-success { + color: #28a745; + background-color: rgba(40, 167, 69, 0.1); + border: 1px solid rgba(40, 167, 69, 0.2); + } + } } .advanced-options {