Dapp

Приложение в репозитории — cairn-app/: фронтенд на Next.js 16 (App Router) с Tailwind v4, а также wagmi и viem для кошелька, локально работает на порту 3100. Оно заменило прежний dapp на React + Vite 2026-10-02. Оно носит новое название проекта, Cairn.fun, под которым токен протокола называется $CAIRN; эта документация сохраняет название StockFun.

Приложение работает на демо-данных с пометкой «Illustrative», пока не настроен деплой, а ничего не задеплоено. Эта страница описывает его таким, какое оно есть сейчас.

Экраны

Роут Экран
/ Лендинг: следующий дроп, лента рынков, публичная история дропов, калькулятор комиссий, корзины, токен протокола, FAQ
/markets Обзор: пульс казн, полоса токена протокола, фильтры, сортировка и поиск, таблица рынков
/market/[slug] Страница рынка: график, панель дропа, сделки, торговая панель с разбивкой комиссии до подписи, прогресс диапазона 1, контракты, позиция кошелька
/launch Форма запуска: айдентика, выбор корзины, живой предпросмотр, чек-лист необратимости. С восьмого цикла аудита она предлагает только корзины, прочитанные из блокчейна, и перед отправкой снова сверяет выбранную с блокчейном
/claim Клейм аирдропа для холдеров, окно за окном, и комиссии создателя
/drops Все дропы, окно за окном
/docs Механизм, комиссии, корзины, дропы, токен протокола, ограничения

Treasury Ratio, устаревший после решения об аирдропе, который прежний dapp ещё показывал 2026-09-28, в приложении в репозитории не появляется. Метрика, которая его заменит, ещё не выбрана.

Экран клейма

С 2026-10-05 /claim показывает каждое окно, которое подключённый кошелёк может заклеймить, рынок за рынком: конец окна, его акции, суммы и их стоимость в долларах. Суммы берутся только из контракта аирдропа (claimable) и перечитываются каждые 60 секунд, пока страница открыта, и после каждой транзакции кошелька. Акция, которой контракту аирдропа не хватает после аварийного перемещения, помечается «awaiting settlement» и не включается в клейм.

Под списком идут итог и «Claim all», которая отправляет claimMany батчами по пять окон (по десять — пока апгрейд Ethereum Glamsterdam не дошёл до Sepolia 2026-10-06, см. ниже), а для окна с акцией, ожидающей урегулирования, — claim с указанием остальных акций. Каждый вызов сначала симулируется: батч, который не прошёл бы, разбивается на свои окна, а окно — на свои остальные акции, так что окно или акция, которые сейчас нельзя выплатить, пропускаются и называются, никогда не топя остальное. Транзакция, квитанцию которой не удаётся прочитать, сохраняет свой хеш и отображается как отправленная, а её подтверждение пока не прочитано; ничего не отправляется повторно, и страница следит за ней, пока не будет смайнена она сама или транзакция на её месте: с десятого цикла аудита ускорение в кошельке засчитывается как сам клейм, и «View tx» указывает на него, а отмена или любая другая замена завершает его как невыполненный, с «View tx» на то, что было смайнено (см. ниже). Результат сообщает, что было выплачено, какие акции перенесены — они остаются к выплате, — и какие окна пропущены, с причиной.

