feat: display truncated lightning address for winners

- Add truncateLightningAddress utility (shows first 2 chars + ******)
- Backend: Include winner_address in past-wins API response
- Frontend: Display truncated address in past winners list
- Telegram: Add truncated address to draw announcements for transparency

Example: username@blink.sv -> us******@blink.sv
This commit is contained in:
Michilis
2025-12-12 16:20:18 +00:00
parent 00f09236a3
commit 3bc067f691
6 changed files with 65 additions and 8 deletions

View File

@@ -503,6 +503,23 @@ interface PastWinRow {
pot_after_fee_sats: number | null; pot_after_fee_sats: number | null;
buyer_name: string | null; buyer_name: string | null;
serial_number: number | null; serial_number: number | null;
winning_lightning_address: string | null;
}
/**
* Truncate lightning address for privacy
* "username@blink.sv" -> "us******@blink.sv"
*/
function truncateLightningAddress(address: string | null): string | null {
if (!address || !address.includes('@')) return address;
const [username, domain] = address.split('@');
// Show first 2 chars of username, then asterisks
const visibleChars = Math.min(2, username.length);
const truncatedUsername = username.substring(0, visibleChars) + '******';
return `${truncatedUsername}@${domain}`;
} }
/** /**
@@ -523,6 +540,7 @@ export async function getPastWins(req: Request, res: Response) {
jc.scheduled_at, jc.scheduled_at,
jc.pot_total_sats, jc.pot_total_sats,
jc.pot_after_fee_sats, jc.pot_after_fee_sats,
jc.winning_lightning_address,
tp.buyer_name, tp.buyer_name,
t.serial_number t.serial_number
FROM jackpot_cycles jc FROM jackpot_cycles jc
@@ -544,6 +562,7 @@ export async function getPastWins(req: Request, res: Response) {
? parseInt(row.pot_after_fee_sats.toString()) ? parseInt(row.pot_after_fee_sats.toString())
: null, : null,
winner_name: row.buyer_name || 'Anon', winner_name: row.buyer_name || 'Anon',
winner_address: truncateLightningAddress(row.winning_lightning_address),
winning_ticket_serial: row.serial_number winning_ticket_serial: row.serial_number
? parseInt(row.serial_number.toString()) ? parseInt(row.serial_number.toString())
: null, : null,

View File

@@ -13,6 +13,7 @@ interface PastWin {
pot_total_sats: number; pot_total_sats: number;
pot_after_fee_sats: number | null; pot_after_fee_sats: number | null;
winner_name: string; winner_name: string;
winner_address: string | null;
winning_ticket_serial: number | null; winning_ticket_serial: number | null;
} }
@@ -87,13 +88,19 @@ export default function PastWinsPage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div> <div>
<div className="text-gray-400 mb-1">{STRINGS.pastWins.winner}</div> <div className="text-gray-400 mb-1">{STRINGS.pastWins.winner}</div>
<div className="text-white font-semibold"> <div className="text-white font-semibold">
{win.winner_name || 'Anon'} {win.winner_name || 'Anon'}
</div> </div>
</div> </div>
<div>
<div className="text-gray-400 mb-1">{STRINGS.pastWins.address}</div>
<div className="text-white font-mono text-xs">
{win.winner_address || 'N/A'}
</div>
</div>
<div> <div>
<div className="text-gray-400 mb-1">{STRINGS.pastWins.ticket}</div> <div className="text-gray-400 mb-1">{STRINGS.pastWins.ticket}</div>
<div className="text-white"> <div className="text-white">

View File

@@ -67,6 +67,7 @@ export const STRINGS = {
description: 'Recent jackpots and their champions.', description: 'Recent jackpots and their champions.',
noWins: 'No completed jackpots yet. Check back soon!', noWins: 'No completed jackpots yet. Check back soon!',
winner: 'Winner', winner: 'Winner',
address: 'Lightning Address',
ticket: 'Ticket #', ticket: 'Ticket #',
pot: 'Pot', pot: 'Pot',
drawTime: 'Draw Time', drawTime: 'Draw Time',

View File

@@ -464,11 +464,13 @@ Good luck next round! 🍀`,
winnerName: string, winnerName: string,
winningTicket: string, winningTicket: string,
prizeSats: string, prizeSats: string,
totalTickets: number totalTickets: number,
winnerAddress?: string
) => ) =>
`🎰 *JACKPOT DRAW COMPLETE!* 🎰 `🎰 *JACKPOT DRAW COMPLETE!* 🎰
🏆 *Winner:* ${winnerName} 🏆 *Winner:* ${winnerName}
⚡ *Address:* \`${winnerAddress || 'N/A'}\`
🎟 *Winning Ticket:* #${winningTicket} 🎟 *Winning Ticket:* #${winningTicket}
💰 *Prize:* ${prizeSats} sats 💰 *Prize:* ${prizeSats} sats
📊 *Total Tickets:* ${totalTickets} 📊 *Total Tickets:* ${totalTickets}

View File

@@ -6,6 +6,7 @@ import { logger } from './logger';
import { messages } from '../messages'; import { messages } from '../messages';
import { GroupSettings, reminderTimeToMinutes, formatReminderTime, ReminderTime, DEFAULT_GROUP_REMINDER_SLOTS } from '../types/groups'; import { GroupSettings, reminderTimeToMinutes, formatReminderTime, ReminderTime, DEFAULT_GROUP_REMINDER_SLOTS } from '../types/groups';
import { TelegramUser } from '../types'; import { TelegramUser } from '../types';
import { truncateLightningAddress } from '../utils/format';
interface CycleInfo { interface CycleInfo {
id: string; id: string;
@@ -158,6 +159,7 @@ class NotificationScheduler {
let winnerDisplayName = 'Anon'; let winnerDisplayName = 'Anon';
let winnerTicketNumber = '0000'; let winnerTicketNumber = '0000';
let winnerTelegramId: number | null = null; let winnerTelegramId: number | null = null;
let winnerLightningAddress = '';
let potSats = 0; let potSats = 0;
let prizeSats = 0; let prizeSats = 0;
let payoutStatus = 'processing'; let payoutStatus = 'processing';
@@ -174,6 +176,7 @@ class NotificationScheduler {
const user = await stateManager.getUser(telegramId); const user = await stateManager.getUser(telegramId);
winnerTelegramId = telegramId; winnerTelegramId = telegramId;
winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
winnerLightningAddress = status.purchase.lightning_address || '';
const winningTicket = status.tickets.find(t => t.is_winning_ticket); const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) { if (winningTicket) {
@@ -240,7 +243,8 @@ class NotificationScheduler {
winnerDisplayName, winnerDisplayName,
winnerTicketNumber, winnerTicketNumber,
potSats, potSats,
uniqueUserCount uniqueUserCount,
winnerLightningAddress
); );
} }
@@ -252,11 +256,13 @@ class NotificationScheduler {
winnerDisplayName: string, winnerDisplayName: string,
winnerTicketNumber: string, winnerTicketNumber: string,
potSats: number, potSats: number,
totalParticipants: number totalParticipants: number,
winnerLightningAddress: string
): Promise<void> { ): Promise<void> {
if (!this.bot) return; if (!this.bot) return;
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements'); const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
const truncatedAddress = truncateLightningAddress(winnerLightningAddress);
for (const group of groups) { for (const group of groups) {
try { try {
@@ -264,7 +270,8 @@ class NotificationScheduler {
winnerDisplayName, winnerDisplayName,
`#${winnerTicketNumber}`, `#${winnerTicketNumber}`,
potSats.toLocaleString(), potSats.toLocaleString(),
totalParticipants totalParticipants,
truncatedAddress
); );
await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' }); await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' });
@@ -384,6 +391,7 @@ class NotificationScheduler {
let winnerDisplayName = 'Anon'; let winnerDisplayName = 'Anon';
let winnerTicketNumber = '0000'; let winnerTicketNumber = '0000';
let winnerTelegramId: number | null = null; let winnerTelegramId: number | null = null;
let winnerLightningAddress = '';
let prizeSats = 0; let prizeSats = 0;
let payoutStatus = 'processing'; let payoutStatus = 'processing';
@@ -397,6 +405,7 @@ class NotificationScheduler {
const user = await stateManager.getUser(telegramId); const user = await stateManager.getUser(telegramId);
winnerTelegramId = telegramId; winnerTelegramId = telegramId;
winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon';
winnerLightningAddress = status.purchase.lightning_address || '';
const winningTicket = status.tickets.find(t => t.is_winning_ticket); const winningTicket = status.tickets.find(t => t.is_winning_ticket);
if (winningTicket) { if (winningTicket) {
@@ -458,7 +467,7 @@ class NotificationScheduler {
// Send group announcements // Send group announcements
const uniqueUserCount = userPurchases.size; const uniqueUserCount = userPurchases.size;
await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount); await this.sendGroupDrawAnnouncements(cycle, winnerDisplayName, winnerTicketNumber, uniqueUserCount, winnerLightningAddress);
} }
/** /**
@@ -468,11 +477,13 @@ class NotificationScheduler {
cycle: CycleInfo, cycle: CycleInfo,
winnerDisplayName: string, winnerDisplayName: string,
winnerTicketNumber: string, winnerTicketNumber: string,
totalParticipants: number totalParticipants: number,
winnerLightningAddress: string
): Promise<void> { ): Promise<void> {
if (!this.bot) return; if (!this.bot) return;
const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements'); const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements');
const truncatedAddress = truncateLightningAddress(winnerLightningAddress);
for (const group of groups) { for (const group of groups) {
const delay = (group.announcementDelaySeconds || 0) * 1000; const delay = (group.announcementDelaySeconds || 0) * 1000;
@@ -483,7 +494,8 @@ class NotificationScheduler {
winnerDisplayName, winnerDisplayName,
`#${winnerTicketNumber}`, `#${winnerTicketNumber}`,
cycle.pot_total_sats.toLocaleString(), cycle.pot_total_sats.toLocaleString(),
totalParticipants totalParticipants,
truncatedAddress
); );
if (this.bot) { if (this.bot) {

View File

@@ -125,4 +125,20 @@ export function truncate(str: string, maxLength: number): string {
return str.substring(0, maxLength - 3) + '...'; return str.substring(0, maxLength - 3) + '...';
} }
/**
* Truncate lightning address for privacy
* "username@blink.sv" -> "us******@blink.sv"
*/
export function truncateLightningAddress(address: string): string {
if (!address || !address.includes('@')) return address;
const [username, domain] = address.split('@');
// Show first 2 chars of username, then asterisks
const visibleChars = Math.min(2, username.length);
const truncatedUsername = username.substring(0, visibleChars) + '******';
return `${truncatedUsername}@${domain}`;
}