Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
273 changes: 45 additions & 228 deletions src/components/ScriptAnalyzer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
Expand All @@ -7,249 +6,67 @@ import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Search, Code, AlertTriangle, CheckCircle } from 'lucide-react';
import { BitcoinTransactionParser } from '@/utils/bitcoinParser';
import { VulnerabilityScanner } from '@/utils/vulnerabilityScanner';

const ScriptAnalyzer = () => {
const [txid, setTxid] = useState('');
const [rawHex, setRawHex] = useState('');
const [analysis, setAnalysis] = useState<any>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);

const mockAnalysis = {
txid: 'bc1qa5wkgaew2dkv56kfvj5x7epdj4zyrhfx5x7a',
inputs: [
{
scriptSig: 'OP_PUSHDATA1 72 304502210089abcdef...',
decodedScript: ['OP_PUSHDATA1', '304502210089abcdef...', 'OP_PUSHDATA1', '021f2f6e1e50cb6a953935c3601284925decd3fd21bc0b6c86c4b6e436d7b8b8b'],
type: 'P2PKH',
vulnerabilities: ['Reused R-value detected in signature'],
severity: 'critical'
}
],
outputs: [
{
scriptPubKey: 'OP_DUP OP_HASH160 89abcdefabbaabbaabbaabbaabbaabbaabbaabba OP_EQUALVERIFY OP_CHECKSIG',
decodedScript: ['OP_DUP', 'OP_HASH160', '89abcdefabbaabbaabbaabbaabbaabbaabbaabba', 'OP_EQUALVERIFY', 'OP_CHECKSIG'],
type: 'P2PKH',
vulnerabilities: [],
severity: 'safe'
},
{
scriptPubKey: 'OP_RETURN 48656c6c6f20426974636f696e',
decodedScript: ['OP_RETURN', 'Hello Bitcoin'],
type: 'NULL_DATA',
vulnerabilities: ['Potential data leak in OP_RETURN'],
severity: 'medium'
}
],
overallRisk: 'high',
recommendations: [
'Private key may be compromised due to R-value reuse',
'Consider this transaction as high-risk',
'Monitor associated addresses for further activity'
]
};

const handleAnalyze = async () => {
if (!txid && !rawHex) return;

setIsAnalyzing(true);
// Simulate analysis delay
setTimeout(() => {
setAnalysis(mockAnalysis);
setIsAnalyzing(false);
}, 2000);
};

const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical': return 'bg-red-600';
case 'high': return 'bg-amber-500';
case 'medium': return 'bg-blue-500';
case 'low': return 'bg-green-600';
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();
});

const parsed = BitcoinTransactionParser.parseRawTransaction(txHex);
const vulnerabilities = await VulnerabilityScanner.scanTransaction(parsed);

Comment on lines +22 to +26

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.

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',
Comment on lines +36 to +47

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.

})),
overallRisk: vulnerabilities.some((v) => v.severity === 'critical') ? 'critical' : vulnerabilities.length ? 'high' : 'safe',
Comment on lines +36 to +49

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.

recommendations: vulnerabilities.length
? vulnerabilities.map((v) => v.description)
: ['No immediate script-level vulnerabilities detected.'],
});
} catch (error) {
setAnalysis(null);
console.error(error);
} finally {
setIsAnalyzing(false);
}
Comment on lines +54 to 59

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.

};

