← Back
// 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...