С 2026-10-06:

  • Прогон останавливает только отказ кошелька, а с десятого цикла аудита — также отмена или замена в кошельке. Транзакция, которая не прошла, учитывается («did not go through»), то, что она покрывала, остаётся в списке, а остальные транзакции уходят
  • Запас для акции, которая ещё в пути. claimMany выплачивает все акции, которые окно перечисляет на момент его исполнения, а окну, ещё открытому для доставок, между оценкой и включением в блок может быть зачислена ещё одна акция: приложение добавляет 400 000 газа на каждую акцию корзины, которую окно ещё не перечисляет (100 000 до апгрейда Glamsterdam). Неиспользованный газ не списывается
  • Газ с расчётом на Glamsterdam. Sepolia активировала апгрейд Ethereum Glamsterdam 2026-10-06, из-за которого новый слот хранилища стоит примерно в пять раз больше газа, чем раньше; у мейннета даты ещё не было. Каждая акция, которую выплачивает клейм, может записать три новых слота, поэтому приложение оставляет 400 000 газа на каждую акцию, которая ещё в пути, и клеймит пять окон за транзакцию, тогда как десять окон по пять акций могли бы занять до примерно 14 млн газа
  • Собственный газ каждой транзакции. При этом апгрейде слот хранилища, записываемый с нуля, стоит около 110 000 газа, а сделка может встретить записи в хранилище, которых её оценка не видела: начисления комиссий на хуке, обнулённые клеймом непосредственно перед ней (два из этих клеймов может вызвать кто угодно), отметку саплая следующего часа, собственную запись владения трейдера, изменённую в том же блоке. С девятого цикла аудита приложение даёт каждой транзакции её оценку плюс 50 000 газа, а сделке, оценённой на последнем блоке, — ещё и газ каждой такой записи, которая всё ещё может произойти, прочитанной на том же блоке (112 000 на начисление, 150 000 на отметку, 135 000 на запись владения; все сразу, когда чтение не проходит). Транзакция, оценка которой не проходит, уходит с фиксированным лимитом, рассчитанным по её самому тяжёлому случаю, и никогда — с собственной оценкой кошелька, у которой нет запаса; запуск рынка тогда не отправляется, и страница предлагает попробовать снова. До того каждая транзакция получала 150 000 газа сверх своей оценки (35 000 до апгрейда). Кошелёк резервирует лимит, умноженный на свою комиссию, до подписи; списывается только использованный газ
  • Акция, не выплаченная при последнем клейме. Акцию, выплату которой клейм перенёс, потому что её токен отклонил перевод на этот кошелёк (например, при заморозке эмитентом), браузер запоминает; она показывается как «not paid at your last claim» и не входит в «Claim all». Каждый клейм сначала пробует её отдельно и снова включает её, как только её клейм прошёл бы; когда больше ничего не причитается, кнопка гласит «Try the assets not paid again»
  • Не проверено — не значит отклонено. Симуляция учитывается, только когда нода сообщает, почему вызов не прошёл бы. Окно, при проверке которого возникла ошибка RPC, остаётся в списке как ещё не проверенное, а когда не ответила ни одна симуляция, ничего не отправляется
  • Непрочитанное — не ноль. Рынок, цифры которого не удалось прочитать, называется и никогда не показывается так, будто он ничего не должен: «Nothing to claim» появляется, только когда прочитаны все рынки, а раздел создателя сообщает, что его рынки не удалось прочитать, вместо «You haven't launched a market»

Комиссии создателя клеймятся на той же странице, рынок за рынком.

Цифры недоступны

С 2026-10-05 казна, чей волт не отвечает на собственные view-функции, например после неудачного апгрейда, показывает «Figures unavailable» вместо своих цифр: в строке и карточке списка рынков, на баннере токена протокола и на странице рынка. Её акции и её ETH по-прежнему перечислены, а итог на странице рынков сообщает, сколько казн он не учёл. Приложение узнаёт об этом от Lens, который читает каждый волт отдельным вызовом: см. Архитектура.

С 2026-10-06 ETH, который хук и лок ликвидности удерживают для волта после выплаты, которую волт не принял, входит в его казну, в его стоимость и в итоги: строка «ETH owed» с пометкой, что он ещё не в волте и перейдёт в него, как только волт его примет. Он читается на хуке и локе, поэтому остаётся актуальным, даже пока волт не отвечает. Волт, который не отвечает и ни разу не был прочитан, получает канал, который предполагает настройка моста протокола, и панель дропа сообщает об этом: где лежат его акции, выводится косвенно, и его активы могут быть показаны не полностью. Рынок токена протокола остаётся на экране в последнем прочитанном виде, помеченный как устаревший, когда Lens не может прочитать его казну, вместо того чтобы отображаться как не запущенный.

Что показывает страница рынка

