diff --git a/back_end/src/controllers/public.ts b/back_end/src/controllers/public.ts index 573cfc3..1019d85 100644 --- a/back_end/src/controllers/public.ts +++ b/back_end/src/controllers/public.ts @@ -503,6 +503,23 @@ interface PastWinRow { pot_after_fee_sats: number | null; buyer_name: string | 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.pot_total_sats, jc.pot_after_fee_sats, + jc.winning_lightning_address, tp.buyer_name, t.serial_number FROM jackpot_cycles jc @@ -544,6 +562,7 @@ export async function getPastWins(req: Request, res: Response) { ? parseInt(row.pot_after_fee_sats.toString()) : null, winner_name: row.buyer_name || 'Anon', + winner_address: truncateLightningAddress(row.winning_lightning_address), winning_ticket_serial: row.serial_number ? parseInt(row.serial_number.toString()) : null, diff --git a/front_end/src/app/past-wins/page.tsx b/front_end/src/app/past-wins/page.tsx index c47027d..c354fbd 100644 --- a/front_end/src/app/past-wins/page.tsx +++ b/front_end/src/app/past-wins/page.tsx @@ -13,6 +13,7 @@ interface PastWin { pot_total_sats: number; pot_after_fee_sats: number | null; winner_name: string; + winner_address: string | null; winning_ticket_serial: number | null; } @@ -87,13 +88,19 @@ export default function PastWinsPage() { -
+
{STRINGS.pastWins.winner}
{win.winner_name || 'Anon'}
+
+
{STRINGS.pastWins.address}
+
+ {win.winner_address || 'N/A'} +
+
{STRINGS.pastWins.ticket}
diff --git a/front_end/src/constants/strings.ts b/front_end/src/constants/strings.ts index e3ad691..7f14e96 100644 --- a/front_end/src/constants/strings.ts +++ b/front_end/src/constants/strings.ts @@ -67,6 +67,7 @@ export const STRINGS = { description: 'Recent jackpots and their champions.', noWins: 'No completed jackpots yet. Check back soon!', winner: 'Winner', + address: 'Lightning Address', ticket: 'Ticket #', pot: 'Pot', drawTime: 'Draw Time', diff --git a/telegram_bot/src/messages/index.ts b/telegram_bot/src/messages/index.ts index c1d820f..d6f6627 100644 --- a/telegram_bot/src/messages/index.ts +++ b/telegram_bot/src/messages/index.ts @@ -464,11 +464,13 @@ Good luck next round! 🍀`, winnerName: string, winningTicket: string, prizeSats: string, - totalTickets: number + totalTickets: number, + winnerAddress?: string ) => `🎰 *JACKPOT DRAW COMPLETE!* 🎰 🏆 *Winner:* ${winnerName} +⚡ *Address:* \`${winnerAddress || 'N/A'}\` 🎟 *Winning Ticket:* #${winningTicket} 💰 *Prize:* ${prizeSats} sats 📊 *Total Tickets:* ${totalTickets} diff --git a/telegram_bot/src/services/notificationScheduler.ts b/telegram_bot/src/services/notificationScheduler.ts index f57efc4..c73d811 100644 --- a/telegram_bot/src/services/notificationScheduler.ts +++ b/telegram_bot/src/services/notificationScheduler.ts @@ -6,6 +6,7 @@ import { logger } from './logger'; import { messages } from '../messages'; import { GroupSettings, reminderTimeToMinutes, formatReminderTime, ReminderTime, DEFAULT_GROUP_REMINDER_SLOTS } from '../types/groups'; import { TelegramUser } from '../types'; +import { truncateLightningAddress } from '../utils/format'; interface CycleInfo { id: string; @@ -158,6 +159,7 @@ class NotificationScheduler { let winnerDisplayName = 'Anon'; let winnerTicketNumber = '0000'; let winnerTelegramId: number | null = null; + let winnerLightningAddress = ''; let potSats = 0; let prizeSats = 0; let payoutStatus = 'processing'; @@ -174,6 +176,7 @@ class NotificationScheduler { const user = await stateManager.getUser(telegramId); winnerTelegramId = telegramId; winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; + winnerLightningAddress = status.purchase.lightning_address || ''; const winningTicket = status.tickets.find(t => t.is_winning_ticket); if (winningTicket) { @@ -240,7 +243,8 @@ class NotificationScheduler { winnerDisplayName, winnerTicketNumber, potSats, - uniqueUserCount + uniqueUserCount, + winnerLightningAddress ); } @@ -252,11 +256,13 @@ class NotificationScheduler { winnerDisplayName: string, winnerTicketNumber: string, potSats: number, - totalParticipants: number + totalParticipants: number, + winnerLightningAddress: string ): Promise { if (!this.bot) return; const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements'); + const truncatedAddress = truncateLightningAddress(winnerLightningAddress); for (const group of groups) { try { @@ -264,7 +270,8 @@ class NotificationScheduler { winnerDisplayName, `#${winnerTicketNumber}`, potSats.toLocaleString(), - totalParticipants + totalParticipants, + truncatedAddress ); await this.bot.sendMessage(group.groupId, message, { parse_mode: 'Markdown' }); @@ -384,6 +391,7 @@ class NotificationScheduler { let winnerDisplayName = 'Anon'; let winnerTicketNumber = '0000'; let winnerTelegramId: number | null = null; + let winnerLightningAddress = ''; let prizeSats = 0; let payoutStatus = 'processing'; @@ -397,6 +405,7 @@ class NotificationScheduler { const user = await stateManager.getUser(telegramId); winnerTelegramId = telegramId; winnerDisplayName = user ? stateManager.getDisplayName(user) : 'Anon'; + winnerLightningAddress = status.purchase.lightning_address || ''; const winningTicket = status.tickets.find(t => t.is_winning_ticket); if (winningTicket) { @@ -458,7 +467,7 @@ class NotificationScheduler { // Send group announcements 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, winnerDisplayName: string, winnerTicketNumber: string, - totalParticipants: number + totalParticipants: number, + winnerLightningAddress: string ): Promise { if (!this.bot) return; const groups = await groupStateManager.getGroupsWithFeature('drawAnnouncements'); + const truncatedAddress = truncateLightningAddress(winnerLightningAddress); for (const group of groups) { const delay = (group.announcementDelaySeconds || 0) * 1000; @@ -483,7 +494,8 @@ class NotificationScheduler { winnerDisplayName, `#${winnerTicketNumber}`, cycle.pot_total_sats.toLocaleString(), - totalParticipants + totalParticipants, + truncatedAddress ); if (this.bot) { diff --git a/telegram_bot/src/utils/format.ts b/telegram_bot/src/utils/format.ts index 4429f2a..ff993bd 100644 --- a/telegram_bot/src/utils/format.ts +++ b/telegram_bot/src/utils/format.ts @@ -125,4 +125,20 @@ export function truncate(str: string, maxLength: number): string { 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}`; +} +