return (
<div className="space-y-6">
<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>
<Input
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>

{analysis && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Input Scripts */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white text-lg">Input Scripts (scriptSig)</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
<div className="space-y-4">
{analysis.inputs.map((input: any, index: number) => (
<div key={index} className="p-3 bg-slate-700/30 rounded-lg">
<div className="flex items-center justify-between mb-2">
<Badge variant="outline" className="text-xs">
Input #{index + 1} - {input.type}
</Badge>
<Badge className={`${getSeverityColor(input.severity)} text-white text-xs`}>
{input.severity.toUpperCase()}
</Badge>
</div>

<div className="text-xs text-slate-300 font-mono mb-2 break-all">
{input.scriptSig}
</div>

<div className="text-sm text-slate-400 mb-2">
<strong>Decoded:</strong>
<div className="mt-1 space-x-1">
{input.decodedScript.map((op: string, i: number) => (
<span key={i} className="inline-block px-2 py-1 bg-slate-600 rounded text-xs">
{op}
</span>
))}
</div>
</div>

{input.vulnerabilities.length > 0 && (
<div className="mt-2">
{input.vulnerabilities.map((vuln: string, i: number) => (
<div key={i} className="flex items-center text-red-400 text-xs">
<AlertTriangle className="mr-1 h-3 w-3" />
{vuln}
</div>
))}
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
const getSeverityColor = (severity: string) => ({ critical: 'bg-red-600', high: 'bg-amber-500', medium: 'bg-blue-500', low: 'bg-green-600', safe: 'bg-gray-600' }[severity] || 'bg-gray-500');

{/* Output Scripts */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white text-lg">Output Scripts (scriptPubKey)</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
<div className="space-y-4">
{analysis.outputs.map((output: any, index: number) => (
<div key={index} className="p-3 bg-slate-700/30 rounded-lg">
<div className="flex items-center justify-between mb-2">
<Badge variant="outline" className="text-xs">
Output #{index + 1} - {output.type}
</Badge>
<Badge className={`${getSeverityColor(output.severity)} text-white text-xs`}>
{output.severity.toUpperCase()}
</Badge>
</div>

<div className="text-xs text-slate-300 font-mono mb-2 break-all">
{output.scriptPubKey}
</div>

<div className="text-sm text-slate-400 mb-2">
<strong>Decoded:</strong>
<div className="mt-1 space-x-1">
{output.decodedScript.map((op: string, i: number) => (
<span key={i} className="inline-block px-2 py-1 bg-slate-600 rounded text-xs">
{op}
</span>
))}
</div>
</div>

{output.vulnerabilities.length > 0 ? (
<div className="mt-2">
{output.vulnerabilities.map((vuln: string, i: number) => (
<div key={i} className="flex items-center text-amber-400 text-xs">
<AlertTriangle className="mr-1 h-3 w-3" />
{vuln}
</div>
))}
</div>
) : (
<div className="flex items-center text-green-400 text-xs mt-2">
<CheckCircle className="mr-1 h-3 w-3" />
No vulnerabilities detected
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
</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.


{analysis && (
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center justify-between">
Analysis Summary
<Badge className={`${getSeverityColor(analysis.overallRisk)} text-white`}>
{analysis.overallRisk.toUpperCase()} RISK
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<h4 className="text-slate-300 font-medium">Recommendations:</h4>
<ul className="space-y-2">
{analysis.recommendations.map((rec: string, index: number) => (
<li key={index} className="flex items-start text-slate-300">
<AlertTriangle className="mr-2 h-4 w-4 text-amber-400 mt-0.5 flex-shrink-0" />
{rec}
</li>
))}
</ul>
</div>
</CardContent>
</Card>
)}
</div>
);
{analysis && <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"><Card className="bg-slate-800/50 border-slate-700"><CardHeader><CardTitle className="text-white text-lg">Input Scripts (scriptSig)</CardTitle></CardHeader><CardContent><ScrollArea className="h-64"><div className="space-y-4">{analysis.inputs.map((input: any, index: number) => <div key={index} className="p-3 bg-slate-700/30 rounded-lg"><div className="flex items-center justify-between mb-2"><Badge variant="outline" className="text-xs">Input #{index + 1}</Badge><Badge className={`${getSeverityColor(input.severity)} text-white text-xs`}>{input.severity.toUpperCase()}</Badge></div><div className="text-xs text-slate-300 font-mono mb-2 break-all">{input.scriptSig}</div>{input.vulnerabilities.length > 0 && input.vulnerabilities.map((v: string, i: number) => <div key={i} className="flex items-center text-red-400 text-xs"><AlertTriangle className="mr-1 h-3 w-3" />{v}</div>)}</div>)}</div></ScrollArea></CardContent></Card>
<Card className="bg-slate-800/50 border-slate-700"><CardHeader><CardTitle className="text-white text-lg">Output Scripts (scriptPubKey)</CardTitle></CardHeader><CardContent><ScrollArea className="h-64"><div className="space-y-4">{analysis.outputs.map((output: any, index: number) => <div key={index} className="p-3 bg-slate-700/30 rounded-lg"><div className="flex items-center justify-between mb-2"><Badge variant="outline" className="text-xs">Output #{index + 1} - {output.type}</Badge><Badge className={`${getSeverityColor(output.severity)} text-white text-xs`}>{output.severity.toUpperCase()}</Badge></div><div className="text-xs text-slate-300 font-mono mb-2 break-all">{output.scriptPubKey}</div>{output.vulnerabilities.length > 0 ? output.vulnerabilities.map((v: string, i: number) => <div key={i} className="flex items-center text-amber-400 text-xs"><AlertTriangle className="mr-1 h-3 w-3" />{v}</div>) : <div className="flex items-center text-green-400 text-xs mt-2"><CheckCircle className="mr-1 h-3 w-3" />No vulnerabilities detected</div>}</div>)}</div></ScrollArea></CardContent></Card></div>}
</div>;
};

export default ScriptAnalyzer;
Loading