С 2026-10-06:

  • У восстановленного пула нет графика. Как только режим завершения лока вывел ликвидность пула, любой может бесплатно сдвинуть его цену. Страница и раньше не показывала для него ни цены, ни торговли; теперь она не показывает и изменения за 24 часа и графика, объясняет почему, а Worker перестаёт снимать его цену
  • Доля сделки для акций — это то, что собственные строки налога этой сделки отправили в казну, по настройкам, которые она оплатила, а не по сегодняшним; «—», когда она неизвестна
  • Рыночная капитализация считает предложение в обращении — общее предложение за вычетом того, что держит адрес сжигания, — и в шапке, и на графике; с седьмого цикла аудита она показывает «—», а не $0, когда предложение не удалось прочитать
  • Баланс, который не удалось прочитать, никогда не равен нулю: панель позиции сообщает, что его не удалось прочитать, а панель торговли показывает последний прочитанный баланс с пометкой «last read», не отклоняя продажу сверх него; реальный перерасход отклоняет симуляция транзакции
  • Рынок токена протокола, оставленный в последнем прочитанном виде, сообщает об этом. Пока Lens не может прочитать его казну, его цена и рыночная капитализация помечаются «(last read)» везде, где отображаются, без изменения за 24 часа, а его график показывает «Last read» вместо «Now». Worker не записывает для него цену, пока Lens снова не ответит, поэтому сбой не добавляет на график ни одной точки. Его текущий налог неизвестен: страница не показывает метку антиснайпа, а панель торговли показывает действующий обычный налог без строки излишка. Собственные цифры его токена по-прежнему читаются на токене, поэтому «Burned by buyback» на лендинге остаётся актуальным; его рыночная капитализация — последняя прочитанная. С седьмого цикла аудита сбой отображается на графике как разрыв, последняя прочитанная цена датирована своим возрастом, а цифры токена, читается ли рынок или оставлен в последнем прочитанном виде, принимают последние прочитанные значения, когда не отвечают, — никогда не нули и не пустое название; контракт протокола, который не работает после неудачного апгрейда, их больше не обнуляет

С седьмого цикла аудита, 2026-10-06:

  • График ставит каждую точку в её время. Каждый отсчёт цены стоит там, где его время приходится на диапазон: 1H, 24H и 7D заканчиваются сейчас, а «All» начинается с первого отсчёта, а не от возраста рынка. Точка под курсором показывает, сколько ей времени («3h 20m ago»), линия прерывается там, где отсчётов нет (сбой, ночь, когда страницу никто не открывал), а в покое график показывает цену рынка, «Now», или «Last read» для рынка, оставленного в последнем прочитанном виде. До того точки распределялись равномерно и датировались по своему положению, поэтому последняя цена, прочитанная перед сбоем, отображалась как текущая
  • Объём за 24 часа показывает «—», пока его нельзя посчитать. Когда чтения логов сделок Worker раз за разом не проходят, его окно сделок перестаёт сдвигаться; объём, по рынку и в итогах, тогда показывает «—» вместо цифры, которая уменьшалась бы, как если бы торговля остановилась, и возвращается с первым чтением, которое наверстает отставание

С восьмого цикла аудита, 2026-10-06:

  • Кошелёк из вайтлиста видит обычный налог. Во время окна антиснайпа рынка подключённый кошелёк из вайтлиста этого рынка платит обычный налог через роутер StockFun, через который торгует панель торговли: теперь панель не показывает для него излишка антиснайпа и объясняет почему в примечании. Котировка и так была верной; строка излишка ей противоречила. Пока вайтлист или кошелёк неизвестны, панель показывает налог без учёта освобождений
  • Почему у акции нет цены. На Robinhood Chain оракул придерживает цену акции во время корпоративного действия, а цену каждой акции — пока секвенсор не работает или только что восстановился; эта проверка выключена, пока Chainlink не опубликует фид аптайма для этой сети (см. Канал Robinhood). Там, где стоимость акции отсутствует по этой причине, приложение так и говорит: «No price for NVDA right now: corporate action in progress», либо что секвенсор Robinhood Chain не работает, восстанавливается или его статус неизвестен, — под таблицей корзины в панели дропа и под карточкой дропа на лендинге, а также рядом с акцией на странице клейма. Карточка лендинга показывала такую акцию по $0.00; теперь она показывает «—»
  • Объём за 24 часа учитывает каждый блок один раз. Чтение Worker, на которое ответила нода, отстающая на несколько блоков от предыдущей, больше не заставляет окно сделок считать одни и те же блоки дважды. Для этого одного чтения самые новые сделки могут отсутствовать в ленте; следующее чтение их восстанавливает

