O dapp
O app do repositório é o cairn-app/: um front end em Next.js 16 (App Router) com
Tailwind v4, e wagmi e viem para a carteira, servido na porta 3100 localmente. Ele
substituiu o dapp anterior em React + Vite em 2026-10-02. Ele traz o novo nome do projeto,
Cairn.fun, sob o qual o token do protocolo é $CAIRN; este livro mantém o nome StockFun.
O app roda com dados de demonstração, marcados "Illustrative", até que uma implantação seja configurada, e nada está implantado. Esta página o descreve tal como está.
As telas
| Rota | Tela |
|---|---|
/ |
Landing: o próximo drop, a fita de cotações dos mercados, o registro público dos drops, a calculadora de taxas, os baskets, o token do protocolo, o FAQ |
/markets |
Descoberta: pulso da tesouraria, a barra do token do protocolo, filtros, ordenação e busca, a tabela de mercados |
/market/[slug] |
Detalhe do mercado: gráfico, painel do drop, trades, painel de trade com a divisão das taxas antes de assinar, progresso da faixa 1, contratos, a posição da carteira |
/launch |
Formulário de lançamento: identidade, seletor de basket, prévia ao vivo, checklist de irreversibilidade. Desde o oitavo loop de auditoria, ele só oferece os baskets lidos da chain, e verifica de novo o escolhido contra a chain antes do envio |
/claim |
A reivindicação do airdrop pelos holders, janela por janela, e as taxas do criador |
/drops |
Todos os drops, janela por janela |
/docs |
Mecanismo, taxas, baskets, drops, o token do protocolo, limites |
O Treasury Ratio, obsoleto desde a decisão do airdrop, que o dapp anterior ainda exibia em 2026-09-28, não aparece no app do repositório. A métrica que o substitui ainda está por decidir.
A tela de reivindicação
Desde 2026-10-05, /claim lista todas as janelas que a carteira conectada pode
reivindicar, mercado por mercado: o fim da janela, as suas ações, os valores e o seu valor
em dólares. Os valores vêm só do contrato do airdrop (claimable), lidos de novo a cada
60 segundos enquanto a página está aberta e depois de cada transação da carteira. Uma ação
que falta ao contrato do airdrop depois de uma transferência de emergência é marcada
"awaiting settlement" e fica de fora da reivindicação.
Abaixo da lista vêm um total e "Claim all", que envia claimMany em lotes de cinco
janelas (dez até a atualização Glamsterdam do Ethereum chegar à Sepolia em 2026-10-06,
abaixo), e claim com as outras ações para uma janela que tem uma ação aguardando
acerto. Toda
chamada é simulada primeiro: um lote que falharia é dividido nas suas janelas, e uma
janela nas suas outras ações, de modo que uma janela ou uma ação que não pode ser paga
agora é pulada e nomeada, sem nunca afundar o resto. Uma transação cujo recibo não pode
ser lido mantém o seu hash e aparece como enviada, com a sua confirmação ainda não lida;
nada é enviado de novo, e a página a acompanha até que ela, ou uma transação no seu lugar,
seja minerada: desde o décimo loop de auditoria, uma aceleração na carteira é tomada pela
própria reivindicação, com "View tx" apontando para ela, enquanto um cancelamento ou
qualquer outra substituição a encerra como não feita, com "View tx" no que foi minerado
(abaixo). O resultado diz o que foi pago, as ações adiadas, que continuam devidas, e as
janelas puladas, com o seu motivo.
Desde 2026-10-06:
- Só a recusa da carteira interrompe uma execução, e desde o décimo loop de auditoria também um cancelamento ou uma substituição na carteira. Uma transação que falha é contada ("did not go through"), o que ela cobria continua listado, e as outras transações seguem
- Espaço para uma ação ainda a caminho. Um
claimManypaga todas as ações que uma janela lista no momento em que roda, e uma janela ainda aberta a entregas pode ser creditada com mais uma ação entre a estimativa e a inclusão: o app acrescenta 400.000 de gas por ação do basket que a janela ainda não lista (100.000 até a atualização Glamsterdam). O gas não usado não é cobrado - Gas dimensionado para a Glamsterdam. A Sepolia ativou a atualização Glamsterdam do Ethereum em 2026-10-06, que faz um novo slot de storage custar cerca de cinco vezes o seu gas anterior; a mainnet ainda não tinha data. Cada ação que uma reivindicação paga pode gravar três novos slots, então o app deixa 400.000 de gas por ação ainda a caminho e reivindica cinco janelas por transação, onde dez janelas de cinco ações poderiam levar até cerca de 14 milhões de gas
- O gas próprio de cada escrita. Com a atualização, um slot de storage gravado a partir de zero custa cerca de 110.000 de gas, e um trade pode encontrar escritas que a sua estimativa nunca viu: os acúmulos de taxas do hook esvaziados por uma reivindicação logo antes (duas dessas reivindicações podem ser chamadas por qualquer pessoa), a marca de supply da hora seguinte, o próprio registro do trader movido no mesmo bloco. Desde o nono loop de auditoria, o app dá a cada escrita a sua estimativa mais 50.000 de gas e, a um trade, estimado no bloco mais recente, também o gas de cada uma dessas escritas que ainda pode acontecer, lido nesse mesmo bloco (112.000 por acúmulo, 150.000 pela marca, 135.000 pelo registro; todos eles quando uma leitura falha). Uma escrita cuja estimativa falha sai com um limite fixo dimensionado a partir do seu caso mais pesado, nunca com a própria estimativa da carteira, que não tem margem; um lançamento então não é enviado, e a página diz para tentar de novo. Até então, toda escrita recebia 150.000 de gas acima da sua estimativa (35.000 antes da atualização). Uma carteira reserva o limite vezes a sua taxa antes de assinar; só o gas usado é cobrado
- Uma ação não paga na última reivindicação. Uma ação que uma reivindicação adiou porque o seu token recusou a transferência para esta carteira (um congelamento do emissor, por exemplo) fica guardada no navegador, aparece como "not paid at your last claim" e fica de fora de "Claim all". Cada reivindicação a tenta primeiro sozinha e a inclui de novo assim que a sua reivindicação passaria; quando nada mais é devido, o botão mostra "Try the assets not paid again"
- Não verificada, nunca recusada. Uma simulação só conta quando o nó diz por que a chamada falharia. Uma janela cuja verificação encontrou um erro de RPC continua listada como ainda não verificada, e quando nenhuma simulação respondeu, nada é enviado
- Não lido não é vazio. Um mercado cujos números não puderam ser lidos é nomeado, nunca mostrado como não devendo nada: "Nothing to claim" só aparece quando todos os mercados foram lidos, e a seção do criador diz que os seus mercados não puderam ser lidos em vez de "You haven't launched a market"
As taxas do criador são reivindicadas na mesma página, mercado por mercado.
Números indisponíveis
Desde 2026-10-05, uma tesouraria cujo vault não responde às suas próprias funções de leitura, depois de um upgrade defeituoso, por exemplo, mostra "Figures unavailable" em vez dos seus números: na linha e no cartão da lista de mercados, no banner do token do protocolo e na página do mercado. As suas ações e o seu ETH continuam listados, e o total na página de mercados diz quantas tesourarias ele deixou de fora. O app fica sabendo disso pela Lens, que lê cada vault na sua própria chamada: veja Arquitetura.
Desde 2026-10-06, o ETH que o hook e o lock de liquidez guardam para um vault, depois de um pagamento que o vault recusou, faz parte da sua tesouraria, do seu valor e dos totais: uma linha "ETH owed", com uma nota de que ele ainda não está no vault e entra nele assim que o vault o aceita. Ele é lido no hook e no lock, então continua atualizado mesmo enquanto o vault não responde. Um vault que não responde e nunca foi lido assume o trilho que a configuração da bridge do protocolo implica, e o painel do drop o diz: onde estão as suas ações é deduzido, e os seus ativos podem estar incompletos. O mercado do token do protocolo continua na tela como foi lido pela última vez, marcado como desatualizado, quando a Lens não consegue ler a sua tesouraria, em vez de aparecer como não lançado.
O que a página do mercado mostra
Desde 2026-10-06:
- Um pool recuperado não tem gráfico. Depois que o modo de encerramento do lock retirou a liquidez de um pool, qualquer pessoa pode mover o seu preço de graça. A página já não mostrava preço nem trading para ele; agora também não mostra variação em 24 horas nem gráfico, e diz por quê, e o Worker para de amostrar o seu preço
- A parte de um trade para as ações é o que as próprias linhas de taxa desse trade enviaram à tesouraria, nos parâmetros que ele pagou, nunca nos de hoje; "—" quando não é conhecida
- O market cap conta o supply em circulação, o total menos o que o endereço de burn detém, no cabeçalho e no gráfico igualmente; desde o sétimo loop de auditoria, ele mostra "—", nunca $0, quando o supply não pôde ser lido
- Um saldo que não pôde ser lido nunca é zero: o painel de posição diz que ele não pôde ser lido, e o painel de trade mostra o último saldo lido, marcado "last read", sem recusar uma venda acima dele; a simulação da transação recusa uma venda que realmente exceda o saldo
- O mercado do token do protocolo mantido como lido pela última vez o diz. Enquanto a Lens não consegue ler a sua tesouraria, o seu preço e o seu market cap são marcados "(last read)" onde quer que apareçam, sem variação em 24 horas, e o seu gráfico mostra "Last read" em vez de "Now". O Worker não registra nenhum preço para ele até que a Lens volte a responder, então a interrupção não acrescenta nenhum ponto ao gráfico. A sua taxa neste momento é desconhecida: a página não mostra nenhuma etiqueta de anti-snipe, e o painel de trade mostra a taxa normal em vigor, sem linha de excedente. Os números próprios do seu token continuam sendo lidos no token, então "Burned by buyback" na landing continua atualizado; o seu market cap é o último lido. Desde o sétimo loop de auditoria, a interrupção aparece no gráfico como uma lacuna, com o último preço lido datado pela sua idade, e os números do token, com o mercado lido ou mantido, assumem os últimos valores lidos quando não respondem, nunca zeros nem um nome vazio; um contrato do protocolo que falha depois de um upgrade quebrado não os apaga mais
Desde o sétimo loop de auditoria, em 2026-10-06:
- O gráfico coloca cada ponto no seu horário. Cada amostra de preço fica onde o seu horário cai no intervalo: 1H, 24H e 7D terminam agora, e "All" começa na primeira amostra, nunca pela idade do mercado. Um ponto sob o cursor diz a sua idade ("3h 20m ago"), a linha se interrompe onde faltam amostras (uma interrupção, uma noite em que ninguém estava com a página aberta), e em repouso o gráfico mostra o preço do mercado, "Now", ou "Last read" para um mercado mantido como lido pela última vez. Até então, os pontos eram distribuídos de forma uniforme e datados pela sua posição, então o último preço lido antes de uma interrupção aparecia como atual
- O volume de 24 horas mostra "—" enquanto não pode ser contado. Quando as leituras dos logs de trades pelo Worker continuam falhando, a sua janela de trades para de avançar; o volume, por mercado e nos totais, mostra então "—" em vez de um número que encolheria como se o trading tivesse parado, e volta com a primeira leitura que se atualiza
Desde o oitavo loop de auditoria, em 2026-10-06:
- Uma carteira da whitelist vê a taxa normal. Durante a janela anti-snipe de um mercado, uma carteira conectada que está na whitelist desse mercado paga a taxa normal pelo router do StockFun, pelo qual o painel de trade negocia: o painel não mostra mais nenhum excedente de anti-snipe para ela, e diz por quê numa nota. A cotação já estava certa; a linha de excedente a contradizia. Enquanto a whitelist ou a carteira são desconhecidas, o painel mostra a taxa sem as isenções
- Por que uma ação não tem preço. Na Robinhood Chain, o oráculo retém o preço de uma ação durante um evento corporativo, e o de todas as ações enquanto o sequenciador está fora do ar ou acabou de voltar, uma verificação desligada até que a Chainlink publique um feed de disponibilidade para a chain (veja O trilho Robinhood). Onde o valor de uma ação falta por esse motivo, o app diz isso: "No price for NVDA right now: corporate action in progress", ou o sequenciador da Robinhood Chain fora do ar, em recuperação ou com o seu estado desconhecido, abaixo da tabela do basket do painel do drop e do cartão do drop da landing, e ao lado da ação na página de reivindicação. O cartão da landing mostrava uma ação assim a $0.00; agora ele mostra "—"
- O volume de 24 horas conta cada bloco uma vez. Uma leitura do Worker respondida por um nó alguns blocos atrás do da leitura anterior não faz mais a janela de trades contar os mesmos blocos duas vezes. Nessa leitura, os trades mais recentes podem faltar no feed; a leitura seguinte os restaura
Desde o nono loop de auditoria, em 2026-10-06:
- Um pote que é uma estimativa diz isso. Quando parte de uma tesouraria não pode ser lida ou precificada (uma ação cujo preço o oráculo retém, um feed ETH/USD parado, um vault espelho mantido como lido pela última vez), o seu número conta o que pôde ser precificado e agora traz um "+" onde quer que apareça: as linhas e os cartões da lista de mercados, o cartão do token do protocolo, o pulso da tesouraria, a landing, o painel do drop e a parte de um holder. A lista de mercados diz que um pote assim é ordenado pelo que pôde ser precificado, e o pulso diz quantas tesourarias contou dessa forma. Até então, só o painel do drop dizia "an estimate"
- Uma aprovação que não pôde ser lida não é zero. Uma venda pelo router do StockFun precisa primeiro da allowance do router. Uma leitura dela que falhava contava como nenhuma: um holder que já tinha aprovado era chamado a aprovar de novo, com duas assinaturas, e uma segunda falha enviava uma aprovação para nada. O painel de trade agora diz que a aprovação não pôde ser lida e não cota nada, e não envia nem uma aprovação nem uma venda até lê-la. Desde o décimo loop de auditoria, a venda logo depois de uma aprovação lê a allowance, é cotada e é verificada num bloco não anterior ao da aprovação (abaixo), então um nó um bloco atrasado não pode mais responder zero
Desde o décimo loop de auditoria, em 2026-10-06:
- Uma transação que a carteira cancela nunca aparece como feita. Uma carteira pode cancelar uma transação pendente (uma transferência de nada para si mesmo no mesmo nonce) ou acelerá-la (a mesma chamada com uma taxa mais alta). O app agora lê o que foi minerado no seu lugar: uma aceleração é tomada pela própria ação, com "View tx" apontando para ela; um cancelamento ou qualquer outra substituição encerra o fluxo como não feito, com "Cancelled in your wallet." ou "Replaced by another transaction in your wallet." e "View tx" no que foi minerado, e interrompe uma execução de reivindicações como faz a recusa da carteira. Até então, um trade cancelado mostrava "Done", uma aprovação cancelada contava como dada, uma reivindicação cancelada das taxas do criador mostrava "Claimed" e escondia o ETH durante a sessão, e um lançamento cancelado mostrava "$TICKER is live." com o endereço de token inventado da demo; uma transação acelerada depois da espera habitual de três minutos nunca era confirmada, e o painel ficava travado até a página ser recarregada
- Acompanhada depois da espera. Quando a espera habitual se esgota, depois de três minutos, o app acompanha a transação pelo seu nonce: quando a contagem de transações enviadas da carteira passou dele e a transação não tem recibo, ele procura o que foi minerado nesse nonce, nunca num bloco anterior ao envio, e aplica a mesma regra. Um cancelamento ou uma substituição só é lido a partir de uma transação realmente encontrada num bloco, nunca a partir de um recibo ausente
- Um lançamento só está no ar quando a chain diz isso. O lançamento lê a criação do mercado no próprio evento da factory; sem ele, volta ao formulário com o link da transação, e o endereço da demo nunca aparece fora da demo
- O bloco da sua última transação, durante um minuto. Durante 60 segundos depois que uma das suas próprias transações é minerada, o app verifica, estima e cota o que envia em seguida nessa chain num bloco não anterior a ela, e lê ali a allowance de uma venda. Um endpoint com balanceamento de carga pode responder de um nó um bloco atrasado: a venda logo depois da sua aprovação era recusada ali ("Approve it first"), e uma segunda tentativa podia enviar uma segunda aprovação. Um nó que não alcançou esse bloco agora conta como atrasado e é consultado de novo; se nenhum o alcança em alguns segundos, nada é enviado. Para uma venda logo depois da sua aprovação, a página diz então "Your approval went through, but no quote could be read from the pool, so the sale was not sent…" (a sua cotação é a primeira leitura feita ali); uma escrita cuja própria verificação não consegue alcançar o bloco (uma aprovação, uma reivindicação, um lançamento) diz "This could not be checked against the block of your last transaction just now, so nothing was sent. Try again in a moment."
- O que resta. Um falso "Replaced" continua possível num caso estreito, quando tudo isto se verifica: nenhum nó mostrou a venda, então o app deduziu o seu nonce da contagem da carteira; um nó atrás dos outros, embora não atrás do bloco lido antes do envio, respondeu a contagem; outra transação da mesma carteira, que esse nó não tinha visto, tomou o nonce deduzido; e a venda, enviada por um relay privado, ainda não estava minerada depois dos três minutos e de um período de carência de cerca de 36 segundos. Um cancelamento nunca aparece como feito, e nada fica travado
O lançamento
O formulário de lançamento só oferece os baskets lidos do registro da chain, com a numeração do próprio registro: antes da primeira leitura, não oferece nenhum ("Reading the baskets from the chain…"), e antes de os contratos serem implantados, diz que o lançamento abre assim que eles estiverem no ar. Logo antes do envio, ele lê de novo na factory tanto a taxa de criação quanto o basket escolhido, o seu nome e as suas ações com os seus pesos, e se recusa a enviar, sem enviar nada, se algum dos dois diferir do que ele mostra; a mensagem nomeia o basket que a chain guarda sob esse número. Até o oitavo loop de auditoria, o formulário oferecia os baskets configurados, com os seus números configurados, antes da sua primeira leitura, e uma implantação cujo registro numera os seus baskets de outra forma poderia ter lançado um mercado em outro basket que não o mostrado. O formulário paga só a taxa de criação: ele não tem compra própria do criador e não indica nenhuma whitelist anti-snipe, o que ele diz (a factory aceita as duas numa chamada direta: veja Lançar um mercado). A confirmação nomeia o basket verificado no envio.
Direção de arte
As regras de design do app ficam no seu próprio DESIGN.md: um tema claro e acolhedor
sobre uma tela branca, terracota para os drops e os números-chave, Poppins para a
interface e Instrument Serif para os grandes valores financeiros, algarismos tabulares nas
tabelas.
De onde as coisas vêm
Taxas, parâmetros de lançamento, supply e composição dos baskets nunca são escritos num
componente: ficam na configuração do app ou vêm da chain, e as linhas de taxa são
verificadas na inicialização contra as constantes geradas a partir de @stockfun/shared,
que espelha os contratos. Uma porcentagem digitada numa página é o caminho para um produto
acabar anunciando uma tabela de taxas que os contratos não implementam. Desde 2026-10-05,
os números são parâmetros do owner: o pacote compartilhado guarda os seus padrões, e um
valor ao vivo é lido no contrato que o guarda.
O estado do protocolo vem da chain; não há indexador. Um Worker, cairn-worker/, o lê uma
única vez para todas as páginas abertas: um snapshot, depois um WebSocket que envia só as
mudanças. Quando o Worker falha, a página lê a própria chain por um RPC público. Os dados
da própria carteira, dos seus saldos ao que ela pode reivindicar, são lidos pelo
navegador. Desde o sétimo loop de auditoria, o Worker só lê endpoints que dizem servir a
sua chain: um de outra chain conta como fora do ar, então um backup na rede errada nunca
pode fazer todos os mercados sumirem. Desde o oitavo, ele publica o seu oitavo esquema de
dados, que acrescenta o motivo pelo qual falta o preço de uma ação; um app que lê um
esquema mais antigo continua funcionando, sem esse motivo. Desde o nono loop de
auditoria, o watcher do Worker mantém o que aprendeu dos seus RPCs (qual endpoint está
falhando e desde quando, quanto tempo esperar antes de tentar de novo, qual chain cada um
serve) durante os momentos em que o seu objeto na Cloudflare dorme entre duas leituras,
sem nunca guardar o endereço de um endpoint: uma queda do RPC principal custa uma
sondagem por pausa que dobra em vez de três requisições a cada leitura, e uma leitura
saudável não pergunta nada duas vezes. Ele também registra o número de bloco próprio da
Robinhood Chain, onde antes registrava o bloco da chain sobre a qual a Robinhood Chain
liquida.
A auditoria de copy
pnpm audit:copy varre o código-fonte do backend, do keeper, do shared e dos contratos.
Ele falha com uma única palavra proibida ou proibição visual verificável estaticamente.
Ele roda sob demanda; não faz parte do pnpm build. Desde que o dapp anterior saiu do
repositório, ele não varre nenhum front end: no app, o vocabulário proibido é uma regra de
revisão, escrita no seu PRODUCT.md.
Não é um linter de estilo: o vocabulário proibido é uma restrição legal.