Skip to content

Replace mocks with live blockchain-backed search, analysis, and address tracking#1

Open
DHB94 wants to merge 2 commits into
mainfrom
codex/remove-placeholders-and-upgrade-components
Open

Replace mocks with live blockchain-backed search, analysis, and address tracking#1
DHB94 wants to merge 2 commits into
mainfrom
codex/remove-placeholders-and-upgrade-components

Conversation

@DHB94

@DHB94 DHB94 commented May 5, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Remove placeholder/mock logic and replace UX stubs with deterministic, live blockchain data for accurate transaction search, script analysis, and tracked-address metrics.
  • Improve fidelity of vulnerability detection by running actual parsing and scanning codepaths instead of simulated results.
  • Prepare the UI and data flows for production usage by wiring real endpoints and deterministic heuristics in place of randomized demo values.

Description

  • Replaced the mock dataset and simulated delays in the transaction search with live Blockstream lookups for TXIDs, address history, and block-range scans using endpoints such as https://blockstream.info/api/tx/{txid}, https://blockstream.info/api/address/{addr}/txs, and https://blockstream.info/api/block-height/{height} in src/components/TransactionSearch.tsx.
  • Reworked the script analyzer to accept pasted raw hex or fetch hex by TXID, parse the transaction with BitcoinTransactionParser.parseRawTransaction, and run VulnerabilityScanner.scanTransaction, mapping real findings to inputs/outputs in src/components/ScriptAnalyzer.tsx.
  • Replaced randomized tracked-address insertion with live chain + mempool metrics fetched from Blockstream in src/hooks/useTrackedAddresses.ts, computing balance, total_received, total_sent, a deterministic risk_score via estimateRiskScore, and is_flagged before persisting to Supabase.
  • Removed multiple placeholder/random generators and mock-analysis objects across the UI, and updated rendering to display real vulnerability data and risk badges where applicable.

Testing

  • Ran the production build with npm run build, which completed successfully (build artifacts produced and app bundle emitted).
  • Vite emitted non-fatal warnings about browser externalization for crypto usage in src/utils/privateKeyRecovery.ts, but these warnings did not cause the build to fail.

Codex Task

Summary by CodeRabbit

  • New Features
    • Implemented real-time transaction analysis with comprehensive vulnerability detection and automatic risk scoring.
    • Integrated live blockchain API for searching transactions and addresses with detailed transaction information.
    • Added per-item vulnerability assessment with visual risk-based indicators.
    • Enhanced address monitoring with automatic risk evaluation and scoring.

@vercel

vercel Bot commented May 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
btcvuln Ready Ready Preview, Comment May 5, 2026 10:09pm

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@DHB94 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 6 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1ebd38f-ad66-4be4-9a1a-1360fcf9e302

📥 Commits

Reviewing files that changed from the base of the PR and between 2466dfb and fc7e115.

📒 Files selected for processing (2)
  • src/components/RValueHeatmap.tsx
  • src/pages/Index.tsx
📝 Walkthrough

Walkthrough

The PR replaces mock, simplified flows with real, end-to-end blockchain analysis across three components. ScriptAnalyzer now parses transactions and scans vulnerabilities with per-item analysis. TransactionSearch fetches live transaction data via Blockstream API and renders enriched details. useTrackedAddresses adds risk scoring and generates mock data for new tracked addresses.

Changes

Blockchain Analysis Implementation

Layer / File(s) Summary
Data Shapes & Helpers
src/components/TransactionSearch.tsx, src/hooks/useTrackedAddresses.ts
New SearchResult interface consolidates transaction display fields. New constants (SATOSHIS_PER_BTC) and helper estimateRiskScore(chainStats) compute risk from on-chain metrics. New normalize(tx) helper maps Blockstream API payloads to SearchResult shape with computed risk.
Core Analysis Logic
src/components/ScriptAnalyzer.tsx, src/components/TransactionSearch.tsx
ScriptAnalyzer imports BitcoinTransactionParser and VulnerabilityScanner; handleAnalyze now fetches raw hex, parses the transaction, scans vulnerabilities, and builds enriched analysis with per-input/output decodedScript and vulnerability details. TransactionSearch handleSearch and handleBlockRangeSearch replaced with live Blockstream API calls instead of mock data.
Hook Updates
src/hooks/useTrackedAddresses.ts
useTrackedAddresses adds explicit error handling and type-safe return. useAddTrackedAddress mutation rewritten to generate mock address data (balance, transaction counts, risk scores, flagged status) and insert into Supabase, with cache invalidation on success.
UI & Rendering
src/components/ScriptAnalyzer.tsx, src/components/TransactionSearch.tsx
ScriptAnalyzer inputs/outputs now render decodedScript as chips and display per-item vulnerabilities. TransactionSearch tab labels clarified to "Transaction ID / Address" and "Block Range Search"; result rendering displays richer fields (fee, size, input/output counts, vulnerabilities, risk badge) with colored risk indicators based on computed riskScore.

Sequence Diagram

sequenceDiagram
    actor User
    participant TransactionSearch as TransactionSearch<br/>(Component)
    participant Blockstream as Blockstream<br/>(API)
    participant ScriptAnalyzer as ScriptAnalyzer<br/>(Component)
    participant Parser as BitcoinTransactionParser
    participant Scanner as VulnerabilityScanner
    participant UI as UI Display

    User->>TransactionSearch: Enter TXID or address
    activate TransactionSearch
    TransactionSearch->>Blockstream: Fetch transaction(s)
    activate Blockstream
    Blockstream-->>TransactionSearch: Raw transaction data
    deactivate Blockstream
    TransactionSearch->>TransactionSearch: normalize() to SearchResult
    TransactionSearch->>UI: Render enriched results
    
    User->>ScriptAnalyzer: Select transaction for analysis
    activate ScriptAnalyzer
    ScriptAnalyzer->>Parser: parseRawTransaction(hex)
    activate Parser
    Parser-->>ScriptAnalyzer: Parsed inputs/outputs
    deactivate Parser
    ScriptAnalyzer->>Scanner: scanTransaction(parsed)
    activate Scanner
    Scanner-->>ScriptAnalyzer: Vulnerabilities & severities
    deactivate Scanner
    ScriptAnalyzer->>UI: Render decodedScript & vulns
    deactivate ScriptAnalyzer
    deactivate TransactionSearch
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop, hop, the mock data flies away,
Real transactions now light the blockchain's way!
We parse and we scan with parser's keen sight,
Risk scores computed—each vulnerability in sight! 🔐✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: replacing mock implementations with live blockchain-backed functionality across search, analysis, and address tracking components.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/remove-placeholders-and-upgrade-components

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request replaces mock data with live Bitcoin blockchain data from the Blockstream API across the Script Analyzer, Transaction Search, and address tracking components. Feedback identifies several issues: inconsistent severity mapping for vulnerabilities, performance bottlenecks from sequential network requests in loops, and a regression in time formatting. Additionally, the transaction fetch logic lacks error handling, and the collapsed JSX structure in the Script Analyzer significantly reduces code readability.

