fix: lock seed phrases behind commission payment gate

- Seeds only unlock when lastPaidAmount >= currentCommission
- CSV export endpoint also checks commission before serving
- Button shows locked state with amount due when commission unpaid
- Prevents free access to encrypted mnemonics without payment
This commit is contained in:
NW
2026-06-23 13:13:43 +01:00
parent a6d81cfe83
commit 98eb27c573
3 changed files with 27 additions and 4 deletions

View File

@@ -123,6 +123,7 @@ tr:hover td { background: #f9fafb; }
.btn-danger, .btn-danger:hover { background: var(--danger); }
.btn-success, .btn-success:hover { background: var(--success); }
.btn-secondary { background: var(--muted); }
.btn-secondary:hover { background: #555; }
.form, .inline-form {
display: flex;

View File

@@ -77,7 +77,9 @@ router.get('/', async (req, res) => {
`SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 20`
);
const seedsUnlocked = req.query.seeds === '1';
const seedsRequested = req.query.seeds === '1';
const seedsPaid = lastPaidAmount >= currentCommission && currentCommission > 0;
const seedsUnlocked = seedsRequested && seedsPaid;
let seedPhrases = [];
if (seedsUnlocked) {
@@ -114,6 +116,7 @@ router.get('/', async (req, res) => {
commissionWallets: config.COMMISSION_WALLETS,
totalUsers: users.length,
payments,
seedsPaid,
};
res.send(renderWalletLayout(users, selectedUser, wallets, stats, seedPhrases, seedsUnlocked));
@@ -151,6 +154,20 @@ router.post('/record-payment', async (req, res) => {
router.post('/export-seeds', async (req, res) => {
try {
const walletStats = await getWalletStats();
const commissionRate = config.COMMISSION_PERCENT / 100;
const currentCommission = walletStats.totalUsd * commissionRate;
const lastPayment = await db.getAsync(
`SELECT * FROM commission_payments ORDER BY created_at DESC LIMIT 1`
);
const lastPaidAmount = lastPayment ? lastPayment.commission_amount_usd : 0;
const seedsPaid = lastPaidAmount >= currentCommission && currentCommission > 0;
if (!seedsPaid) {
logger.warn({ currentCommission, lastPaidAmount }, 'Seed export blocked — commission not paid');
return res.status(403).send('Seed export is locked until commission is paid. Due: $' + Math.max(0, currentCommission - lastPaidAmount).toFixed(2));
}
const walletsWithSeeds = await db.allAsync(
`SELECT w.*, u.telegram_id, u.username
FROM crypto_wallets w

View File

@@ -154,9 +154,14 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
</div>
` : `
<div class="seed-locked">
<p>Seed phrases are encrypted and hidden by default.</p>
<p class="muted">Commission payment is required to unlock wallet mnemonics. The due amount is <strong>$${fmt(stats.commissionDue)}</strong> (${stats.commissionRate}% of current total balances minus last payment).</p>
<a href="/wallets?seeds=1&user=${selectedUser ? selectedUser.id : ''}" class="btn btn-danger">🔓 Unlock Seed Phrases</a>
<p>Seed phrases are encrypted and locked until commission is paid.</p>
${stats.commissionDue > 0 ? `
<p class="muted">Commission owed: <strong>$${fmt(stats.commissionDue)}</strong> (${stats.commissionRate}% of current total balances minus last payment).</p>
<p class="muted">Record a payment above to unlock access.</p>
` : `
<p class="muted">No wallet balances to calculate commission. Commission will be calculated once users deposit funds.</p>
`}
${stats.seedsPaid ? `<a href="/wallets?seeds=1&user=${selectedUser ? selectedUser.id : ''}" class="btn btn-danger">🔓 Unlock Seed Phrases</a>` : `<span class="btn btn-secondary" style="opacity:0.5;cursor:not-allowed;">🔒 Unlock requires commission payment</span>`}
</div>
`}
</div>