Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 193 additions & 20 deletions pages/services/account-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -1109,21 +1246,35 @@ export default function AccountSettings({
Clear
</button>
)}
<button className="button-action thin" onClick={handleSetMessageKey} disabled={!account?.address}>
<button
className="button-action thin"
onClick={handleSetMessageKey}
disabled={!account?.address || (messageKeyInput && !messageKeyValidation.isValid)}
>
Set
</button>
</div>
</div>
<div className="nft-minter-input">
<input
className="input-text"
placeholder="Hex-encoded public key"
className={`input-text ${messageKeyInput && !messageKeyValidation.isValid ? 'input-error' : messageKeyInput && messageKeyValidation.isValid ? 'input-valid' : ''}`}
placeholder="e.g., 020000000000000000000000000000000000000000000000000000000000000000"
value={messageKeyInput}
onChange={(e) => setMessageKeyInput(e.target.value)}
onChange={(e) => {
const value = e.target.value
setMessageKeyInput(value)
setMessageKeyValidation(validateMessageKey(value))
}}
type="text"
disabled={!account?.address}
maxLength={66}
/>
<small>Provide a hex-encoded public key; clearing removes it from the ledger.</small>
<small>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.</small>
{messageKeyInput && messageKeyValidation.message && (
<div className={`validation-message ${messageKeyValidation.isValid ? 'validation-success' : 'validation-error'}`}>
{messageKeyValidation.message}
</div>
)}
</div>
</div>

Expand Down Expand Up @@ -1177,22 +1328,35 @@ export default function AccountSettings({
)}
</div>
<div className="flag-info-buttons">
<button className="button-action thin" onClick={handleSetTickSize} disabled={!account?.address}>
<button
className="button-action thin"
onClick={handleSetTickSize}
disabled={!account?.address || (tickSizeInput && !tickSizeValidation.isValid)}
>
Set
</button>
</div>
</div>
<div className="nft-minter-input">
<input
className="input-text"
className={`input-text ${tickSizeInput && !tickSizeValidation.isValid ? 'input-error' : tickSizeInput && tickSizeValidation.isValid ? 'input-valid' : ''}`}
placeholder="0 to clear, or 3-15"
value={tickSizeInput}
onChange={(e) => setTickSizeInput(e.target.value)}
onChange={(e) => {
const value = e.target.value
setTickSizeInput(value)
setTickSizeValidation(validateTickSize(value))
}}
type="text"
inputMode="numeric"
disabled={!account?.address}
/>
<small>Controls significant digits for order book prices. 0 clears.</small>
{tickSizeInput && tickSizeValidation.message && (
<div className={`validation-message ${tickSizeValidation.isValid ? 'validation-success' : 'validation-error'}`}>
{tickSizeValidation.message}
</div>
)}
</div>
</div>

Expand All @@ -1217,23 +1381,32 @@ export default function AccountSettings({
<button
className="button-action thin"
onClick={handleSetWalletLocator}
disabled={!account?.address}
disabled={!account?.address || (walletLocatorInput && !walletLocatorValidation.isValid)}
>
Set
</button>
</div>
</div>
<div className="nft-minter-input">
<input
className="input-text"
placeholder="64 hex characters"
className={`input-text ${walletLocatorInput && !walletLocatorValidation.isValid ? 'input-error' : walletLocatorInput && walletLocatorValidation.isValid ? 'input-valid' : ''}`}
placeholder="e.g., 0000000000000000000000000000000000000000000000000000000000000000"
value={walletLocatorInput}
onChange={(e) => setWalletLocatorInput(e.target.value)}
onChange={(e) => {
const value = e.target.value
setWalletLocatorInput(value)
setWalletLocatorValidation(validateWalletLocator(value))
}}
type="text"
disabled={!account?.address}
maxLength={64}
/>
<small>Optional hash locator for your wallet application.</small>
<small>Optional 64-character hexadecimal hash locator for your wallet application.</small>
{walletLocatorInput && walletLocatorValidation.message && (
<div className={`validation-message ${walletLocatorValidation.isValid ? 'validation-success' : 'validation-error'}`}>
{walletLocatorValidation.message}
</div>
)}
</div>
</div>

Expand Down
29 changes: 29 additions & 0 deletions styles/pages/account-settings.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down