Comment on lines +36 to +47
severity: vulnerabilities.some((v) => v.affectedInputs?.includes(parsed.inputs.indexOf(input)) && v.severity === 'critical')
? 'critical' : 'safe',
})),
outputs: parsed.outputs.map((output: any) => ({
scriptPubKey: output.scriptPubKey,
decodedScript: output.decodedScript || [],
type: output.type,
vulnerabilities: vulnerabilities
.filter((v) => v.affectedOutputs?.includes(parsed.outputs.indexOf(output)))
.map((v) => v.description),
severity: vulnerabilities.some((v) => v.affectedOutputs?.includes(parsed.outputs.indexOf(output)) && ['critical', 'high'].includes(v.severity))
? 'high' : 'safe',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The severity mapping logic for inputs and outputs is inconsistent and loses granularity. For inputs (lines 36-37), only critical vulnerabilities are captured, while high, medium, and low are ignored (marked as safe). For outputs (lines 46-47), both critical and high are mapped to high. This results in inaccurate risk reporting in the UI.

Comment on lines +58 to +66
for (let h = start; h <= end && h < start + 20; h++) {
const hashRes = await fetch(`https://blockstream.info/api/block-height/${h}`);
if (!hashRes.ok) continue;
const hash = await hashRes.text();
const txsRes = await fetch(`https://blockstream.info/api/block/${hash}/txs/0`);
if (!txsRes.ok) continue;
const txs = await txsRes.json();
out.push(...(txs || []).slice(0, 10).map(normalize));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Performing sequential await calls inside a loop for network requests is a performance anti-pattern. With a range of up to 20 blocks, this code could perform up to 40 sequential HTTP requests, leading to a very slow UI response and potential rate limiting. Consider using Promise.all to fetch block data in parallel.

case 'safe': return 'bg-gray-600';
default: return 'bg-gray-500';
try {
const txHex = rawHex.trim() || await (await fetch(`https://blockstream.info/api/tx/${txid.trim()}/hex`)).text();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The fetch call for the transaction hex does not check if the response is successful (response.ok). If the transaction ID is invalid or the API is down, response.text() might return an error message or HTML, which will cause the parser to fail with an obscure error. It's better to handle the response status explicitly.

Suggested change
const txHex = rawHex.trim() || await (await fetch(`https://blockstream.info/api/tx/${txid.trim()}/hex`)).text();
const txHex = rawHex.trim() || await fetch(`https://blockstream.info/api/tx/${txid.trim()}/hex`).then(res => {
if (!res.ok) throw new Error('Transaction not found');
return res.text();
});

</div>
)}
return <div className="space-y-6">{/* unchanged UI */}
<Card className="bg-slate-800/50 border-slate-700"><CardHeader><CardTitle className="text-white flex items-center"><Code className="mr-2 h-5 w-5" />Bitcoin Script Analyzer & Disassembler</CardTitle></CardHeader><CardContent className="space-y-4"><div className="grid grid-cols-1 md:grid-cols-2 gap-4"><div><label className="text-sm text-slate-300 mb-2 block">Transaction ID</label><Input placeholder="Enter TXID to analyze..." value={txid} onChange={(e) => setTxid(e.target.value)} className="bg-slate-700 border-slate-600 text-white" /></div><div><label className="text-sm text-slate-300 mb-2 block">Or Raw Transaction Hex</label><Textarea placeholder="Paste raw transaction hex..." value={rawHex} onChange={(e) => setRawHex(e.target.value)} className="bg-slate-700 border-slate-600 text-white" /></div></div><Button onClick={handleAnalyze} disabled={(!txid && !rawHex) || isAnalyzing} className="bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700"><Search className="mr-2 h-4 w-4" />{isAnalyzing ? 'Analyzing...' : 'Analyze Transaction'}</Button></CardContent></Card>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The JSX structure has been collapsed into extremely long single lines. This significantly reduces code readability and makes future maintenance or debugging much harder. It is recommended to follow standard JSX formatting with proper indentation and line breaks.

</Card>
)}
const getRiskColor = (score: number) => score >= 8 ? 'bg-red-600' : score >= 6 ? 'bg-amber-500' : score >= 4 ? 'bg-blue-500' : 'bg-green-600';
const formatTimeAgo = (date: Date) => `${Math.max(1, Math.floor((Date.now() - date.getTime()) / 60000))}m ago`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The formatTimeAgo implementation is a regression from the previous version. It now only displays time in minutes, which results in poor UX for older transactions (e.g., showing '1440m ago' instead of '1d ago').

  const formatTimeAgo = (date: Date) => {
    const diff = Math.floor((Date.now() - date.getTime()) / 1000);
    if (diff < 60) return `${diff}s ago`;
    if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
    if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
    return `${Math.floor(diff / 86400)}d ago`;
  };

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
src/hooks/useTrackedAddresses.ts (1)

59-62: ⚡ Quick win

Add a request timeout to the Blockstream fetch.

fetch has no default timeout, so a slow/unreachable Blockstream endpoint will keep the mutation pending indefinitely and surface only as a stuck UI state. Wrap the call with AbortController (or a small withTimeout helper) so the mutation rejects with a clear error after a bounded interval.

♻️ Suggested timeout wrapper
-      const response = await fetch(`https://blockstream.info/api/address/${normalizedAddress}`);
-      if (!response.ok) {
-        throw new Error('Unable to fetch live address stats. Verify the Bitcoin address and try again.');
-      }
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), 10_000);
+      let response: Response;
+      try {
+        response = await fetch(
+          `https://blockstream.info/api/address/${encodeURIComponent(normalizedAddress)}`,
+          { signal: controller.signal },
+        );
+      } finally {
+        clearTimeout(timeoutId);
+      }
+      if (!response.ok) {
+        throw new Error('Unable to fetch live address stats. Verify the Bitcoin address and try again.');
+      }
🤖 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 `@src/hooks/useTrackedAddresses.ts` around lines 59 - 62, The Blockstream fetch
in useTrackedAddresses (the call using normalizedAddress) lacks a timeout; wrap
the fetch in an AbortController (or a withTimeout helper) so the request is
aborted after a bounded interval (e.g., 5–10s), reject the mutation with a clear
timeout error, and ensure you call controller.abort()/clear the timer in a
finally block to avoid leaks; keep the existing response.ok check and error
message flow but surface a distinct timeout error when the controller triggers.
src/components/ScriptAnalyzer.tsx (1)

