A complete blockchain implementation featuring: - synord: Full node with GHOSTDAG consensus - explorer-web: Modern React blockchain explorer with 3D DAG visualization - CLI wallet and tools - Smart contract SDK and example contracts (DEX, NFT, token) - WASM crypto library for browser/mobile
262 lines
9.3 KiB
TypeScript
262 lines
9.3 KiB
TypeScript
/**
|
|
* Visual flow diagram showing transaction inputs and outputs.
|
|
* Creates a Sankey-style visualization of fund flow.
|
|
*/
|
|
|
|
import { Link } from 'react-router-dom';
|
|
import { ArrowRight, Coins, Wallet, Gift } from 'lucide-react';
|
|
import { truncateHash, formatSynor } from '../lib/utils';
|
|
import type { ExplorerInput, ExplorerOutput } from '../lib/types';
|
|
|
|
interface TransactionFlowDiagramProps {
|
|
inputs: ExplorerInput[];
|
|
outputs: ExplorerOutput[];
|
|
isCoinbase: boolean;
|
|
totalInput: number;
|
|
totalOutput: number;
|
|
fee: number;
|
|
}
|
|
|
|
export default function TransactionFlowDiagram({
|
|
inputs,
|
|
outputs,
|
|
isCoinbase,
|
|
totalInput,
|
|
totalOutput,
|
|
fee,
|
|
}: TransactionFlowDiagramProps) {
|
|
// Calculate percentages for visual sizing
|
|
const maxValue = Math.max(totalInput, totalOutput);
|
|
|
|
return (
|
|
<div className="relative overflow-hidden rounded-2xl border border-gray-700/50 bg-gray-900/40 backdrop-blur-xl p-6">
|
|
{/* Background gradient */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-green-500/5 via-transparent to-red-500/5" />
|
|
|
|
<div className="relative">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center gap-2">
|
|
<Coins size={18} className="text-synor-400" />
|
|
<h3 className="font-semibold">Transaction Flow</h3>
|
|
</div>
|
|
{!isCoinbase && fee > 0 && (
|
|
<div className="text-sm text-gray-400">
|
|
Fee: <span className="text-amber-400 font-mono">{formatSynor(fee, 4)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-[1fr_auto_1fr] gap-4 items-center">
|
|
{/* Inputs Column */}
|
|
<div className="space-y-2">
|
|
<div className="text-xs text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-red-500" />
|
|
Inputs
|
|
</div>
|
|
|
|
{isCoinbase ? (
|
|
<CoinbaseInput />
|
|
) : (
|
|
<div className="space-y-2 max-h-64 overflow-y-auto scrollbar-thin pr-2">
|
|
{inputs.map((input, i) => (
|
|
<InputNode
|
|
key={i}
|
|
input={input}
|
|
percentage={input.value ? (input.value / maxValue) * 100 : 50}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Total input */}
|
|
{!isCoinbase && (
|
|
<div className="pt-2 border-t border-gray-700/50 mt-3">
|
|
<div className="text-xs text-gray-500">Total Input</div>
|
|
<div className="text-lg font-bold text-red-400 font-mono">
|
|
{formatSynor(totalInput, 4)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Center Flow Arrow */}
|
|
<div className="flex flex-col items-center justify-center px-4">
|
|
{/* Animated flow lines */}
|
|
<div className="relative h-48 w-16 flex items-center justify-center">
|
|
{/* Background track */}
|
|
<div className="absolute inset-y-8 left-1/2 -translate-x-1/2 w-1 bg-gray-700 rounded-full" />
|
|
|
|
{/* Animated particles */}
|
|
<div className="absolute inset-y-8 left-1/2 -translate-x-1/2 w-1 overflow-hidden rounded-full">
|
|
<div className="absolute w-full h-4 bg-gradient-to-b from-transparent via-synor-500 to-transparent animate-flow" />
|
|
</div>
|
|
|
|
{/* Central transaction icon */}
|
|
<div className="relative z-10 p-3 rounded-full bg-gray-800 border-2 border-synor-500 shadow-[0_0_20px_rgba(124,58,237,0.4)]">
|
|
<ArrowRight size={20} className="text-synor-400" />
|
|
</div>
|
|
|
|
{/* Top fade */}
|
|
<div className="absolute top-0 inset-x-0 h-8 bg-gradient-to-b from-gray-900/40 to-transparent" />
|
|
{/* Bottom fade */}
|
|
<div className="absolute bottom-0 inset-x-0 h-8 bg-gradient-to-t from-gray-900/40 to-transparent" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Outputs Column */}
|
|
<div className="space-y-2">
|
|
<div className="text-xs text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
Outputs
|
|
</div>
|
|
|
|
<div className="space-y-2 max-h-64 overflow-y-auto scrollbar-thin pr-2">
|
|
{outputs.map((output, i) => (
|
|
<OutputNode
|
|
key={i}
|
|
output={output}
|
|
index={i}
|
|
percentage={(output.value / maxValue) * 100}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Total output */}
|
|
<div className="pt-2 border-t border-gray-700/50 mt-3">
|
|
<div className="text-xs text-gray-500">Total Output</div>
|
|
<div className="text-lg font-bold text-green-400 font-mono">
|
|
{formatSynor(totalOutput, 4)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Flow summary bar */}
|
|
<div className="mt-6 pt-4 border-t border-gray-700/50">
|
|
<div className="flex items-center gap-2 h-3">
|
|
{/* Input portion */}
|
|
<div className="flex-1 h-full bg-gray-800 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-red-600 to-red-500 transition-all duration-500"
|
|
style={{ width: `${(totalInput / maxValue) * 100}%` }}
|
|
/>
|
|
</div>
|
|
<ArrowRight size={14} className="text-gray-600 flex-shrink-0" />
|
|
{/* Output portion */}
|
|
<div className="flex-1 h-full bg-gray-800 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-green-600 to-green-500 transition-all duration-500"
|
|
style={{ width: `${(totalOutput / maxValue) * 100}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CSS for flow animation */}
|
|
<style>{`
|
|
@keyframes flow {
|
|
0% { transform: translateY(-100%); }
|
|
100% { transform: translateY(calc(100% + 12rem)); }
|
|
}
|
|
.animate-flow {
|
|
animation: flow 2s linear infinite;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CoinbaseInput() {
|
|
return (
|
|
<div className="flex items-center gap-3 px-4 py-3 rounded-lg bg-amber-900/20 border border-amber-700/50">
|
|
<div className="p-2 rounded-lg bg-amber-500/20">
|
|
<Gift size={18} className="text-amber-400" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium text-amber-300">Block Reward</div>
|
|
<div className="text-xs text-gray-500">Coinbase Transaction</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface InputNodeProps {
|
|
input: ExplorerInput;
|
|
percentage: number;
|
|
}
|
|
|
|
function InputNode({ input, percentage }: InputNodeProps) {
|
|
const barWidth = Math.max(20, Math.min(100, percentage));
|
|
|
|
return (
|
|
<div className="relative group">
|
|
{/* Value bar background */}
|
|
<div
|
|
className="absolute inset-0 bg-red-500/10 rounded-lg transition-all duration-300"
|
|
style={{ width: `${barWidth}%` }}
|
|
/>
|
|
|
|
<div className="relative flex items-center gap-3 px-3 py-2.5 rounded-lg border border-gray-700/50 group-hover:border-gray-600 transition-colors">
|
|
<Wallet size={14} className="text-gray-500 flex-shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
{input.address ? (
|
|
<Link
|
|
to={`/address/${input.address}`}
|
|
className="font-mono text-xs text-gray-300 hover:text-synor-400 transition-colors truncate block"
|
|
>
|
|
{truncateHash(input.address, 8, 8)}
|
|
</Link>
|
|
) : (
|
|
<span className="font-mono text-xs text-gray-500">Unknown</span>
|
|
)}
|
|
</div>
|
|
{input.value !== undefined && (
|
|
<span className="text-xs font-mono text-red-400 flex-shrink-0">
|
|
-{formatSynor(input.value, 2)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface OutputNodeProps {
|
|
output: ExplorerOutput;
|
|
index: number;
|
|
percentage: number;
|
|
}
|
|
|
|
function OutputNode({ output, percentage }: OutputNodeProps) {
|
|
const barWidth = Math.max(20, Math.min(100, percentage));
|
|
|
|
return (
|
|
<div className="relative group">
|
|
{/* Value bar background */}
|
|
<div
|
|
className="absolute inset-0 bg-green-500/10 rounded-lg transition-all duration-300"
|
|
style={{ width: `${barWidth}%` }}
|
|
/>
|
|
|
|
<div className="relative flex items-center gap-3 px-3 py-2.5 rounded-lg border border-gray-700/50 group-hover:border-gray-600 transition-colors">
|
|
<Wallet size={14} className="text-gray-500 flex-shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
{output.address ? (
|
|
<Link
|
|
to={`/address/${output.address}`}
|
|
className="font-mono text-xs text-gray-300 hover:text-synor-400 transition-colors truncate block"
|
|
>
|
|
{truncateHash(output.address, 8, 8)}
|
|
</Link>
|
|
) : (
|
|
<span className="font-mono text-xs text-gray-500">{output.scriptType}</span>
|
|
)}
|
|
</div>
|
|
<span className="text-xs font-mono text-green-400 flex-shrink-0">
|
|
+{formatSynor(output.value, 2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|