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:
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
</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 className="text-gray-400 mb-1">{STRINGS.pastWins.winner}</div>
|
||||
<div className="text-white font-semibold">
|
||||
{win.winner_name || 'Anon'}
|
||||
</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 className="text-gray-400 mb-1">{STRINGS.pastWins.ticket}</div>
|
||||
<div className="text-white">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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) {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user