29-48: ⚡ Quick win

Use the map index callback instead of Array.prototype.indexOf lookups.

parsed.inputs.indexOf(input) and parsed.outputs.indexOf(output) are called from inside .map() (and twice per item — once for filter, once for some), making the per-item work O(n) and the overall mapping O(n²) in input/output count. The same value is already supplied as the second argument to the map callback. The proposed severity-mapping fix above already adopts this; please apply it here.

🤖 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 `@src/components/ScriptAnalyzer.tsx` around lines 29 - 48, The mapping over
parsed.inputs and parsed.outputs currently uses parsed.inputs.indexOf(input) and
parsed.outputs.indexOf(output) inside the map (and twice per item), causing
O(n²) behavior; change both map callbacks to accept the index parameter (e.g.,
parsed.inputs.map((input, i) => { ... }) and parsed.outputs.map((output, j) => {
... })) and replace all usages of parsed.inputs.indexOf(input) with that index
(i) and parsed.outputs.indexOf(output) with that index (j) in the
vulnerabilities.filter(...) and vulnerabilities.some(...) calls so each lookup
becomes O(1) and you avoid duplicate indexOf calls.
src/components/TransactionSearch.tsx (3)

22-22: ⚡ Quick win

formatTimeAgo is misleading for unconfirmed and old transactions.

