Problem
There are two different "add a contact" implementations in src/components/ContactBook/:
AddContact.tsx — a properly validated component:
const submit = () => {
const e: typeof errors = {}
if (!name.trim()) e.name = 'Name is required'
if (!isValidStellarAddress(address)) e.address = 'Invalid Stellar address'
setErrors(e)
if (Object.keys(e).length === 0) onAdd({ name: name.trim(), address })
}
index.tsx — the component actually rendered by ContactBook, with its own inline, unvalidated form:
export function ContactBook() {
const { contacts, addContact, removeContact } = useContacts()
const [name, setName] = useState(''); const [addr, setAddr] = useState('')
const submit = () => { if (name && addr) { addContact({ name, address: addr }); setName(''); setAddr('') } }
return (
<div className='p-4'>
<h2 className='text-lg font-semibold mb-4'>Contacts ({contacts.length})</h2>
{contacts.map(c => <ContactItem key={c.address} contact={c} onRemove={() => removeContact(c.address)} />)}
<div className='mt-4 space-y-2'>
<input value={name} onChange={e => setName(e.target.value)} placeholder='Name' />
<input value={addr} onChange={e => setAddr(e.target.value)} placeholder='G... address' />
<button onClick={submit}>Add contact</button>
</div>
</div>
)
}
A repo-wide search (grep -rn "AddContact" src) turns up exactly one reference — AddContact.tsx's own function declaration. Nothing imports or renders AddContact anywhere. ContactBook/index.tsx is the component that's actually wired into the app, and its inline submit only checks that name and addr are non-empty truthy strings — no isValidStellarAddress call, no length/prefix check, nothing.
Why it matters
useContacts()'s addContact (in src/hooks/useContacts.ts) persists whatever { name, address } it's given straight into localStorage under ss-contacts, deduping only by exact address string equality. Since index.tsx's form performs no validation, a user can save a malformed, truncated, or otherwise invalid "address" as a contact.
- Anywhere a saved contact's address is later used to prefill a send/escrow/subscription form (
onSelect prop on ContactItem), an invalid address stored here will surface downstream as a confusing "Invalid Stellar address" error at send-time instead of being caught at the point of entry — or, if a downstream form doesn't re-validate carefully, could be submitted as-is.
- This is a "the good code is dead, the wired code is bad" pattern rather than a simple typo — meaning fixing
AddContact.tsx in isolation would do nothing for the shipped experience, and a future contributor changing AddContact.tsx might reasonably (but incorrectly) believe they've fixed the feature.
Reproduction
- Open the Contacts view.
- Type any non-empty garbage string (e.g.
"not-an-address") into the address field and click "Add contact."
- Observe it's accepted and persisted with no error —
AddContact.tsx's validation logic never runs because that component isn't part of the render tree.
Suggested fix
- Wire
AddContact.tsx in as the actual add-contact UI inside ContactBook/index.tsx (it already accepts the right onAdd/onCancel props), removing the duplicate inline form, or, if AddContact.tsx was an abandoned redesign, port its validation logic into index.tsx's inline form and delete the orphaned file to avoid future confusion.
- Either way,
index.tsx's submit must call isValidStellarAddress (from whichever module wins the consolidation described in the separate duplicate-helpers issue in this batch) before calling addContact.
- Consider deduping contacts more permissively too —
useContacts's addContact only dedupes on identical address string, so the same address with different casing or leading/trailing whitespace (index.tsx never trims addr, unlike AddContact.tsx which trims) would be treated as a distinct contact.
Edge cases
- Existing users who already saved invalid addresses via the current unvalidated form will have bad data sitting in
localStorage — the fix should probably also filter/warn on invalid entries when useContacts loads existing data, not just prevent new bad entries.
Testing strategy
- Add a test to
ContactBook's actual rendered component (there is currently no index.test.tsx at all — only SearchBar.test.tsx and EscrowItem.test.tsx-style tests exist elsewhere in the repo, none for this file) asserting that submitting an invalid address does not call addContact.
- Add a regression test asserting
AddContact.tsx (or whichever component wins) is actually present in ContactBook's render output via screen.getByPlaceholderText/getByLabelText queries scoped to the real exported ContactBook.
Related issues in this batch
Depends on the outcome of the duplicate isValidStellarAddress issue filed in this batch, and is thematically identical to the orphaned TransactionHistory component tree issue also in this batch — both are cases of a "second, unwired implementation" masking that the live code path is worse than it looks from reading the file tree alone.
Problem
There are two different "add a contact" implementations in
src/components/ContactBook/:AddContact.tsx— a properly validated component:index.tsx— the component actually rendered byContactBook, with its own inline, unvalidated form:A repo-wide search (
grep -rn "AddContact" src) turns up exactly one reference —AddContact.tsx's own function declaration. Nothing imports or rendersAddContactanywhere.ContactBook/index.tsxis the component that's actually wired into the app, and its inlinesubmitonly checks thatnameandaddrare non-empty truthy strings — noisValidStellarAddresscall, no length/prefix check, nothing.Why it matters
useContacts()'saddContact(insrc/hooks/useContacts.ts) persists whatever{ name, address }it's given straight intolocalStorageunderss-contacts, deduping only by exact address string equality. Sinceindex.tsx's form performs no validation, a user can save a malformed, truncated, or otherwise invalid "address" as a contact.onSelectprop onContactItem), an invalid address stored here will surface downstream as a confusing "Invalid Stellar address" error at send-time instead of being caught at the point of entry — or, if a downstream form doesn't re-validate carefully, could be submitted as-is.AddContact.tsxin isolation would do nothing for the shipped experience, and a future contributor changingAddContact.tsxmight reasonably (but incorrectly) believe they've fixed the feature.Reproduction
"not-an-address") into the address field and click "Add contact."AddContact.tsx's validation logic never runs because that component isn't part of the render tree.Suggested fix
AddContact.tsxin as the actual add-contact UI insideContactBook/index.tsx(it already accepts the rightonAdd/onCancelprops), removing the duplicate inline form, or, ifAddContact.tsxwas an abandoned redesign, port its validation logic intoindex.tsx's inline form and delete the orphaned file to avoid future confusion.index.tsx'ssubmitmust callisValidStellarAddress(from whichever module wins the consolidation described in the separate duplicate-helpers issue in this batch) before callingaddContact.useContacts'saddContactonly dedupes on identical address string, so the same address with different casing or leading/trailing whitespace (index.tsxnever trimsaddr, unlikeAddContact.tsxwhich trims) would be treated as a distinct contact.Edge cases
localStorage— the fix should probably also filter/warn on invalid entries whenuseContactsloads existing data, not just prevent new bad entries.Testing strategy
ContactBook's actual rendered component (there is currently noindex.test.tsxat all — onlySearchBar.test.tsxandEscrowItem.test.tsx-style tests exist elsewhere in the repo, none for this file) asserting that submitting an invalid address does not calladdContact.AddContact.tsx(or whichever component wins) is actually present inContactBook's render output viascreen.getByPlaceholderText/getByLabelTextqueries scoped to the real exportedContactBook.Related issues in this batch
Depends on the outcome of the duplicate
isValidStellarAddressissue filed in this batch, and is thematically identical to the orphanedTransactionHistorycomponent tree issue also in this batch — both are cases of a "second, unwired implementation" masking that the live code path is worse than it looks from reading the file tree alone.