С девятого цикла аудита, 2026-10-06:

  • Сумма казны, которая является оценкой, сообщает об этом. Когда часть казны нельзя прочитать или оценить (акция, цену которой придерживает оракул, устаревший фид ETH/USD, зеркальный волт, оставленный в последнем прочитанном виде), её цифра учитывает то, что удалось оценить, и теперь несёт «+» везде, где отображается: в строках и карточках списка рынков, на карточке токена протокола, в пульсе казн, на лендинге, в панели дропа и в доле холдера. Список рынков сообщает, что такая сумма сортируется по тому, что удалось оценить, а пульс — сколько казн он учёл таким образом. До того только панель дропа говорила «an estimate»
  • Одобрение, которое не удалось прочитать, — не ноль. Продаже через роутер StockFun сначала нужно одобрение (allowance) для роутера. Неудачное чтение одобрения раньше считалось его отсутствием: холдера, который уже дал одобрение, просили дать его снова, с двумя подписями, а при втором сбое отправлялось бесполезное одобрение. Теперь панель торговли сообщает, что одобрение не удалось прочитать, ничего не котирует и не отправляет ни одобрения, ни продажи, пока не прочитает его. С десятого цикла аудита продажа сразу после одобрения читает одобрение, котируется и проверяется на блоке не раньше блока одобрения (см. ниже), поэтому нода, отстающая на блок, больше не может ответить нулём

С десятого цикла аудита, 2026-10-06:

  • Транзакция, которую кошелёк отменил, никогда не показывается как выполненная. Кошелёк может отменить ожидающую транзакцию (перевод нулевой суммы самому себе с тем же nonce) или ускорить её (тот же вызов с более высокой комиссией). Теперь приложение читает, что было смайнено на её месте: ускорение засчитывается как само действие, и «View tx» указывает на него; отмена или любая другая замена завершает процесс как невыполненный, с «Cancelled in your wallet.» или «Replaced by another transaction in your wallet.» и «View tx» на то, что было смайнено, и останавливает прогон клеймов так же, как отказ кошелька. До того отменённая сделка показывала «Done», отменённое одобрение считалось данным, отменённый клейм создателя показывал «Claimed» и скрывал ETH на весь сеанс, а отменённый запуск показывал «$TICKER is live.» с выдуманным адресом токена из демо-данных; транзакция, ускоренная после обычных трёх минут ожидания, так и не подтверждалась, и панель оставалась заблокированной до перезагрузки страницы
  • Отслеживание после ожидания. Когда обычное ожидание прекращается, через три минуты, приложение отслеживает транзакцию по её nonce: когда счётчик отправленных транзакций кошелька ушёл дальше этого nonce, а у транзакции нет квитанции, оно ищет, что было смайнено с этим nonce, никогда — в блоке, предшествующем отправке, и применяет то же правило. Отмена или замена всегда считывается только с транзакции, действительно найденной в блоке, и никогда — из отсутствующей квитанции
  • Запуск считается состоявшимся, только когда так говорит блокчейн. Запуск читает создание рынка из собственного события фабрики; без него он возвращается к форме со ссылкой на транзакцию, и адрес из демо-данных никогда не показывается вне демо-режима
  • Блок вашей последней транзакции, в течение минуты. В течение 60 секунд после того, как одна из его собственных транзакций смайнена, приложение проверяет, оценивает и котирует то, что отправляет следом в этой сети, на блоке не раньше этого и там же читает одобрение для продажи. Эндпоинт с балансировкой нагрузки может ответить с ноды, отстающей на блок: продажа сразу после её одобрения раньше там отклонялась («Approve it first»), а вторая попытка могла отправить второе одобрение. Нода, которая не дошла до этого блока, теперь считается отстающей, и её опрашивают снова; если ни одна не доходит до него за несколько секунд, ничего не отправляется. Для продажи сразу после её одобрения страница тогда сообщает «Your approval went through, but no quote could be read from the pool, so the sale was not sent…» (её котировка — первое чтение, которое там выполняется), а для транзакции, собственная проверка которой не может дойти до этого блока (одобрение, клейм, запуск), — «This could not be checked against the block of your last transaction just now, so nothing was sent. Try again in a moment.»
  • Что остаётся. Ложное «Replaced» остаётся возможным в одном узком случае, когда выполняется всё это сразу: ни одна нода не показала продажу, поэтому приложение угадало её nonce по счётчику кошелька; нода, отстающая от остальных, но не от блока, прочитанного перед отправкой, ответила этим счётчиком; другая транзакция того же кошелька, которую эта нода не видела, заняла угаданный nonce; а продажа, отправленная через приватный релей, всё ещё не была смайнена после трёх минут и льготного периода примерно в 36 секунд. Отмена никогда не показывается как выполненная, и ничего не остаётся заблокированным