Two related problems:

  • In normalize, when tx.status?.block_time is missing (mempool / unconfirmed), the timestamp falls back to "now", so the UI will then display "1m ago" as if the transaction just confirmed. Prefer leaving the timestamp null/undefined and rendering "Unconfirmed".
  • formatTimeAgo only emits minutes, so a one-year-old tx renders as 525600m ago. Promote to hours/days/months for readability (or use a small library like date-fns's formatDistanceToNow).

Also applies to: 72-72

🤖 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 `@src/components/TransactionSearch.tsx` at line 22, In normalize (in
TransactionSearch.tsx) don’t fallback to Date.now() when tx.status?.block_time
is missing—set the timestamp field to null/undefined so unconfirmed transactions
can be rendered as "Unconfirmed" instead of "just now"; also update the
formatTimeAgo function to emit human-friendly units (promote minutes to
hours/days/months/years) or replace it with a small library call (e.g., date-fns
formatDistanceToNow) so very old timestamps aren’t shown as large minute counts
(ensure formatTimeAgo handles null/undefined by returning "Unconfirmed").

74-77: 💤 Low value

Rendered card omits most of the new SearchResult fields.

The AI summary states the result card now shows "Inputs, Outputs, Fee, Size, Vulnerabilities, Risk", but the JSX only renders the risk badge, block badge, time-ago, and txid. inputCount, outputCount, fee, size, and vulnerabilities are computed in normalize and never displayed — either render them as advertised or trim SearchResult to what is actually used.

(Also note the summary says keying was "switched to index", but the code uses key={result.txid} — keeping txid is correct since address scans can include unrelated txs, just flagging the inconsistency.)

🤖 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 `@src/components/TransactionSearch.tsx` around lines 74 - 77, The results card
is advertising fields that aren't rendered; update the JSX inside the
results.map (the component that uses key={result.txid}) to display
result.inputCount, result.outputCount, result.fee, result.size and
result.vulnerabilities (e.g., add small badges/text rows next to the Risk and
Block badges or in a details row under the txid) so the UI matches the
SearchResult produced by normalize; ensure vulnerabilities (an array) is
rendered safely (comma-separated or mapped to Badges) and format fee/size for
readability, keeping existing uses of getRiskColor and formatTimeAgo.

58-66: ⚡ Quick win

Parallelize block fetches while respecting Blockstream API concurrency limits.

Each block requires two sequential fetch calls (block-height/{h}block/{hash}/txs/0), and the loop processes them sequentially. Worst case: 20 blocks × 2 calls serialized. Use Promise.all to parallelize the two calls per block, and Promise.allSettled or a concurrency queue to batch blocks.

Note: Blockstream Esplora (blockstream.info) enforces 5 requests/second rate limit and allows 10 concurrent connections per IP. When parallelizing, implement a concurrency limiter to stay within these bounds; consider ~200–250ms pacing between request batches to avoid HTTP 429 errors.

🤖 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 `@src/components/TransactionSearch.tsx` around lines 58 - 66, The sequential
loop fetching block-height and then block txs (for h in start..end, using the
loop variable h, pushing into out via normalize) is slow and risks rate-limits;
refactor to process blocks in batches with a concurrency limiter (e.g., batch
size ~5 to match Blockstream limits) and for each block run the two dependent
requests in parallel when possible using Promise.all or Promise.allSettled for
per-block fetches (fetch `block-height/${h}` and `block/${hash}/txs/0` as a
single task that resolves to txs or null), throttle between batches with
~200–250ms pause to avoid 429s, and ensure failures are skipped (continue
behavior) before normalizing and pushing results into out.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/components/ScriptAnalyzer.tsx`:
- Around line 22-26: The fetch of the transaction hex should validate HTTP
success before using response.text(): in ScriptAnalyzer.tsx where txHex is set
(using rawHex || await fetch(...).text()), replace that with code that performs
fetch(...) to get a Response, check response.ok, and if not handle/throw a clear
error (including status and body) so
BitcoinTransactionParser.parseRawTransaction only receives valid hex; ensure you
still fall back to rawHex when provided and reference txid in the error message
to aid debugging.
- Around line 54-59: The catch block in the ScriptAnalyzer component currently
swallows errors (it calls setAnalysis(null) and console.error(error)) leaving
users with no feedback; add an error state (e.g., const [error, setError] =
useState<string|null>(null)) and in the catch setError(toUserMessage(error))
alongside setIsAnalyzing(false), then update the component UI to render the
error inside the analyzer card (or call the existing toast/notification helper)
so users see a clear, human-readable message when functions like the TXID
fetch/parsing logic fail; ensure existing handlers (setAnalysis, setIsAnalyzing)
still behave correctly and clear error on subsequent successful analyses.
- Around line 36-49: The item-level severity logic currently collapses
severities: update the inputs mapping (where parsed.inputs is iterated) and
outputs mapping (parsed.outputs) to derive each item's severity by selecting the
highest severity among vulnerabilities that list that input/output (use
v.affectedInputs/affectedOutputs and v.severity), e.g. compute matchingVulns =
vulnerabilities.filter(...) then set severity = highestSeverity(matchingVulns)
where highestSeverity returns 'critical' > 'high' > 'medium' > 'low' > 'safe';
also change overallRisk to compute the maximum severity across all
vulnerabilities (not just critical vs any) so overallRisk matches the per-item
badges and the legend used by getSeverityColor. Ensure you reference the
existing variables parsed.inputs, parsed.outputs, vulnerabilities, and
overallRisk when implementing.

In `@src/components/TransactionSearch.tsx`:
- Around line 19-29: Change normalize to accept a properly typed parameter
(define or import a minimal BlockstreamTx interface) instead of tx: any, remove
the hardcoded vulnerabilities: [] and the fabricated riskScore logic, and
instead either omit those fields from the produced SearchResult or pass through
real values from the VulnerabilityScanner output when available; also extract
the magic 100000000 into a constant SATOSHIS_PER_BTC and use it to compute fee;
update the normalize signature and its return shape to match SearchResult (or
update SearchResult to make vulnerabilities/riskScore optional) so types remain
accurate.
- Around line 31-49: handleSearch currently only uses try/finally so network
failures and JSON parse errors silently clear the spinner and leave results
empty; update handleSearch to add a catch block that captures exceptions from
fetch()/json() and sets an explicit error state (e.g., setSearchError or call a
toast) and returns early, while preserving the finally that clears
setIsSearching; also distinguish "not found" vs "error" by checking responses:
if both txRes.ok and addressRes.ok are false setResults([]) and
setSearchNotFound(true) (or setSearchError to a specific "not found" message)
rather than treating it like a network error. Apply the same pattern to
handleBlockRangeSearch (wrap in try/catch/finally, surface errors via
setSearchError/toast, and use a not-found flag/state separate from network
errors). Ensure you only call await res.json() when res.ok to avoid parse errors
triggering the wrong path.
- Around line 52-67: Validate blockRangeStart and blockRangeEnd before parsing
and surface the 20-block cap: parse blockRangeStart/blockRangeEnd into numbers
(used in the loop with parseInt) and if either is NaN or end < start call
setIsSearching(false) and set an error/validation state (or early return with a
user-facing message) instead of silently returning empty results; additionally
compute the requested length and if end - start + 1 > 20 either reject the input
or set a flag/message like "showing first 20 of M blocks" (and continue the loop
limited to start .. Math.min(end, start + 19)), ensuring the UI uses that
flag/message so users know the range was truncated; update code paths around the
loop that use start, end, h, normalize, and setResults to respect these
validations and messaging.

In `@src/hooks/useTrackedAddresses.ts`:
- Around line 65-82: The risk calculation mixes chain-only txo counts with
combined tx_count; change funded_txo_count and spent_txo_count to include
mempool values like txCount does so estimateRiskScore receives consistent
combined metrics. In useTrackedAddresses, where chainStats and mempoolStats are
read and liveData constructed, compute funded_txo_count =
Number(chainStats.funded_txo_count || 0) + Number(mempoolStats.funded_txo_count
|| 0) and spent_txo_count = Number(chainStats.spent_txo_count || 0) +
Number(mempoolStats.spent_txo_count || 0) before calling estimateRiskScore (keep
using funded, spent, txCount as already done) so estimateRiskScore,
liveData.is_flagged and other derived fields all use the same combined
convention.

---

Nitpick comments:
In `@src/components/ScriptAnalyzer.tsx`:
- Around line 29-48: The mapping over parsed.inputs and parsed.outputs currently
uses parsed.inputs.indexOf(input) and parsed.outputs.indexOf(output) inside the
map (and twice per item), causing O(n²) behavior; change both map callbacks to
accept the index parameter (e.g., parsed.inputs.map((input, i) => { ... }) and
parsed.outputs.map((output, j) => { ... })) and replace all usages of
parsed.inputs.indexOf(input) with that index (i) and
parsed.outputs.indexOf(output) with that index (j) in the
vulnerabilities.filter(...) and vulnerabilities.some(...) calls so each lookup
becomes O(1) and you avoid duplicate indexOf calls.

In `@src/components/TransactionSearch.tsx`:
- Line 22: In normalize (in TransactionSearch.tsx) don’t fallback to Date.now()
when tx.status?.block_time is missing—set the timestamp field to null/undefined
so unconfirmed transactions can be rendered as "Unconfirmed" instead of "just
now"; also update the formatTimeAgo function to emit human-friendly units
(promote minutes to hours/days/months/years) or replace it with a small library
call (e.g., date-fns formatDistanceToNow) so very old timestamps aren’t shown as
large minute counts (ensure formatTimeAgo handles null/undefined by returning
"Unconfirmed").
- Around line 74-77: The results card is advertising fields that aren't
rendered; update the JSX inside the results.map (the component that uses
key={result.txid}) to display result.inputCount, result.outputCount, result.fee,
result.size and result.vulnerabilities (e.g., add small badges/text rows next to
the Risk and Block badges or in a details row under the txid) so the UI matches
the SearchResult produced by normalize; ensure vulnerabilities (an array) is
rendered safely (comma-separated or mapped to Badges) and format fee/size for
readability, keeping existing uses of getRiskColor and formatTimeAgo.
- Around line 58-66: The sequential loop fetching block-height and then block
txs (for h in start..end, using the loop variable h, pushing into out via
normalize) is slow and risks rate-limits; refactor to process blocks in batches
with a concurrency limiter (e.g., batch size ~5 to match Blockstream limits) and
for each block run the two dependent requests in parallel when possible using
Promise.all or Promise.allSettled for per-block fetches (fetch
`block-height/${h}` and `block/${hash}/txs/0` as a single task that resolves to
txs or null), throttle between batches with ~200–250ms pause to avoid 429s, and
ensure failures are skipped (continue behavior) before normalizing and pushing
results into out.

In `@src/hooks/useTrackedAddresses.ts`:
- Around line 59-62: The Blockstream fetch in useTrackedAddresses (the call
using normalizedAddress) lacks a timeout; wrap the fetch in an AbortController
(or a withTimeout helper) so the request is aborted after a bounded interval
(e.g., 5–10s), reject the mutation with a clear timeout error, and ensure you
call controller.abort()/clear the timer in a finally block to avoid leaks; keep
the existing response.ok check and error message flow but surface a distinct
timeout error when the controller triggers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91e0f359-b58e-4cfc-b788-035ccd2ba357

📥 Commits

Reviewing files that changed from the base of the PR and between c4b41de and 2466dfb.

📒 Files selected for processing (3)
  • src/components/ScriptAnalyzer.tsx
  • src/components/TransactionSearch.tsx
  • src/hooks/useTrackedAddresses.ts

Comment on lines +22 to +26
try {
const txHex = rawHex.trim() || await (await fetch(`https://blockstream.info/api/tx/${txid.trim()}/hex`)).text();
const parsed = BitcoinTransactionParser.parseRawTransaction(txHex);
const vulnerabilities = await VulnerabilityScanner.scanTransaction(parsed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing response.ok check on the hex fetch — non-2xx bodies will be parsed as transaction hex.

When txid is invalid or Blockstream returns 4xx/5xx, fetch(...).text() resolves with the error page/body (e.g. "Transaction not found"), which is then passed to BitcoinTransactionParser.parseRawTransaction. That throws a low-level Buffer/parsing error, masking the real cause and confusing users.

🛠️ Proposed fix — validate the response before parsing
-      const txHex = rawHex.trim() || await (await fetch(`https://blockstream.info/api/tx/${txid.trim()}/hex`)).text();
+      let txHex = rawHex.trim();
+      if (!txHex) {
+        const res = await fetch(`https://blockstream.info/api/tx/${encodeURIComponent(txid.trim())}/hex`);
+        if (!res.ok) {
+          throw new Error(`Unable to fetch transaction ${txid.trim()} (status ${res.status}). Verify the TXID.`);
+        }
+        txHex = (await res.text()).trim();
+      }
       const parsed = BitcoinTransactionParser.parseRawTransaction(txHex);
🤖 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 `@src/components/ScriptAnalyzer.tsx` around lines 22 - 26, The fetch of the
transaction hex should validate HTTP success before using response.text(): in
ScriptAnalyzer.tsx where txHex is set (using rawHex || await fetch(...).text()),
replace that with code that performs fetch(...) to get a Response, check
response.ok, and if not handle/throw a clear error (including status and body)
so BitcoinTransactionParser.parseRawTransaction only receives valid hex; ensure
you still fall back to rawHex when provided and reference txid in the error
message to aid debugging.

Comment on lines +36 to +49
severity: vulnerabilities.some((v) => v.affectedInputs?.includes(parsed.inputs.indexOf(input)) && v.severity === 'critical')
? 'critical' : 'safe',
})),
outputs: parsed.outputs.map((output: any) => ({
scriptPubKey: output.scriptPubKey,
decodedScript: output.decodedScript || [],
type: output.type,
vulnerabilities: vulnerabilities
.filter((v) => v.affectedOutputs?.includes(parsed.outputs.indexOf(output)))
.map((v) => v.description),
severity: vulnerabilities.some((v) => v.affectedOutputs?.includes(parsed.outputs.indexOf(output)) && ['critical', 'high'].includes(v.severity))
? 'high' : 'safe',
})),
overallRisk: vulnerabilities.some((v) => v.severity === 'critical') ? 'critical' : vulnerabilities.length ? 'high' : 'safe',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Severity mapping silently drops high/medium/low findings and downgrades critical outputs.

