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;
|
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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user