Запуск

Форма запуска предлагает только корзины, прочитанные из реестра в блокчейне, под собственной нумерацией реестра: до первого чтения она не предлагает ни одной («Reading the baskets from the chain…»), а до деплоя контрактов сообщает, что запуск откроется, как только они заработают. Непосредственно перед отправкой она снова читает из фабрики и комиссию за создание, и выбранную корзину — её название и её акции с их весами — и отказывается отправлять, ничего не отправляя, если что-либо из этого отличается от того, что она показывает; сообщение называет корзину, которую блокчейн хранит под этим номером. До восьмого цикла аудита форма до своего первого чтения предлагала сконфигурированные корзины с их сконфигурированными номерами, и в деплое, реестр которого нумерует свои корзины иначе, можно было запустить рынок на другой корзине, чем показанная. Форма платит только комиссию за создание: в ней нет собственной покупки создателя, и она не указывает вайтлист антиснайпа, о чём сообщает (фабрика принимает и то, и другое при прямом вызове: см. Запуск рынка). Подтверждение называет корзину, проверенную при отправке.

Арт-дирекшн

Правила дизайна приложения описаны в его собственном DESIGN.md: тёплая светлая тема на белом фоне, терракотовый для дропов и ключевых цифр, Poppins для интерфейса и Instrument Serif для крупных финансовых цифр, табличные цифры в таблицах.

Откуда что берётся

Комиссии, параметры запуска, саплай и состав корзин никогда не прописываются в компоненте: они хранятся в конфигурации приложения или берутся из блокчейна, а строки комиссии при запуске сверяются с константами, сгенерированными из @stockfun/shared, который зеркалит контракты. Процент, вписанный вручную в страницу, — это путь, которым продукт в итоге начинает рекламировать схему комиссий, которую контракты не реализуют. С 2026-10-05 эти числа — настройки владельца: общий пакет хранит их значения по умолчанию, а текущее значение читается из контракта, который его хранит.

Состояние протокола берётся из блокчейна; индексатора нет. Worker, cairn-worker/, читает его один раз для всех открытых страниц: снимок, затем WebSocket, который передаёт только изменения. Когда Worker не работает, страница сама читает блокчейн через публичный RPC. Собственные данные кошелька, от его балансов до того, что он может заклеймить, читает браузер. С седьмого цикла аудита Worker читает только эндпоинты, которые сообщают, что обслуживают его сеть: эндпоинт в другой сети считается недоступным, поэтому резервный эндпоинт не в той сети никогда не заставит исчезнуть все рынки. С восьмого он публикует восьмую версию своей схемы данных, которая добавляет причину, по которой отсутствует цена акции; приложение, читающее более старую схему, по-прежнему работает, без этой причины. С девятого цикла аудита наблюдатель Worker сохраняет то, что он узнал о своих RPC (какой эндпоинт не работает и с какого момента, сколько выжидать, какую сеть обслуживает каждый), даже когда его объект Cloudflare засыпает между двумя чтениями, никогда не сохраняя адрес эндпоинта: сбой основного RPC стоит одну пробу за каждую удваивающуюся паузу вместо трёх запросов при каждом чтении, а исправное чтение ничего не запрашивает дважды. Он также записывает номер блока самой Robinhood Chain, тогда как раньше записывал номер блока сети, в которой Robinhood Chain проводит расчёты.

Аудит текстов

pnpm audit:copy сканирует исходники бэкенда, кипера, shared и контрактов. Он завершается с ошибкой при единственном запрещённом слове или статически проверяемом визуальном запрете. Он запускается по требованию и не входит в pnpm build. С тех пор как прежний dapp ушёл из репозитория, он не сканирует ни одного фронтенда: в приложении запрещённая лексика — правило ревью, записанное в его PRODUCT.md.

Это не линтер стиля: запрещённая лексика — юридическое ограничение.