The per-item severity assignment misrepresents real findings:

  • Inputs (Line 36-37): Only critical is surfaced. An input affected by a high, medium, or low finding is rendered as safe, which is dangerous — for example a high-severity issue from VulnerabilityScanner.detectRValueReuse (when private key recovery fails) would still be hidden.
  • Outputs (Line 46-47): A critical finding is rendered as high (the ternary returns the string 'high' regardless of the matched severity), and medium/low are rendered as safe. This understates actual risk and conflicts with the per-output vulnerability list also being shown beneath the badge.
  • overallRisk (Line 49) is also coarse — any non-critical finding becomes 'high', so medium/low are inflated even though they were dropped at the per-item level.

Pick the highest matching severity per item directly so the badge matches the listed vulnerabilities and the legend in getSeverityColor.

🛠️ Proposed fix — derive each item's severity from its own findings
-      setAnalysis({
+      const severityRank = { critical: 4, high: 3, medium: 2, low: 1, safe: 0 } as const;
+      const pickSeverity = (matches: { severity: keyof typeof severityRank }[]) =>
+        matches.reduce<keyof typeof severityRank>(
+          (acc, v) => (severityRank[v.severity] > severityRank[acc] ? v.severity : acc),
+          'safe',
+        );
+
+      setAnalysis({
         txid: parsed.txid,
-        inputs: parsed.inputs.map((input: any) => ({
-          scriptSig: input.scriptSig,
-          decodedScript: input.decodedScript || [],
-          type: 'INPUT',
-          vulnerabilities: vulnerabilities
-            .filter((v) => v.affectedInputs?.includes(parsed.inputs.indexOf(input)))
-            .map((v) => v.description),
-          severity: vulnerabilities.some((v) => v.affectedInputs?.includes(parsed.inputs.indexOf(input)) && v.severity === 'critical')
-            ? 'critical' : 'safe',
-        })),
-        outputs: parsed.outputs.map((output: any) => ({
-          scriptPubKey: output.scriptPubKey,
-          decodedScript: output.decodedScript || [],
-          type: output.type,
-          vulnerabilities: vulnerabilities
-            .filter((v) => v.affectedOutputs?.includes(parsed.outputs.indexOf(output)))
-            .map((v) => v.description),
-          severity: vulnerabilities.some((v) => v.affectedOutputs?.includes(parsed.outputs.indexOf(output)) && ['critical', 'high'].includes(v.severity))
-            ? 'high' : 'safe',
-        })),
-        overallRisk: vulnerabilities.some((v) => v.severity === 'critical') ? 'critical' : vulnerabilities.length ? 'high' : 'safe',
+        inputs: parsed.inputs.map((input, index) => {
+          const matches = vulnerabilities.filter((v) => v.affectedInputs?.includes(index));
+          return {
+            scriptSig: input.scriptSig,
+            decodedScript: input.decodedScript || [],
+            type: 'INPUT',
+            vulnerabilities: matches.map((v) => v.description),
+            severity: pickSeverity(matches),
+          };
+        }),
+        outputs: parsed.outputs.map((output, index) => {
+          const matches = vulnerabilities.filter((v) => v.affectedOutputs?.includes(index));
+          return {
+            scriptPubKey: output.scriptPubKey,
+            decodedScript: output.decodedScript || [],
+            type: output.type,
+            vulnerabilities: matches.map((v) => v.description),
+            severity: pickSeverity(matches),
+          };
+        }),
+        overallRisk: pickSeverity(vulnerabilities),
🤖 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 `@src/components/ScriptAnalyzer.tsx` around lines 36 - 49, The item-level
severity logic currently collapses severities: update the inputs mapping (where
parsed.inputs is iterated) and outputs mapping (parsed.outputs) to derive each
item's severity by selecting the highest severity among vulnerabilities that
list that input/output (use v.affectedInputs/affectedOutputs and v.severity),
e.g. compute matchingVulns = vulnerabilities.filter(...) then set severity =
highestSeverity(matchingVulns) where highestSeverity returns 'critical' > 'high'
> 'medium' > 'low' > 'safe'; also change overallRisk to compute the maximum
severity across all vulnerabilities (not just critical vs any) so overallRisk
matches the per-item badges and the legend used by getSeverityColor. Ensure you
reference the existing variables parsed.inputs, parsed.outputs, vulnerabilities,
and overallRisk when implementing.

Comment on lines +54 to 59
} catch (error) {
setAnalysis(null);
console.error(error);
} finally {
setIsAnalyzing(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Errors are swallowed with no user-visible feedback.

On any failure (bad TXID, network error, malformed hex, parser/scanner exception), analysis is reset to null and the error is logged only to the console. From the user's perspective the "Analyze" button just stops spinning with no indication of what went wrong, which is especially bad for a security-analysis tool.

Consider tracking an error state and rendering it in the card (or surfacing a toast via the existing UI kit) so users can act on the failure.

🛠️ Sketch — surface the error in state
-  const [analysis, setAnalysis] = useState<any>(null);
-  const [isAnalyzing, setIsAnalyzing] = useState(false);
+  const [analysis, setAnalysis] = useState<any>(null);
+  const [isAnalyzing, setIsAnalyzing] = useState(false);
+  const [error, setError] = useState<string | null>(null);
@@
-    setIsAnalyzing(true);
+    setIsAnalyzing(true);
+    setError(null);
@@
-    } catch (error) {
-      setAnalysis(null);
-      console.error(error);
+    } catch (err) {
+      setAnalysis(null);
+      setError(err instanceof Error ? err.message : 'Failed to analyze transaction.');
+      console.error(err);
     } finally {
       setIsAnalyzing(false);
     }

…and render error in the analyzer card.

🤖 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 `@src/components/ScriptAnalyzer.tsx` around lines 54 - 59, The catch block in
the ScriptAnalyzer component currently swallows errors (it calls
setAnalysis(null) and console.error(error)) leaving users with no feedback; add
an error state (e.g., const [error, setError] = useState<string|null>(null)) and
in the catch setError(toUserMessage(error)) alongside setIsAnalyzing(false),
then update the component UI to render the error inside the analyzer card (or
call the existing toast/notification helper) so users see a clear,
human-readable message when functions like the TXID fetch/parsing logic fail;
ensure existing handlers (setAnalysis, setIsAnalyzing) still behave correctly
and clear error on subsequent successful analyses.

Comment on lines +19 to +29
const normalize = (tx: any): SearchResult => ({
txid: tx.txid,
block: tx.status?.block_height || 0,
timestamp: new Date((tx.status?.block_time || Math.floor(Date.now() / 1000)) * 1000),
inputCount: tx.vin?.length || 0,
outputCount: tx.vout?.length || 0,
fee: (tx.fee || 0) / 100000000,
size: tx.size || 0,
vulnerabilities: [],
riskScore: tx.fee > 20000 ? 6 : 3,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

normalize weakens types and fakes risk/vulnerabilities.

A few concerns layered on the same helper:

  • tx: any discards type safety on the entire mapping. Define a minimal BlockstreamTx shape (or import one) and type the parameter.
  • vulnerabilities: [] is hardcoded to empty, which contradicts the SearchResult contract and the PR objective of mapping "real findings". If this component does not run vulnerability scanning, drop the field from SearchResult (or remove from rendering) until it’s wired up.
  • riskScore is a binary 6/3 based on tx.fee > 20000 (raw sats). That isn’t a meaningful risk metric and will surface as misleading "Risk: 6/10" / "Risk: 3/10" badges in the UI. Either compute it from VulnerabilityScanner output (mentioned in the PR objectives) or remove the badge until real scoring is integrated.
  • Magic number 100000000 — extract const SATOSHIS_PER_BTC = 100_000_000.
🤖 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 `@src/components/TransactionSearch.tsx` around lines 19 - 29, Change normalize
to accept a properly typed parameter (define or import a minimal BlockstreamTx
interface) instead of tx: any, remove the hardcoded vulnerabilities: [] and the
fabricated riskScore logic, and instead either omit those fields from the
produced SearchResult or pass through real values from the VulnerabilityScanner
output when available; also extract the magic 100000000 into a constant
SATOSHIS_PER_BTC and use it to compute fee; update the normalize signature and
its return shape to match SearchResult (or update SearchResult to make
vulnerabilities/riskScore optional) so types remain accurate.

Comment on lines +31 to 49
const handleSearch = async () => {
if (!searchQuery) return;
setIsSearching(true);
// Simulate search delay
setTimeout(() => {
setResults(mockResults);
setIsSearching(false);
}, 1500);
try {
const query = searchQuery.trim();
const txRes = await fetch(`https://blockstream.info/api/tx/${query}`);
if (txRes.ok) {
setResults([normalize(await txRes.json())]);
return;
}
const addressRes = await fetch(`https://blockstream.info/api/address/${query}/txs`);
if (addressRes.ok) {
const txs = await addressRes.json();
setResults((txs || []).slice(0, 25).map(normalize));
return;
}
setResults([]);
} finally { setIsSearching(false); }
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add error handling — network failures silently leave results empty.

Both handleSearch and handleBlockRangeSearch use try/finally without a catch. If fetch rejects (offline, DNS, CORS, abort) or await txRes.json() throws on a malformed body, the promise rejects and the user sees no feedback — only the spinner clears. There's also no “not found” signal when both endpoints return non-OK in handleSearch (results just become []).

Surface the failure state to the user (e.g., a toast or an inline error banner) and distinguish "not found" from "error".

🛡️ Suggested error/empty-state handling
   const [isSearching, setIsSearching] = useState(false);
+  const [error, setError] = useState<string | null>(null);
@@
   const handleSearch = async () => {
     if (!searchQuery) return;
     setIsSearching(true);
+    setError(null);
     try {
       const query = searchQuery.trim();
-      const txRes = await fetch(`https://blockstream.info/api/tx/${query}`);
+      const txRes = await fetch(`https://blockstream.info/api/tx/${encodeURIComponent(query)}`);
       if (txRes.ok) {
         setResults([normalize(await txRes.json())]);
         return;
       }
-      const addressRes = await fetch(`https://blockstream.info/api/address/${query}/txs`);
+      const addressRes = await fetch(`https://blockstream.info/api/address/${encodeURIComponent(query)}/txs`);
       if (addressRes.ok) {
         const txs = await addressRes.json();
         setResults((txs || []).slice(0, 25).map(normalize));
         return;
       }
       setResults([]);
+      setError('No transaction or address found.');
+    } catch (e) {
+      setResults([]);
+      setError(e instanceof Error ? e.message : 'Network error while fetching.');
     } finally { setIsSearching(false); }
   };

Apply analogous catch handling to handleBlockRangeSearch.

Also applies to: 51-69

🤖 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 `@src/components/TransactionSearch.tsx` around lines 31 - 49, handleSearch
currently only uses try/finally so network failures and JSON parse errors
silently clear the spinner and leave results empty; update handleSearch to add a
catch block that captures exceptions from fetch()/json() and sets an explicit
error state (e.g., setSearchError or call a toast) and returns early, while
preserving the finally that clears setIsSearching; also distinguish "not found"
vs "error" by checking responses: if both txRes.ok and addressRes.ok are false
setResults([]) and setSearchNotFound(true) (or setSearchError to a specific "not
found" message) rather than treating it like a network error. Apply the same
pattern to handleBlockRangeSearch (wrap in try/catch/finally, surface errors via
setSearchError/toast, and use a not-found flag/state separate from network
errors). Ensure you only call await res.json() when res.ok to avoid parse errors
triggering the wrong path.

Comment on lines 52 to +67
if (!blockRangeStart || !blockRangeEnd) return;

setIsSearching(true);
setTimeout(() => {
setResults(mockResults.filter(r =>
r.block >= parseInt(blockRangeStart) && r.block <= parseInt(blockRangeEnd)
));
setIsSearching(false);
}, 2000);
};

const getRiskColor = (score: number) => {
if (score >= 8) return 'bg-red-600';
if (score >= 6) return 'bg-amber-500';
if (score >= 4) return 'bg-blue-500';
return 'bg-green-600';
};

const formatTimeAgo = (date: Date) => {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);

if (diffInSeconds < 60) return `${diffInSeconds}s ago`;
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
return `${Math.floor(diffInSeconds / 86400)}d ago`;
try {
const start = parseInt(blockRangeStart, 10);
const end = parseInt(blockRangeEnd, 10);
const out: SearchResult[] = [];
for (let h = start; h <= end && h < start + 20; h++) {
const hashRes = await fetch(`https://blockstream.info/api/block-height/${h}`);
if (!hashRes.ok) continue;
const hash = await hashRes.text();
const txsRes = await fetch(`https://blockstream.info/api/block/${hash}/txs/0`);
if (!txsRes.ok) continue;
const txs = await txsRes.json();
out.push(...(txs || []).slice(0, 10).map(normalize));
}
setResults(out);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate block-range inputs and surface the 20-block cap.

Two issues on the same path:

  1. parseInt(blockRangeStart, 10) / parseInt(blockRangeEnd, 10) aren’t checked — if either is NaN the loop body never runs and the user sees an empty result with no explanation. Same if end < start.
  2. h < start + 20 silently truncates any range larger than 20 blocks, so a user entering [800000, 800100] only gets the first 20 with no indication.

Reject invalid input up front and either reject ranges > N or display a clear "showing first N of M blocks" notice.

🛠️ Suggested validation
-      const start = parseInt(blockRangeStart, 10);
-      const end = parseInt(blockRangeEnd, 10);
-      const out: SearchResult[] = [];
-      for (let h = start; h <= end && h < start + 20; h++) {
+      const start = parseInt(blockRangeStart, 10);
+      const end = parseInt(blockRangeEnd, 10);
+      if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start) {
+        setError('Enter a valid block-height range (start ≤ end).');
+        setResults([]);
+        return;
+      }
+      const MAX_BLOCKS = 20;
+      const cappedEnd = Math.min(end, start + MAX_BLOCKS - 1);
+      if (cappedEnd < end) {
+        setError(`Range capped to first ${MAX_BLOCKS} blocks (${start}-${cappedEnd}).`);
+      }
+      const out: SearchResult[] = [];
+      for (let h = start; h <= cappedEnd; h++) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!blockRangeStart || !blockRangeEnd) return;
setIsSearching(true);
setTimeout(() => {
setResults(mockResults.filter(r =>
r.block >= parseInt(blockRangeStart) && r.block <= parseInt(blockRangeEnd)
));
setIsSearching(false);
}, 2000);
};
const getRiskColor = (score: number) => {
if (score >= 8) return 'bg-red-600';
if (score >= 6) return 'bg-amber-500';
if (score >= 4) return 'bg-blue-500';
return 'bg-green-600';
};
const formatTimeAgo = (date: Date) => {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) return `${diffInSeconds}s ago`;
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
return `${Math.floor(diffInSeconds / 86400)}d ago`;
try {
const start = parseInt(blockRangeStart, 10);
const end = parseInt(blockRangeEnd, 10);
const out: SearchResult[] = [];
for (let h = start; h <= end && h < start + 20; h++) {
const hashRes = await fetch(`https://blockstream.info/api/block-height/${h}`);
if (!hashRes.ok) continue;
const hash = await hashRes.text();
const txsRes = await fetch(`https://blockstream.info/api/block/${hash}/txs/0`);
if (!txsRes.ok) continue;
const txs = await txsRes.json();
out.push(...(txs || []).slice(0, 10).map(normalize));
}
setResults(out);
if (!blockRangeStart || !blockRangeEnd) return;
setIsSearching(true);
try {
const start = parseInt(blockRangeStart, 10);
const end = parseInt(blockRangeEnd, 10);
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start) {
setError('Enter a valid block-height range (start ≤ end).');
setResults([]);
return;
}
const MAX_BLOCKS = 20;
const cappedEnd = Math.min(end, start + MAX_BLOCKS - 1);
if (cappedEnd < end) {
setError(`Range capped to first ${MAX_BLOCKS} blocks (${start}-${cappedEnd}).`);
}
const out: SearchResult[] = [];
for (let h = start; h <= cappedEnd; h++) {
const hashRes = await fetch(`https://blockstream.info/api/block-height/${h}`);
if (!hashRes.ok) continue;
const hash = await hashRes.text();
const txsRes = await fetch(`https://blockstream.info/api/block/${hash}/txs/0`);
if (!txsRes.ok) continue;
const txs = await txsRes.json();
out.push(...(txs || []).slice(0, 10).map(normalize));
}
setResults(out);
🤖 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 `@src/components/TransactionSearch.tsx` around lines 52 - 67, Validate
blockRangeStart and blockRangeEnd before parsing and surface the 20-block cap:
parse blockRangeStart/blockRangeEnd into numbers (used in the loop with
parseInt) and if either is NaN or end < start call setIsSearching(false) and set
an error/validation state (or early return with a user-facing message) instead
of silently returning empty results; additionally compute the requested length
and if end - start + 1 > 20 either reject the input or set a flag/message like
"showing first 20 of M blocks" (and continue the loop limited to start ..
Math.min(end, start + 19)), ensuring the UI uses that flag/message so users know
the range was truncated; update code paths around the loop that use start, end,
h, normalize, and setResults to respect these validations and messaging.

Comment on lines +65 to +82
const chainStats = payload.chain_stats ?? {};
const mempoolStats = payload.mempool_stats ?? {};
const funded = Number(chainStats.funded_txo_sum || 0) + Number(mempoolStats.funded_txo_sum || 0);
const spent = Number(chainStats.spent_txo_sum || 0) + Number(mempoolStats.spent_txo_sum || 0);
const txCount = Number(chainStats.tx_count || 0) + Number(mempoolStats.tx_count || 0);

if (error) {
console.error('Error adding tracked address:', error);
throw error;
}
const liveData = {
address: normalizedAddress,
balance: (funded - spent) / SATOSHIS_PER_BTC,
transaction_count: txCount,
total_received: funded / SATOSHIS_PER_BTC,
total_sent: spent / SATOSHIS_PER_BTC,
risk_score: estimateRiskScore({
funded_txo_count: Number(chainStats.funded_txo_count || 0),
spent_txo_count: Number(chainStats.spent_txo_count || 0),
tx_count: txCount,
}),
is_flagged: txCount > 1000 || spent > funded * 0.95,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Risk score is computed from a mismatched mix of chain-only and chain+mempool stats.

txCount passed into estimateRiskScore (Line 80) sums chain_stats and mempool_stats, but funded_txo_count and spent_txo_count (Lines 78-79) read only from chainStats. Inside estimateRiskScore, the rule spent_txo_count > funded_txo_count * 0.9 then compares two chain-only counts while the tx_count > N thresholds (Lines 27-29) see the combined count. This produces inconsistent risk scoring for addresses with active mempool activity (an address with many mempool txs can cross the tx_count tier while its txo counts under-represent the same activity).

Pick one convention (all combined or all chain-only) and apply it uniformly. Combined is consistent with how balance, total_received, total_sent, and is_flagged are derived.

🛠️ Proposed fix — combine chain + mempool consistently
       const funded = Number(chainStats.funded_txo_sum || 0) + Number(mempoolStats.funded_txo_sum || 0);
       const spent = Number(chainStats.spent_txo_sum || 0) + Number(mempoolStats.spent_txo_sum || 0);
       const txCount = Number(chainStats.tx_count || 0) + Number(mempoolStats.tx_count || 0);
+      const fundedTxoCount =
+        Number(chainStats.funded_txo_count || 0) + Number(mempoolStats.funded_txo_count || 0);
+      const spentTxoCount =
+        Number(chainStats.spent_txo_count || 0) + Number(mempoolStats.spent_txo_count || 0);

       const liveData = {
         address: normalizedAddress,
         balance: (funded - spent) / SATOSHIS_PER_BTC,
         transaction_count: txCount,
         total_received: funded / SATOSHIS_PER_BTC,
         total_sent: spent / SATOSHIS_PER_BTC,
         risk_score: estimateRiskScore({
-          funded_txo_count: Number(chainStats.funded_txo_count || 0),
-          spent_txo_count: Number(chainStats.spent_txo_count || 0),
+          funded_txo_count: fundedTxoCount,
+          spent_txo_count: spentTxoCount,
           tx_count: txCount,
         }),
         is_flagged: txCount > 1000 || spent > funded * 0.95,
       };
🤖 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 `@src/hooks/useTrackedAddresses.ts` around lines 65 - 82, The risk calculation
mixes chain-only txo counts with combined tx_count; change funded_txo_count and
spent_txo_count to include mempool values like txCount does so estimateRiskScore
receives consistent combined metrics. In useTrackedAddresses, where chainStats
and mempoolStats are read and liveData constructed, compute funded_txo_count =
Number(chainStats.funded_txo_count || 0) + Number(mempoolStats.funded_txo_count
|| 0) and spent_txo_count = Number(chainStats.spent_txo_count || 0) +
Number(mempoolStats.spent_txo_count || 0) before calling estimateRiskScore (keep
using funded, spent, txCount as already done) so estimateRiskScore,
liveData.is_flagged and other derived fields all use the same combined
convention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant