Skip to content

isValidStellarAddress, truncateAddress, and formatAmount are implemented twice with diverging behavior (utils/stellar.ts vs lib/stellar.ts) #18

Description

@abayomicornelius

Problem

This codebase has two independent, actively-imported implementations of the same three helpers:

src/utils/stellar.ts (imported by components/ContactBook/AddContact.tsx):

import { StrKey } from '@stellar/stellar-sdk'
export const isValidStellarAddress = (a: string) => StrKey.isValidEd25519PublicKey(a)
export const truncateAddress = (a: string, c = 6) => a.length <= c*2 ? a : a.slice(0,c)+'…'+a.slice(-c)
export const xlmToStroops = (x: number) => BigInt(Math.round(x * 10_000_000))
export const stroopsToXlm = (s: bigint) => Number(s) / 10_000_000

src/lib/stellar.ts (imported by BatchForm.tsx, EscrowForm.tsx, SubscriptionForm.tsx, BatchConfirmModal.tsx, ConfirmEscrowModal.tsx, ConfirmSubscriptionModal.tsx, RequestItem.tsx, and more — this is the one the bulk of the app actually uses):

export function isValidStellarAddress(address: string): boolean {
  try {
    Keypair.fromPublicKey(address)
    return true
  } catch {
    return false
  }
}

export function truncateAddress(address: string, chars = 6): string {
  if (!address) return ''
  return `${address.slice(0, chars)}...${address.slice(-chars)}`
}

export function formatAmount(amount: string | number, decimals = 2, symbol = ''): string {
  const num = typeof amount === 'string' ? parseFloat(amount) : amount
  if (isNaN(num)) return '—'
  const formatted = new Intl.NumberFormat('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(num)
  return symbol ? `${formatted} ${symbol}` : formatted
}

(src/utils/format.ts additionally has a third, differently-signed formatAmount(n: number, d = 2) that only accepts number, not string | number.)

Why it matters — concrete, observable divergences, not just "duplication is bad"

  1. truncateAddress has no short-input guard in lib/stellar.ts. utils/stellar.ts's version explicitly checks a.length <= c*2 and returns the address unmodified if it's too short to usefully truncate. lib/stellar.ts's version has no such check — for any address (or, more realistically, any non-address string accidentally passed through, e.g. a truncated/placeholder value) shorter than 2 * chars, slice(0, chars) and slice(-chars) overlap, producing a garbled, overlapping-substring result (e.g. an 8-character string truncated with chars = 6 prints characters 0–5 and 2–7, silently duplicating the middle).
  2. Different ellipsis rendering — one uses the character, the other uses literal ... — so truncated addresses render inconsistently across the app depending on which import a given component happened to use.
  3. isValidStellarAddress uses different underlying validation. StrKey.isValidEd25519PublicKey (utils) strictly validates a base32-encoded Ed25519 public key. Keypair.fromPublicKey (lib) goes through the SDK's keypair construction path — for most well-formed G... addresses these agree, but they are two independently-maintained code paths that can silently drift apart on SDK upgrades (e.g. future support for M... muxed addresses, or changes in how the SDK surfaces checksum-failure errors) with nothing forcing them to be kept in sync, since neither file references the other.
  4. formatAmount's signature is genuinely different: lib/stellar.ts's version takes an optional trailing symbol argument and accepts string | number; utils/format.ts's only accepts number. A component built against one signature cannot be trivially switched to the other without a call-site rewrite.

Reproduction

  • grep -rn "from '@/utils/stellar'" src vs grep -rn "from '@/lib/stellar'" src shows both are live, non-dead imports.
  • Feed a 6-character string into both truncateAddress implementations with the default chars = 6 and compare outputs — lib/stellar.ts's version double-renders overlapping characters where utils/stellar.ts's returns the string unchanged.

Suggested fix (flagging as spike — this needs a consolidation decision, not just a patch)

  • Pick one canonical location (given lib/stellar.ts has far more call sites, it's the more likely keeper) and re-export or delete the other, updating the one remaining import site (AddContact.tsx) accordingly.
  • Before deleting either, write characterization tests against both implementations' current behavior so the migration doesn't silently change behavior for existing call sites (e.g. if AddContact.tsx's users are relying on utils/stellar.ts's short-string guard, the consolidated version needs to keep it).
  • Decide whether StrKey.isValidEd25519PublicKey or Keypair.fromPublicKey-based validation is actually the more correct/future-proof choice (e.g. does the codebase need to eventually accept M... muxed addresses? StrKey has separate isValidMed25519PublicKey for that, which neither current implementation exposes).

Testing strategy

  • Snapshot both truncateAddress implementations against a table of inputs (empty string, very short string, exactly 2*chars long, normal 56-char address) and assert the consolidated version matches whichever behavior is chosen as correct for every case, especially the short-string overlap bug.
  • Property-test isValidStellarAddress against a corpus of known-valid and known-invalid Stellar addresses (bad checksum, wrong length, wrong version byte) to confirm the consolidated implementation doesn't regress either former implementation's coverage.

Related issues in this batch

The BatchForm duplicate/self-send issue and the EscrowForm self-escrow issue in this batch both depend on isValidStellarAddress/equality semantics from whichever implementation wins this consolidation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26bugSomething isn't workingdocumentationImprovements or additions to documentationvery hardVery difficult / senior-level bounty issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions