// Shared whale presentation helpers — used by Leaders and My Copies so a whale
// looks and reads the same everywhere (distinct identicon + trading-style chip).
/** Deterministic avatar hue from an address, so each whale is a distinct colour. */
export function addrHue(addr: string): number {
let h = 0;
for (let i = 2; i < addr.length; i++) h = (h * 31 + addr.charCodeAt(i)) % 360;
return h;
}
/** Two-char initials from the address for the identicon avatar. */
export function addrInitials(addr: string): string {
return addr.slice(2, 4).toUpperCase();
}
export type WhaleStyle = { icon: string; key: string; color: string };
/**
* Trading style from avg buy price + sell rate, readable at a glance:
* 🎯 directional (avgBuy 0.35–0.80, the edge we copy)
* ⭐ favorite-farmer (avgBuy ≥ 0.80 — high win% but low edge; good for volume)
* ⚡ active manager (sells ≥ 25% — actively cuts/locks)
*/
export function whaleStyle(avgBuy: number | null | undefined, sellRate: number | null | undefined): WhaleStyle | null {
if (avgBuy != null && avgBuy >= 0.80) return { icon: '⭐', key: 'leaders.styleFav', color: '#eab308' };
if (sellRate != null && sellRate >= 0.25) return { icon: '⚡', key: 'leaders.styleActive', color: '#38bdf8' };
if (avgBuy != null && avgBuy >= 0.35) return { icon: '🎯', key: 'leaders.styleDir', color: '#22c55e' };
return null;
}
/** Emoji for what the whale mainly trades — the avatar glyph (style-based, anonymous). */
export function categoryGlyph(cat?: string | null): string {
const c = (cat || '').toLowerCase();
if (c.includes('sport')) return '⚽';
if (c.includes('politic') || c.includes('election')) return '🗳️';
if (c.includes('crypto')) return '₿';
if (c.includes('commodit')) return '🛢️';
if (c.includes('econ') || c.includes('fed') || c.includes('rate')) return '📈';
if (c.includes('tech') || c.includes('ai')) return '🤖';
if (c.includes('entertain') || c.includes('culture') || c.includes('movie')) return '🎬';
if (c.includes('weather') || c.includes('climate')) return '🌡️';
return '🐋';
}
export type WhaleHorizon = { icon: string; key: string };
/**
* Trading horizon proxied by cadence (trades per active day) — readable as how
* fast the whale churns: ⏱️ short-term (day-trader), 📆 mid-term, 🐢 long-term
* (rarely trades, holds positions). Computed from existing fields, no backend.
*/
export function whaleHorizon(totalTrades: number, firstSeen?: string | null, lastSeen?: string | null): WhaleHorizon | null {
if (!totalTrades || !firstSeen || !lastSeen) return null;
const d0 = new Date(firstSeen).getTime();
const d1 = new Date(lastSeen).getTime();
if (!isFinite(d0) || !isFinite(d1)) return null;
const days = Math.max(1, (d1 - d0) / 86400000);
const perDay = totalTrades / days;
if (perDay >= 4) return { icon: '⏱️', key: 'leaders.horShort' };
if (perDay >= 1) return { icon: '📆', key: 'leaders.horMid' };
return { icon: '🐢', key: 'leaders.horLong' };
}
/** Anonymous codename for a whale — never expose the raw 0x address to users. */
/**
* Hand-picked nicknames for the copied roster, tied to each whale's style/size.
* Anonymous (no address/identity leak) — just a friendlier handle than "Whale #N".
* Keyed by lowercase address; localized RU/EN; takes priority over any raw label.
*/
export const WHALE_NICKNAMES: Record<string, { ru: string; en: string }> = {
'0xf8831548531d56ad6a4331493243c447a827cd1f': { ru: 'Жадный Левиафан', en: 'Hungry Leviathan' }, // +$3.46M король стола
'0x84cfffc3f16dcc353094de30d4a45226eccd2f63': { ru: 'Стальной Бульдозер', en: 'Steel Bulldozer' }, // заходит по $1M в одну позу
'0x492442eab586f242b53bda933fd5de859c8a3782': { ru: 'Хладнокровный Снайпер', en: 'Cold-Blooded Sniper' }, // 0% фаворитов, направленный
'0x224a89dbe0db0d6124b335edabd15b3f877da3d5': { ru: 'Быстрая Ракета', en: 'Swift Rocket' }, // свежий, острый
'0x6db568e61e5e3de7d87f831431b673f38ce2e279': { ru: 'Точный Метроном', en: 'Steady Metronome' }, // ровная машина
'0xf39651f0addaad0221806d828197064b97feed0d': { ru: 'Мудрый Оракул', en: 'Wise Oracle' }, // политика + фьючерсы
'0x1eaf5d5f822dc5211c25b5839e5b7aa70f319bf0': { ru: 'Вездесущий Спрут', en: 'Omnipresent Octopus' }, // 90 поз, щупальца всюду
'0xc44f432b014f36679e13615f95996eac32bbd49f': { ru: 'Цифровой Хамелеон', en: 'Digital Chameleon' }, // esports / нишевые рынки
'0xc1a9273b15a17d21af09dddb42b9239b93d4e2db': { ru: 'Жадный Чалкоед', en: 'Greedy Chalk-Eater' }, // фаворит-фармер
'0x0346afae2603313d2bbee96b628536c8cbe352a5': { ru: 'Фартовый Лонгшот', en: 'Lucky Longshot' }, // вернули из паузы — +$23 на 12x
};
// Generated-codename word banks: «Adjective + Beast». ~50×50 ≈ 2500 combos →
// varied, fun, anonymous handles for the whole roster (no address/identity leak,
// not discoverable on Polymarket — keeps our moat). Index picked deterministically
// from the address, so a whale always reads the same.
const CODENAME_ADJ = {
ru: ['Жадный', 'Стальной', 'Хладнокровный', 'Быстрый', 'Фартовый', 'Мудрый', 'Дерзкий', 'Тихий', 'Багровый', 'Полночный', 'Грозовой', 'Ледяной', 'Огненный', 'Золотой', 'Бешеный', 'Хитрый', 'Свирепый', 'Неоновый', 'Призрачный', 'Гранитный', 'Бронзовый', 'Космический', 'Электрический', 'Алмазный', 'Штормовой', 'Голодный', 'Безжалостный', 'Тёмный', 'Яростный', 'Молниеносный', 'Угрюмый', 'Дикий', 'Матёрый', 'Колючий', 'Жёсткий', 'Цепкий', 'Чёткий', 'Резкий', 'Упрямый', 'Везучий', 'Шальной', 'Лютый', 'Седой', 'Бравый', 'Отчаянный', 'Каменный', 'Туманный', 'Кислотный', 'Кобальтовый', 'Адский'],
en: ['Greedy', 'Steel', 'Cold-Blooded', 'Swift', 'Lucky', 'Wise', 'Brazen', 'Silent', 'Crimson', 'Midnight', 'Thunder', 'Icy', 'Fiery', 'Golden', 'Rabid', 'Cunning', 'Savage', 'Neon', 'Phantom', 'Granite', 'Bronze', 'Cosmic', 'Electric', 'Diamond', 'Storm', 'Hungry', 'Ruthless', 'Dark', 'Furious', 'Lightning', 'Grim', 'Wild', 'Seasoned', 'Spiky', 'Rugged', 'Tenacious', 'Sharp', 'Brisk', 'Stubborn', 'Fortunate', 'Reckless', 'Feral', 'Grizzled', 'Brave', 'Desperate', 'Stone', 'Misty', 'Acid', 'Cobalt', 'Hellish'],
};
const CODENAME_BEAST = {
// RU: masculine-gender beasts only, so the masculine adjectives above agree
// ("Электрический Кобра" would be wrong — feminine nouns dropped).
ru: ['Левиафан', 'Кракен', 'Кашалот', 'Марлин', 'Скат', 'Спрут', 'Нарвал', 'Дельфин', 'Тунец', 'Угорь', 'Краб', 'Осьминог', 'Кит', 'Морж', 'Тюлень', 'Альбатрос', 'Сокол', 'Ястреб', 'Орёл', 'Гриф', 'Ворон', 'Филин', 'Тигр', 'Лев', 'Ягуар', 'Гепард', 'Волк', 'Медведь', 'Барсук', 'Кабан', 'Бизон', 'Носорог', 'Бегемот', 'Питон', 'Варан', 'Аллигатор', 'Дракон', 'Грифон', 'Феникс', 'Голем', 'Титан', 'Минотавр', 'Скорпион', 'Беркут', 'Стервятник', 'Кугуар', 'Буйвол', 'Барс', 'Шершень', 'Богомол'],
en: ['Leviathan', 'Kraken', 'Orca', 'Cachalot', 'Marlin', 'Barracuda', 'Piranha', 'Stingray', 'Octopus', 'Narwhal', 'Dolphin', 'Tuna', 'Eel', 'Crab', 'Squid', 'Shark', 'Whale', 'Walrus', 'Seal', 'Albatross', 'Falcon', 'Hawk', 'Eagle', 'Vulture', 'Raven', 'Owl', 'Tiger', 'Lion', 'Panther', 'Jaguar', 'Cheetah', 'Lynx', 'Wolf', 'Bear', 'Badger', 'Wolverine', 'Boar', 'Bison', 'Rhino', 'Hippo', 'Cobra', 'Python', 'Monitor', 'Alligator', 'Dragon', 'Griffin', 'Phoenix', 'Golem', 'Titan', 'Minotaur'],
};
/** FNV-1a 32-bit hash over the address with a seed, for independent index picks. */
function hashSeed(addr: string, seed: number): number {
let h = seed >>> 0;
for (let i = 2; i < addr.length; i++) h = Math.imul(h ^ addr.charCodeAt(i), 16777619) >>> 0;
return h >>> 0;
}
/**
* Anonymous, fun codename for a whale — never the raw 0x address.
* Hand-picked nickname (WHALE_NICKNAMES) wins; otherwise a deterministic
* «Adjective + Beast» generated from the address in the UI language.
*/
export function whaleCodename(addr: string, lang: 'ru' | 'en' = 'en'): string {
const nick = WHALE_NICKNAMES[addr.toLowerCase()];
if (nick) return nick[lang] ?? nick.en;
const adj = CODENAME_ADJ[lang];
const beast = CODENAME_BEAST[lang];
const a = adj[hashSeed(addr, 2166136261) % adj.length];
const b = beast[hashSeed(addr, 0x9e3779b1) % beast.length];
return `${a} ${b}`;
}
/**
* Codenames for a LIST of whales, guaranteed distinct within that list. The ~2500-combo
* generator collides by birthday paradox across the 4000+ discovered roster, so two whales
* shown side-by-side can read identically ("Чёткий Дракон" ×2). Here we detect any collision
* and re-roll the clashing ones to the next free «Adjective+Beast» combo (walking the combo
* space deterministically from each whale's natural name) — so duplicates become genuinely
* distinct, clean handles ("Чёткий Дракон" / "Чёткий Скорпион") with no address leak.
*
* Stable per address set: addresses are processed in sorted order, so the same roster always
* yields the same assignment, and the lexicographically-smaller address keeps the base name.
* Hand-picked WHALE_NICKNAMES are fixed identities — never re-rolled, only reserved.
* Returns a Map keyed by lowercase address.
*/
export function whaleCodenamesUnique(addrs: string[], lang: 'ru' | 'en' = 'en'): Map<string, string> {
const adj = CODENAME_ADJ[lang];
const beast = CODENAME_BEAST[lang];
const out = new Map<string, string>();
const used = new Set<string>();
const uniqSorted = [...new Set(addrs.map(a => a.toLowerCase()))].sort();
for (const a of uniqSorted) {
const nick = WHALE_NICKNAMES[a];
if (nick) {
const name = nick[lang] ?? nick.en;
out.set(a, name);
used.add(name);
continue;
}
// Natural combo for this address, then walk the full space (beast inner, adjective
// outer) until a free name is found. 2500 combos >> any shown roster, so always resolves.
const a0 = hashSeed(a, 2166136261) % adj.length;
const b0 = hashSeed(a, 0x9e3779b1) % beast.length;
let name = `${adj[a0]} ${beast[b0]}`;
for (let k = 1; used.has(name) && k < adj.length * beast.length; k++) {
const bi = (b0 + k) % beast.length;
const ai = (a0 + Math.floor((b0 + k) / beast.length)) % adj.length;
name = `${adj[ai]} ${beast[bi]}`;
}
out.set(a, name);
used.add(name);
}
return out;
}
📜 Git History
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...