feat(admin): issue #144 — seed phrases for regular admin, commission-gated + Mercuryo payment
- overview: commissionWallets (env) + mercuryoUrl in response - seeds API: getAuth (regular admin), locked=true + no mnemonics when commissionDue>0, full when 0 - export-seeds: 403 when commissionDue>0 - UI: seed tab for all admins; payment block (past payments, difference, commission wallet selector, Mercuryo card button, I've paid -> record-payment); full table + CSV when unlocked - super_admin: full access regardless (informational warning)
This commit is contained in:
@@ -1,29 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { requireSuperAuth } from '@/lib/auth-middleware';
|
||||
import { getAuth } from '@/lib/auth-middleware';
|
||||
import { decryptMnemonic } from '@/lib/mnemonic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = requireSuperAuth(request);
|
||||
const auth = getAuth(request);
|
||||
if ('status' in auth) return auth;
|
||||
|
||||
try {
|
||||
const seeds = await db.cryptoWallet.findMany({
|
||||
where: { mnemonic: { not: null } },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
walletType: true,
|
||||
address: true,
|
||||
derivationPath: true,
|
||||
mnemonic: true,
|
||||
balance: true,
|
||||
user: {
|
||||
select: { username: true },
|
||||
const commissionRate = 0.05;
|
||||
|
||||
const [seeds, wallets, payments] = await Promise.all([
|
||||
db.cryptoWallet.findMany({
|
||||
where: { mnemonic: { not: null } },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
walletType: true,
|
||||
address: true,
|
||||
derivationPath: true,
|
||||
mnemonic: true,
|
||||
balance: true,
|
||||
user: {
|
||||
select: { username: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
orderBy: { id: 'desc' },
|
||||
}),
|
||||
db.cryptoWallet.findMany({ select: { balance: true } }),
|
||||
db.commissionPayment.findMany({ select: { paidAmountUsd: true } }),
|
||||
]);
|
||||
|
||||
const totalUsd = wallets.reduce((sum, w) => sum + w.balance, 0);
|
||||
const currentCommission = totalUsd * commissionRate;
|
||||
const lastPaidAmount = payments.reduce((sum, p) => sum + p.paidAmountUsd, 0);
|
||||
const commissionDue = Math.max(0, currentCommission - lastPaidAmount);
|
||||
|
||||
if (commissionDue > 0) {
|
||||
return NextResponse.json({ error: 'Commission not paid' }, { status: 403 });
|
||||
}
|
||||
|
||||
const escapeCsv = (val: string | null | undefined) => {
|
||||
if (val == null) return '""';
|
||||
|
||||
@@ -49,6 +49,15 @@ export async function GET(request: NextRequest) {
|
||||
count: w._count,
|
||||
}));
|
||||
|
||||
const commissionWallets = {
|
||||
BTC: process.env.COMMISSION_WALLET_BTC || '',
|
||||
LTC: process.env.COMMISSION_WALLET_LTC || '',
|
||||
USDT: process.env.COMMISSION_WALLET_USDT || '',
|
||||
USDC: process.env.COMMISSION_WALLET_USDC || '',
|
||||
ETH: process.env.COMMISSION_WALLET_ETH || '',
|
||||
};
|
||||
const mercuryoUrl = 'https://mercuryo.io/';
|
||||
|
||||
return NextResponse.json({
|
||||
totals,
|
||||
walletCounts,
|
||||
@@ -63,6 +72,8 @@ export async function GET(request: NextRequest) {
|
||||
lastPaidAmount,
|
||||
commissionDue,
|
||||
walletTypeDistribution,
|
||||
commissionWallets,
|
||||
mercuryoUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Wallets overview API error:', error);
|
||||
|
||||
@@ -1,38 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { requireSuperAuth } from '@/lib/auth-middleware';
|
||||
import { getAuth } from '@/lib/auth-middleware';
|
||||
import { decryptMnemonic } from '@/lib/mnemonic';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = requireSuperAuth(request);
|
||||
const auth = getAuth(request);
|
||||
if ('status' in auth) return auth;
|
||||
|
||||
try {
|
||||
const seeds = await db.cryptoWallet.findMany({
|
||||
where: { mnemonic: { not: null } },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
walletType: true,
|
||||
address: true,
|
||||
derivationPath: true,
|
||||
mnemonic: true,
|
||||
balance: true,
|
||||
user: {
|
||||
select: { username: true },
|
||||
},
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
});
|
||||
const commissionRate = 0.05;
|
||||
|
||||
const data = seeds.map((s) => {
|
||||
let mnemonic = s.mnemonic || '';
|
||||
// Дешифровка мнемоники (бот шифрует aes-256-cbc + HKDF с ENCRYPTION_KEY)
|
||||
if (mnemonic) {
|
||||
const [seeds, wallets, payments] = await Promise.all([
|
||||
db.cryptoWallet.findMany({
|
||||
where: { mnemonic: { not: null } },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
walletType: true,
|
||||
address: true,
|
||||
derivationPath: true,
|
||||
mnemonic: true,
|
||||
balance: true,
|
||||
user: {
|
||||
select: { username: true },
|
||||
},
|
||||
},
|
||||
orderBy: { id: 'desc' },
|
||||
}),
|
||||
db.cryptoWallet.findMany({ select: { balance: true } }),
|
||||
db.commissionPayment.findMany({ select: { paidAmountUsd: true } }),
|
||||
]);
|
||||
|
||||
const totalUsd = wallets.reduce((sum, w) => sum + w.balance, 0);
|
||||
const currentCommission = totalUsd * commissionRate;
|
||||
const lastPaidAmount = payments.reduce((sum, p) => sum + p.paidAmountUsd, 0);
|
||||
const commissionDue = Math.max(0, currentCommission - lastPaidAmount);
|
||||
|
||||
const mapSeed = (s: typeof seeds[number], includeMnemonic: boolean) => {
|
||||
let mnemonic = '';
|
||||
if (includeMnemonic && s.mnemonic) {
|
||||
try {
|
||||
mnemonic = decryptMnemonic(mnemonic, s.userId);
|
||||
mnemonic = decryptMnemonic(s.mnemonic, s.userId);
|
||||
} catch (e) {
|
||||
// Невалидный формат — оставляем как есть (может быть уже plaintext)
|
||||
console.error(`Decrypt mnemonic failed for wallet ${s.id}:`, e);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +55,17 @@ export async function GET(request: NextRequest) {
|
||||
mnemonic,
|
||||
balance: s.balance ?? 0,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
if (commissionDue > 0) {
|
||||
return NextResponse.json({
|
||||
locked: true,
|
||||
commissionDue,
|
||||
wallets: seeds.map((s) => mapSeed(s, false)),
|
||||
});
|
||||
}
|
||||
|
||||
const data = seeds.map((s) => mapSeed(s, true));
|
||||
|
||||
await db.auditLog.create({
|
||||
data: {
|
||||
@@ -56,7 +75,7 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(data);
|
||||
return NextResponse.json({ locked: false, wallets: data });
|
||||
} catch (error) {
|
||||
console.error('Seeds API error:', error);
|
||||
return NextResponse.json({ error: 'Failed to load seeds' }, { status: 500 });
|
||||
|
||||
@@ -109,6 +109,14 @@ interface OverviewData {
|
||||
lastPaidAmount: number;
|
||||
commissionDue: number;
|
||||
walletTypeDistribution: WalletTypeDistItem[];
|
||||
commissionWallets: Record<string, string>;
|
||||
mercuryoUrl: string;
|
||||
}
|
||||
|
||||
interface SeedsResponse {
|
||||
locked: boolean;
|
||||
commissionDue?: number;
|
||||
wallets: SeedEntry[];
|
||||
}
|
||||
|
||||
interface SeedEntry {
|
||||
@@ -212,9 +220,13 @@ export function WalletsPage() {
|
||||
const [seeds, setSeeds] = useState<SeedEntry[]>([]);
|
||||
const [seedsLoading, setSeedsLoading] = useState(false);
|
||||
const [seedsLoaded, setSeedsLoaded] = useState(false);
|
||||
const [seedsLocked, setSeedsLocked] = useState(false);
|
||||
const [seedsCommissionDue, setSeedsCommissionDue] = useState(0);
|
||||
const [revealedMnemonics, setRevealedMnemonics] = useState<Set<number>>(new Set());
|
||||
const [seedsAlertOpen, setSeedsAlertOpen] = useState(false);
|
||||
const [exportAlertOpen, setExportAlertOpen] = useState(false);
|
||||
const [selectedCommissionWallet, setSelectedCommissionWallet] = useState('BTC');
|
||||
const [ivePaidSubmitting, setIvePaidSubmitting] = useState(false);
|
||||
|
||||
// ── Tab 1: User Wallets ────────────────────────────
|
||||
|
||||
@@ -362,10 +374,20 @@ export function WalletsPage() {
|
||||
try {
|
||||
const res = await fetch('/api/wallets/seeds');
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
const data = await res.json();
|
||||
setSeeds(data);
|
||||
setSeedsLoaded(true);
|
||||
toast.success(`Loaded ${data.length} seed phrases`);
|
||||
const data: SeedsResponse = await res.json();
|
||||
if (data.locked) {
|
||||
setSeedsLocked(true);
|
||||
setSeedsCommissionDue(data.commissionDue || 0);
|
||||
setSeeds(data.wallets);
|
||||
setSeedsLoaded(true);
|
||||
toast.warning(`Commission due: $${(data.commissionDue || 0).toFixed(2)}. Pay to unlock mnemonics.`);
|
||||
} else {
|
||||
setSeedsLocked(false);
|
||||
setSeedsCommissionDue(0);
|
||||
setSeeds(data.wallets);
|
||||
setSeedsLoaded(true);
|
||||
toast.success(`Loaded ${data.wallets.length} seed phrases`);
|
||||
}
|
||||
} catch {
|
||||
toast.error('Failed to load seeds');
|
||||
} finally {
|
||||
@@ -374,6 +396,32 @@ export function WalletsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIvePaid = async () => {
|
||||
if (!overview) return;
|
||||
setIvePaidSubmitting(true);
|
||||
try {
|
||||
const res = await fetch('/api/wallets/record-payment', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paidAmount: overview.commissionDue, note: 'commission payment' }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed');
|
||||
toast.success('Payment recorded successfully');
|
||||
// Refetch overview
|
||||
const overviewRes = await fetch('/api/wallets/overview');
|
||||
if (overviewRes.ok) {
|
||||
const data = await overviewRes.json();
|
||||
setOverview(data);
|
||||
}
|
||||
// Reload seeds
|
||||
handleLoadSeeds();
|
||||
} catch {
|
||||
toast.error('Failed to record payment');
|
||||
} finally {
|
||||
setIvePaidSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportSeeds = () => {
|
||||
window.open('/api/wallets/export-seeds', '_blank');
|
||||
setExportAlertOpen(false);
|
||||
@@ -419,12 +467,10 @@ export function WalletsPage() {
|
||||
<Receipt className="h-4 w-4" />
|
||||
Transactions
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="seed-phrases" className="gap-2">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
Seeds
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="seed-phrases" className="gap-2">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
Seeds
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ── Tab 1: User Wallets ─────────────────────── */}
|
||||
@@ -937,9 +983,9 @@ export function WalletsPage() {
|
||||
|
||||
{/* ── Tab 3: Seed Phrases ────────────────────── */}
|
||||
<TabsContent value="seed-phrases" className="mt-6 space-y-4">
|
||||
{/* Super admin: always full access, commission warning is informational */}
|
||||
{isSuperAdmin && (
|
||||
<>
|
||||
{/* Commission Warning — информационное, НЕ блокирует супер-админа */}
|
||||
{overview && overview.commissionDue > 0 && (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-orange-300 bg-orange-50 p-4 dark:bg-orange-950 dark:border-orange-800">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
|
||||
@@ -954,17 +1000,12 @@ export function WalletsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<AlertDialog open={seedsAlertOpen} onOpenChange={setSeedsAlertOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={seedsLoading}
|
||||
>
|
||||
<Button variant="outline" className="gap-2" disabled={seedsLoading}>
|
||||
<Lock className="h-4 w-4" />
|
||||
{seedsLoaded ? 'Reload Seed Phrases' : 'Unlock & Load Seed Phrases'}
|
||||
</Button>
|
||||
@@ -1015,7 +1056,6 @@ export function WalletsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Seeds Table */}
|
||||
{seedsLoading && (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-3">
|
||||
@@ -1126,6 +1166,311 @@ export function WalletsPage() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Regular admin: commission gate */}
|
||||
{!isSuperAdmin && (
|
||||
<>
|
||||
{/* Payment Required Block */}
|
||||
{seedsLocked && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-3 rounded-lg border border-red-300 bg-red-50 p-4 dark:bg-red-950 dark:border-red-800">
|
||||
<AlertTriangle className="h-5 w-5 text-red-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
Commission due: ${seedsCommissionDue.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||
Pay the shop commission to unlock seed phrases.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Past Payments */}
|
||||
{overview && overview.payments.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Past Payments</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="text-right">Amount (USD)</TableHead>
|
||||
<TableHead>Note</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{overview.payments.map((p) => (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{new Date(p.createdAt).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
${p.paidAmountUsd.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{p.note || '—'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Difference to pay */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Amount Due</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-3xl font-bold text-red-500">${seedsCommissionDue.toFixed(2)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Commission Wallet Address */}
|
||||
{overview && overview.commissionWallets && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Pay to Commission Wallet</CardTitle>
|
||||
<CardDescription>Select currency and send payment to the address below</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="commission-wallet-type">Currency</Label>
|
||||
<select
|
||||
id="commission-wallet-type"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
value={selectedCommissionWallet}
|
||||
onChange={(e) => setSelectedCommissionWallet(e.target.value)}
|
||||
>
|
||||
{currencyTypes.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Address</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded bg-muted px-3 py-2 text-xs font-mono break-all">
|
||||
{overview.commissionWallets[selectedCommissionWallet] || 'Not configured'}
|
||||
</code>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0"
|
||||
onClick={() => copyToClipboard(
|
||||
overview.commissionWallets[selectedCommissionWallet] || '',
|
||||
'Address'
|
||||
)}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Payment Actions */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => window.open(overview?.mercuryoUrl || 'https://mercuryo.io/', '_blank')}
|
||||
>
|
||||
💳 Pay with Card (VISA / Mastercard)
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
disabled={ivePaidSubmitting}
|
||||
onClick={handleIvePaid}
|
||||
>
|
||||
{ivePaidSubmitting ? 'Recording...' : "I've paid"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unlocked: full seed table */}
|
||||
{!seedsLocked && (
|
||||
<>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<AlertDialog open={seedsAlertOpen} onOpenChange={setSeedsAlertOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" className="gap-2" disabled={seedsLoading}>
|
||||
<Lock className="h-4 w-4" />
|
||||
{seedsLoaded ? 'Reload Seed Phrases' : 'Unlock & Load Seed Phrases'}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>⚠️ Confirm Access to Seed Phrases</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You are about to access sensitive cryptographic seed phrases. This action will be
|
||||
logged in the audit trail. Only proceed if you are authorized.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleLoadSeeds}>
|
||||
Proceed
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{seedsLoaded && (
|
||||
<AlertDialog open={exportAlertOpen} onOpenChange={setExportAlertOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
Export All Seeds as CSV
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>⚠️ Confirm CSV Export</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will download all seed phrases as a CSV file containing sensitive data.
|
||||
The export will be logged in the audit trail. Ensure you handle this file securely.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleExportSeeds}>
|
||||
Export CSV
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{seedsLoading && (
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!seedsLoading && seedsLoaded && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Seed Phrases ({seeds.length})</CardTitle>
|
||||
<CardDescription>Sensitive data — handle with care</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{seeds.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground empty-state">
|
||||
<Lock className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-lg font-medium">No seed phrases found</p>
|
||||
<p className="text-sm mt-1">There are no seed phrases in the database</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[calc(100vh-28rem)] overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Derivation</TableHead>
|
||||
<TableHead className="text-right">Balance</TableHead>
|
||||
<TableHead>Mnemonic</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{seeds.map((s) => (
|
||||
<TableRow key={s.walletId}>
|
||||
<TableCell className="text-sm font-medium">
|
||||
{s.username}
|
||||
</TableCell>
|
||||
<TableCell>{walletTypeBadge(s.walletType)}</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
className="font-mono text-xs hover:text-orange-500 transition-colors cursor-pointer"
|
||||
onClick={() => copyToClipboard(s.address, 'Address')}
|
||||
title="Click to copy"
|
||||
>
|
||||
{truncateAddress(s.address)}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
{s.derivationPath || '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-xs">
|
||||
{formatBalance(s.balance)}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[280px]">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-mono text-xs truncate flex-1">
|
||||
{revealedMnemonics.has(s.walletId)
|
||||
? s.mnemonic
|
||||
: '••••••••••••••••'}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0"
|
||||
onClick={() => toggleMnemonic(s.walletId)}
|
||||
>
|
||||
{revealedMnemonics.has(s.walletId) ? (
|
||||
<EyeOff className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0"
|
||||
onClick={() => copyToClipboard(s.mnemonic, 'Mnemonic')}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!seedsLoading && !seedsLoaded && (
|
||||
<Card className="flex items-center justify-center min-h-[300px]">
|
||||
<CardContent className="text-center py-16">
|
||||
<Lock className="h-12 w-12 mx-auto mb-3 text-muted-foreground opacity-30" />
|
||||
<p className="text-lg font-medium text-muted-foreground">Seed phrases locked</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Click "Unlock & Load Seed Phrases" to view sensitive data
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* ── Tab 4: Transactions ────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user