Skip to content
Open
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
48 changes: 48 additions & 0 deletions apps/src/components/AddressInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ClipboardPaste } from 'lucide-react';
import toast from 'react-hot-toast';

interface Props {
value: string;
onChange: (val: string) => void;
placeholder?: string;
error?: string;
}

export default function AddressInput({ value, onChange, placeholder = 'G...', error }: Props) {
async function handlePaste() {
try {
const text = await navigator.clipboard.readText();
const trimmed = text.trim();
if (trimmed.startsWith('G') && trimmed.length === 56) {
onChange(trimmed);
toast.success('Address pasted');
} else {
toast.error('Clipboard does not contain a valid Stellar address');
Comment on lines +16 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the declared Stellar SDK and existing validation helpers.
fd -H -t f '^package\.json$' . -x sh -c 'echo "--- $1"; jq -r ".dependencies, .devDependencies" "$1" | grep -i stellar || true' _ {}

fd -t f '^stellar\.(ts|tsx)$' apps/src/lib \
  -x ast-grep outline {} --items all

rg -n -C3 'StrKey|isValidEd25519PublicKey|decodeAddress|buildSendTx|startsWith\(.G.\)|length !== 56' apps/src

Repository: MJ-RWA/Stellarflow

Length of output: 5570


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- AddressInput.tsx"
cat -n apps/src/components/AddressInput.tsx

echo "--- SendPage.tsx relevant section"
sed -n '1,150p' apps/src/pages/SendPage.tsx | cat -n

echo "--- SchedulePage.tsx relevant section"
sed -n '1,150p' apps/src/pages/SchedulePage.tsx | cat -n

echo "--- scheduler/stellar relevant sections"
sed -n '1,80p' apps/src/hooks/useScheduler.ts | cat -n
sed -n '1,90p' apps/src/lib/stellar.ts | cat -n

echo "--- read-only behavioral probe: clipboard structural validator accepts invalid base32 StrKeys"
python3 - <<'PY'
import string
prefix = "G"
choices = [c.upper() for c in string.ascii_uppercase[:4]] + list(string.digits)
bad_prefix = "A" + "".join(choices[:40]) + ("G" * (55 - len(choices[:40])))
bad_len = prefix + "".join(choices[:30]) * 2
print("prefix+length-only matches 'G'+ 55 base32 chars:", prefix + "".join(choices[:40]) + ("G" * (55 - len(choices[:40]))) in [prefix + c for c in choices] * 55)
print("any G-prefix 56-character string made only of base32 chars is accepted:", True)
print("bad_prefix", bad_prefix, "starts G, len", len(bad_prefix), "matches false positives")
print("bad_len", bad_len, "len", len(bad_len), "matches false positives")
PY

Repository: MJ-RWA/Stellarflow

Length of output: 22419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- AddressInput.tsx"
cat -n apps/src/components/AddressInput.tsx

echo "--- SendPage.tsx relevant section"
sed -n '1,150p' apps/src/pages/SendPage.tsx | cat -n

echo "--- SchedulePage.tsx relevant section"
sed -n '1,150p' apps/src/pages/SchedulePage.tsx | cat -n

echo "--- scheduler/stellar relevant sections"
sed -n '1,80p' apps/src/hooks/useScheduler.ts | cat -n
sed -n '1,90p' apps/src/lib/stellar.ts | cat -n

echo "--- read-only behavioral probe: clipboard structural validator accepts invalid base32 StrKeys"
python3 - <<'PY'
import string
prefix = "G"
choices = [c.upper() for c in string.ascii_uppercase[:4]] + list(string.digits)
bad_prefix = "A" + "".join(choice for choice in choices[:40]) + ("G" * (55 - len(choices[:40])))
bad_len = prefix + "".join(choice for choice in choices[:30]) * 2
print("prefix+length-only matches 'G'+ 55 base32 chars:", prefix + "".join(choice for choice in choices[:40]) + ("G" * (55 - len(choices[:40]))) in [prefix + c for c in choices] * 55)
print("any G-prefix 56-character string made only of base32 chars is accepted:", True)
print("bad_prefix", bad_prefix, "len", len(bad_prefix), "starts G false positive")
print("bad_len", bad_len, "len", len(bad_len), "false positive")
PY

Repository: MJ-RWA/Stellarflow

Length of output: 22400


Use one protocol-level Stellar address validator.

AddressInput only checks G... and length before calling onChange, so malformed base32 StrKeys can be accepted. SendPage uses the same structural check and can reach buildSendTx with an invalid destination. SchedulePage stores any nonempty destination before execution. Use a shared Stellar SDK validator for clipboard input, manual input, scheduled saves, and pre-send validation.

📍 Affects 3 files
  • apps/src/components/AddressInput.tsx#L16-L20 (this comment)
  • apps/src/pages/SchedulePage.tsx#L108-L108
  • apps/src/pages/SendPage.tsx#L121-L123
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/src/components/AddressInput.tsx` around lines 16 - 20, Replace the
structural G-prefix and length checks with one shared Stellar SDK address
validator across AddressInput.tsx:16-20, SchedulePage.tsx:108, and
SendPage.tsx:121-123. Apply it to clipboard input, manual input, scheduled
destination saves, and pre-send validation so malformed addresses are rejected
before onChange, storage, or buildSendTx; preserve the existing success and
error behaviors.

}
} catch {
toast.error('Clipboard access denied — paste manually');
}
}

return (
<div>
<div className="relative">
<input
className={`input-field font-mono text-sm pr-12 ${error ? 'border-red-500' : ''}`}
placeholder={placeholder}
value={value}
onChange={e => onChange(e.target.value)}
/>
<button
type="button"
onClick={handlePaste}
className="absolute right-3 top-1/2 -translate-y-1/2 text-stellar-600 hover:text-stellar-400 transition-colors"
title="Paste from clipboard"
>
<ClipboardPaste size={16} />
</button>
</div>
{error && <p className="text-red-400 text-xs mt-1 font-body">{error}</p>}
</div>
);
}
3 changes: 2 additions & 1 deletion apps/src/pages/SchedulePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
XCircle, ChevronDown, ChevronUp, Loader2,
} from 'lucide-react';
import type { ScheduledPayment } from '../types';
import AddressInput from '../components/AddressInput';

const FREQUENCIES: ScheduledPayment['frequency'][] = ['daily', 'weekly', 'monthly'];
const ASSETS = ['XLM', 'USDC'];
Expand Down Expand Up @@ -104,7 +105,7 @@ export default function SchedulePage() {

<div>
<label className="text-stellar-600 text-xs font-display uppercase tracking-widest block mb-1">To Address</label>
<input className="input-field font-mono text-sm" placeholder="G..." value={destination} onChange={e => setDestination(e.target.value)} />
<AddressInput value={destination} onChange={setDestination} />
</div>

<div className="flex gap-2">
Expand Down
36 changes: 15 additions & 21 deletions apps/src/pages/SendPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { buildSendTx, submitTx, USDC_ASSET } from '../lib/stellar';
import AssetBadge from '../components/AssetBadge';
import toast from 'react-hot-toast';
import { CheckCircle, ArrowRight } from 'lucide-react';
import AddressInput from '../components/AddressInput';

const ASSETS = [
{ code: 'XLM', label: 'Stellar Lumens' },
Expand Down Expand Up @@ -109,28 +110,21 @@ export default function SendPage() {
</div>

{/* Destination */}
<div className="mb-4">
<label className="text-stellar-600 text-xs font-display font-600 uppercase tracking-widest block mb-2">
Destination Address
<div className="mb-4">
<label className="text-stellar-600 text-xs font-display font-600 uppercase tracking-widest block mb-2">
Destination Address
</label>
<input
className="input-field font-mono text-sm"
placeholder="G..."
value={destination}
onChange={e => {
const val = e.target.value;
setDestination(val);
if (val.length === 0) {
setDestError('');
} else if (!val.startsWith('G') || val.length !== 56) {
setDestError('Invalid Stellar address — must start with G and be 56 characters');
} else {
setDestError('');
}
}}
/>
{destError && <p className="text-red-500 text-xs mt-1">{destError}</p>}
</div>
<AddressInput
value={destination}
onChange={val => {
setDestination(val);
if (val.length === 0) setDestError('');
else if (!val.startsWith('G') || val.length !== 56) setDestError('Invalid Stellar address');
else setDestError('');
}}
error={destError}
/>
</div>

{/* Amount */}
<div className="mb-4">
Expand Down