La dapp

L'app nel repository è cairn-app/: un front end Next.js 16 (App Router) con Tailwind v4, e wagmi e viem per il wallet, servito in locale sulla porta 3100. Ha sostituito la precedente dapp React + Vite il 2026-10-02. Porta il nuovo nome del progetto, Cairn.fun, con cui il token del protocollo è $CAIRN; questo libro mantiene il nome StockFun.

L'app funziona con dati demo, contrassegnati come "Illustrative", finché non viene configurato un deploy, e non c'è nulla di deployato. Questa pagina la descrive così com'è.

Le schermate

Route Schermata
/ Landing: il prossimo drop, il nastro dei mercati, il registro pubblico dei drop, il calcolatore delle commissioni, i basket, il token del protocollo, le FAQ
/markets Scoperta: polso della tesoreria, la striscia del token del protocollo, filtri, ordinamento e ricerca, la tabella dei mercati
/market/[slug] Dettaglio del mercato: grafico, pannello del drop, trade, pannello di trading con la ripartizione delle commissioni prima della firma, avanzamento della banda 1, contratti, la posizione del wallet
/launch Modulo di lancio: identità, selettore del basket, anteprima in tempo reale, checklist di irreversibilità. Dall'ottavo ciclo di audit offre solo i basket letti dalla chain, e ricontrolla quello scelto rispetto alla chain prima dell'invio
/claim La riscossione dell'airdrop da parte degli holder, finestra per finestra, e le commissioni del creatore
/drops Ogni drop, finestra per finestra
/docs Meccanismo, commissioni, basket, drop, il token del protocollo, limiti

Il Treasury Ratio, obsoleto dalla decisione sull'airdrop, che la dapp precedente mostrava ancora il 2026-09-28, non compare nell'app nel repository. La metrica che lo sostituisce è ancora da decidere.

La schermata di riscossione

Dal 2026-10-05 /claim elenca ogni finestra che il wallet connesso può riscuotere, mercato per mercato: la fine della finestra, le sue azioni, gli importi e il loro valore in dollari. Gli importi vengono solo dal contratto dell'airdrop (claimable), riletti ogni 60 secondi mentre la pagina è aperta e dopo ogni transazione del wallet. Un'azione che manca al contratto dell'airdrop dopo un trasferimento di emergenza viene contrassegnata come "awaiting settlement" e lasciata fuori dalla riscossione.

Sotto l'elenco vengono un totale e "Claim all", che invia claimMany in batch di cinque finestre (dieci finché l'aggiornamento Glamsterdam di Ethereum non ha raggiunto Sepolia il 2026-10-06, vedi sotto), e claim con le altre azioni per una finestra che ha un'azione in attesa di sistemazione. Ogni chiamata viene prima simulata: un batch che fallirebbe viene diviso nelle sue finestre, e una finestra nelle sue altre azioni, così una finestra o un'azione che non si può pagare ora viene saltata e nominata, senza mai affondare il resto. Una transazione la cui ricevuta non si può leggere conserva il suo hash e appare come inviata, con la conferma non ancora letta; nulla viene inviato di nuovo, e la pagina la segue finché essa, o una transazione al suo posto, non viene minata: dal decimo ciclo di audit, un'accelerazione nel wallet vale come la riscossione stessa, con "View tx" che punta a essa, mentre un annullamento o qualsiasi altra sostituzione la chiude come non avvenuta, con "View tx" su ciò che è stato minato (sotto). Il risultato indica ciò che è stato pagato, le azioni differite, che restano dovute, e le finestre saltate, con il loro motivo.

Dal 2026-10-06:

  • Solo il rifiuto del wallet ferma un'esecuzione, e dal decimo ciclo di audit anche un annullamento o una sostituzione nel wallet. Una transazione che fallisce viene contata ("did not go through"), ciò che copriva resta in elenco, e le altre transazioni partono
  • Spazio per un'azione ancora in arrivo. Un claimMany paga ogni azione che una finestra elenca nel momento in cui viene eseguito, e a una finestra ancora aperta alle consegne può essere accreditata un'azione in più tra la stima e l'inclusione: l'app aggiunge 400.000 gas per ogni azione del basket che la finestra non elenca ancora (100.000 fino all'aggiornamento Glamsterdam). Il gas non usato non viene addebitato
  • Gas dimensionato per Glamsterdam. Sepolia ha attivato l'aggiornamento Glamsterdam di Ethereum il 2026-10-06, che fa costare a un nuovo slot di storage circa cinque volte il suo gas precedente; la mainnet non aveva ancora una data. Ogni azione che una riscossione paga può scrivere tre nuovi slot, quindi l'app lascia 400.000 gas per ogni azione ancora in arrivo e riscuote cinque finestre per transazione, dove dieci finestre di cinque azioni potrebbero richiedere fino a circa 14 milioni di gas
  • Il gas proprio di ogni scrittura. Con l'aggiornamento, uno slot di storage scritto da zero costa circa 110.000 gas, e un trade può incontrare scritture che la sua stima non ha mai visto: gli accumuli di commissioni dell'hook svuotati da una riscossione appena prima (due di quelle riscossioni le può chiamare chiunque), il segno di supply dell'ora successiva, la registrazione del trader stesso spostata nello stesso blocco. Dal nono ciclo di audit l'app dà a ogni scrittura la sua stima più 50.000 gas e, a un trade, stimato all'ultimo blocco, anche il gas di ciascuna di quelle scritture che può ancora avvenire, letto a quello stesso blocco (112.000 per accumulo, 150.000 per il segno, 135.000 per la registrazione; tutti quando una lettura fallisce). Una scrittura la cui stima fallisce parte con un limite fisso dimensionato sul suo caso più pesante, mai con la stima propria del wallet, che non ha margine; un lancio allora non viene inviato, e la pagina dice di riprovare. Fino ad allora ogni scrittura riceveva 150.000 gas oltre la sua stima (35.000 prima dell'aggiornamento). Un wallet mette da parte il limite moltiplicato per la sua commissione prima di firmare; viene addebitato solo il gas usato
  • Un'azione non pagata all'ultima riscossione. Un'azione che una riscossione ha differito perché il suo token ha rifiutato il trasferimento verso questo wallet (un congelamento dell'emittente, per esempio) viene ricordata dal browser, mostrata come "not paid at your last claim" e lasciata fuori da "Claim all". Ogni riscossione la prova prima da sola e la riprende non appena la sua riscossione andrebbe a buon fine; quando non è dovuto nient'altro, il pulsante mostra "Try the assets not paid again"
  • Non verificata, mai rifiutata. Una simulazione conta solo quando il nodo dice perché la chiamata fallirebbe. Una finestra il cui controllo ha incontrato un errore RPC resta in elenco come non ancora verificata, e quando nessuna simulazione ha risposto, non viene inviato nulla
  • Non letto non è vuoto. Un mercato le cui cifre non si sono potute leggere viene nominato, mai mostrato come se non dovesse nulla: "Nothing to claim" compare solo quando tutti i mercati sono stati letti, e la sezione del creatore dice che i suoi mercati non si sono potuti leggere invece di "You haven't launched a market"

Le commissioni del creatore si riscuotono nella stessa pagina, mercato per mercato.

Cifre non disponibili

Dal 2026-10-05 una tesoreria il cui vault non risponde alle proprie funzioni di lettura, dopo un upgrade difettoso per esempio, mostra "Figures unavailable" al posto delle sue cifre: nella riga e nella scheda dell'elenco dei mercati, nel banner del token del protocollo e nella pagina del mercato. Le sue azioni e il suo ETH vengono ancora elencati, e il totale della pagina dei mercati indica quante tesorerie ha lasciato fuori. L'app lo sa dalla Lens, che legge ogni vault in una chiamata a sé: vedi Architettura.

Dal 2026-10-06 l'ETH che l'hook e il lock della liquidità conservano per un vault, dopo un pagamento che il vault ha rifiutato, fa parte della sua tesoreria, del suo valore e dei totali: una riga "ETH owed", con una nota che indica che non è ancora nel vault e vi entra non appena il vault lo accetta. Si legge sull'hook e sul lock, quindi resta aggiornato anche mentre il vault non risponde. Un vault che non risponde e non è mai stato letto prende il rail che la configurazione del bridge del protocollo implica, e il pannello del drop lo dice: dove si trovano le sue azioni è dedotto, e le sue disponibilità possono essere incomplete. Il mercato del token del protocollo resta sullo schermo come letto l'ultima volta, contrassegnato come non aggiornato, quando la Lens non riesce a leggerne la tesoreria, invece di apparire come non lanciato.

Cosa mostra la pagina del mercato

Dal 2026-10-06:

  • Un pool recuperato non ha grafico. Una volta che la modalità di chiusura del lock ha ritirato la liquidità di un pool, chiunque può muoverne il prezzo gratis. La pagina già non mostrava per esso né prezzo né trading; ora non mostra nemmeno la variazione a 24 ore né il grafico, e dice perché, e il Worker smette di campionarne il prezzo
  • La quota di un trade per le azioni è ciò che le righe della tassa di quel trade hanno inviato alla tesoreria, alle impostazioni che ha pagato, mai a quelle di oggi; "—" quando non è nota
  • La market cap conta la supply in circolazione, il totale meno ciò che detiene l'indirizzo di burn, nell'intestazione come nel grafico; dal settimo ciclo di audit mostra "—", mai $0, quando la supply non si è potuta leggere
  • Un saldo che non si è potuto leggere non è mai zero: il pannello della posizione dice che non si è potuto leggere, e il pannello di trading mostra l'ultimo saldo letto, contrassegnato "last read", senza rifiutare una vendita superiore; la simulazione della transazione rifiuta uno scoperto reale
  • Il mercato del token del protocollo mantenuto come letto l'ultima volta lo dice. Finché la Lens non riesce a leggerne la tesoreria, il suo prezzo e la sua market cap sono contrassegnati "(last read)" ovunque compaiano, senza variazione a 24 ore, e il suo grafico mostra "Last read" invece di "Now". Il Worker non registra alcun prezzo per esso finché la Lens non risponde di nuovo, così l'interruzione non aggiunge alcun punto al grafico. La sua tassa in questo momento è sconosciuta: la pagina non mostra alcuna etichetta anti-snipe, e il pannello di trading mostra la tassa normale in vigore, senza riga dell'eccedenza. Le cifre proprie del suo token si leggono ancora sul token, così "Burned by buyback" sulla landing resta aggiornato; la sua market cap è l'ultima letta. Dal settimo ciclo di audit l'interruzione appare sul grafico come un vuoto, con l'ultimo prezzo letto datato dalla sua età, e le cifre del token, con il mercato letto o mantenuto, prendono gli ultimi valori letti quando non rispondono, mai zeri o un nome vuoto; un contratto del protocollo che fallisce dopo un upgrade difettoso non le azzera più

Dal settimo ciclo di audit, il 2026-10-06:

  • Il grafico colloca ogni punto al suo orario. Ogni campione di prezzo si trova dove cade il suo orario nell'intervallo: 1H, 24H e 7D finiscono adesso, e "All" parte dal primo campione, mai dall'età del mercato. Un punto sotto il cursore dice quanto è vecchio ("3h 20m ago"), la linea si interrompe dove mancano campioni (un'interruzione, una notte in cui nessuno aveva la pagina aperta), e a riposo il grafico mostra il prezzo del mercato, "Now", o "Last read" per un mercato mantenuto come letto l'ultima volta. Fino ad allora i punti erano distribuiti uniformemente e datati dalla loro posizione, così l'ultimo prezzo letto prima di un'interruzione appariva come attuale
  • Il volume a 24 ore mostra "—" finché non può essere contato. Quando le letture dei log dei trade da parte del Worker continuano a fallire, la sua finestra dei trade smette di avanzare; il volume, per mercato e nei totali, mostra allora "—" invece di una cifra che si ridurrebbe come se il trading si fosse fermato, e torna con la prima lettura che recupera

Dall'ottavo ciclo di audit, il 2026-10-06:

  • Un wallet nella whitelist vede la tassa normale. Durante la finestra anti-snipe di un mercato, un wallet connesso che è nella whitelist di quel mercato paga la tassa normale attraverso il router di StockFun, attraverso cui opera il pannello di trading: il pannello ora non mostra alcuna eccedenza anti-snipe per esso, e spiega perché in una nota. La quotazione era già giusta; la riga dell'eccedenza la contraddiceva. Finché la whitelist o il wallet sono sconosciuti, il pannello mostra la tassa al di fuori delle esenzioni
  • Perché un'azione non ha prezzo. Su Robinhood Chain l'oracolo trattiene il prezzo di un'azione durante un'operazione societaria, e il prezzo di ogni azione finché il sequencer è fermo o appena ripartito, un controllo disattivato finché Chainlink non pubblica un feed di uptime per la chain (vedi Il rail Robinhood). Dove il valore di un'azione manca per quel motivo, l'app lo dice: "No price for NVDA right now: corporate action in progress", o il sequencer di Robinhood Chain fermo, in ripresa o con stato sconosciuto, sotto la tabella del basket del pannello del drop e la scheda del drop della landing, e accanto all'azione nella pagina di riscossione. La scheda della landing mostrava un'azione del genere a $0.00; ora mostra "—"
  • Il volume a 24 ore conta ogni blocco una volta. Una lettura del Worker a cui risponde un nodo qualche blocco indietro rispetto a quello della lettura precedente non fa più contare due volte gli stessi blocchi alla finestra dei trade. Per quella sola lettura i trade più recenti possono mancare dal feed; la lettura successiva li ripristina

Dal nono ciclo di audit, il 2026-10-06:

  • Un piatto che è una stima lo dice. Quando una parte di una tesoreria non si può leggere o valorizzare (un'azione il cui prezzo l'oracolo trattiene, un feed ETH/USD non aggiornato, un vault speculare mantenuto come letto l'ultima volta), la sua cifra conta ciò che si è potuto valorizzare e ora porta un "+" ovunque compaia: le righe e le schede dell'elenco dei mercati, la scheda del token del protocollo, il polso della tesoreria, la landing, il pannello del drop e la quota di un holder. L'elenco dei mercati dice che un piatto del genere si ordina in base a ciò che si è potuto valorizzare, e il polso dice quante tesorerie ha contato in quel modo. Fino ad allora solo il pannello del drop diceva "an estimate"
  • Un'approvazione che non si è potuta leggere non è zero. Una vendita attraverso il router di StockFun richiede prima l'allowance del router. Una lettura fallita di essa contava come nessuna: a un holder che aveva già approvato veniva chiesto di approvare di nuovo, con due firme, e un secondo fallimento inviava un'approvazione per niente. Il pannello di trading ora dice che l'approvazione non si è potuta leggere e non quota nulla, e non invia né un'approvazione né una vendita finché non la legge. Dal decimo ciclo di audit la vendita subito dopo un'approvazione legge l'allowance, si quota e si verifica a un blocco non anteriore a quello dell'approvazione (sotto), così un nodo indietro di un blocco non può più rispondere zero

Dal decimo ciclo di audit, il 2026-10-06:

  • Una transazione che il wallet annulla non viene mai mostrata come fatta. Un wallet può annullare una transazione in sospeso (un trasferimento di niente a sé stessi con lo stesso nonce) o accelerarla (la stessa chiamata con una commissione più alta). L'app ora legge ciò che è stato minato al suo posto: un'accelerazione vale come l'azione stessa, con "View tx" che punta a essa; un annullamento o qualsiasi altra sostituzione chiude il flusso come non fatto, con "Cancelled in your wallet." o "Replaced by another transaction in your wallet." e "View tx" su ciò che è stato minato, e ferma un'esecuzione di riscossioni come fa il rifiuto del wallet. Fino ad allora un trade annullato mostrava "Done", un'approvazione annullata contava come data, una riscossione annullata delle commissioni del creatore mostrava "Claimed" e nascondeva l'ETH per la sessione, e un lancio annullato mostrava "$TICKER is live." con l'indirizzo del token inventato della demo; una transazione accelerata dopo l'attesa abituale di tre minuti non veniva mai confermata, e il pannello restava bloccato finché la pagina non veniva ricaricata
  • Seguita dopo l'attesa. Una volta esaurita l'attesa abituale, dopo tre minuti, l'app segue la transazione tramite il suo nonce: quando il conteggio delle transazioni inviate dal wallet ha superato quel nonce e la transazione non ha ricevuta, cerca ciò che è stato minato a quel nonce, mai in un blocco precedente all'invio, e applica la stessa regola. Un annullamento o una sostituzione si legge solo da una transazione effettivamente trovata in un blocco, mai da una ricevuta mancante
  • Un lancio è attivo solo quando la chain lo dice. Il lancio legge la creazione del mercato dall'evento della factory stessa; senza di esso, torna al modulo con il link della transazione, e l'indirizzo della demo non compare mai fuori dalla demo
  • Il blocco della tua ultima transazione, per un minuto. Per 60 secondi dopo che una delle sue transazioni è stata minata, l'app verifica, stima e quota ciò che invia dopo su quella chain a un blocco non anteriore a quello, e vi legge l'allowance di una vendita. Un endpoint con bilanciamento del carico può rispondere da un nodo indietro di un blocco: la vendita subito dopo la sua approvazione vi veniva rifiutata ("Approve it first"), e un secondo tentativo poteva inviare una seconda approvazione. Un nodo che non ha raggiunto quel blocco ora conta come in ritardo e viene interrogato di nuovo; se nessuno lo raggiunge entro pochi secondi, non viene inviato nulla. Per una vendita subito dopo la sua approvazione la pagina dice allora "Your approval went through, but no quote could be read from the pool, so the sale was not sent…" (la sua quotazione è la prima lettura fatta lì); una scrittura il cui controllo non può raggiungere il blocco (un'approvazione, una riscossione, un lancio) dice "This could not be checked against the block of your last transaction just now, so nothing was sent. Try again in a moment."
  • Ciò che resta. Un falso "Replaced" resta possibile in un caso ristretto, quando valgono tutte queste condizioni: nessun nodo ha mostrato la vendita, così l'app ne ha dedotto il nonce dal conteggio del wallet; un nodo indietro rispetto agli altri, ma non rispetto al blocco letto prima dell'invio, ha risposto al conteggio; un'altra transazione dello stesso wallet, che quel nodo non aveva visto, ha preso il nonce dedotto; e la vendita, inviata tramite un relay privato, non era ancora minata dopo i tre minuti e un periodo di grazia di circa 36 secondi. Un annullamento non viene mai mostrato come fatto, e nulla resta bloccato

Il lancio

Il modulo di lancio offre solo i basket letti dal registro della chain, con la numerazione propria del registro: prima della prima lettura non ne offre nessuno ("Reading the baskets from the chain…"), e prima che i contratti siano deployati dice che il lancio si apre non appena sono attivi. Subito prima dell'invio rilegge dalla factory sia la commissione di creazione sia il basket scelto, il suo nome e le sue azioni con i loro pesi, e si rifiuta di inviare, senza inviare nulla, se uno dei due differisce da ciò che mostra; il messaggio nomina il basket che la chain detiene sotto quel numero. Fino all'ottavo ciclo di audit il modulo offriva i basket configurati, con i loro numeri configurati, prima della sua prima lettura, e un deploy il cui registro numera i suoi basket in altro modo avrebbe potuto lanciare un mercato su un basket diverso da quello mostrato. Il modulo paga solo la commissione di creazione: non ha un acquisto proprio del creatore, e non indica alcuna whitelist anti-snipe, cosa che dice (la factory accetta entrambi da una chiamata diretta: vedi Lanciare un mercato). La conferma nomina il basket controllato all'invio.

Direzione artistica

Le regole di design dell'app stanno nel suo DESIGN.md: un tema chiaro e caldo su una tela bianca, terracotta per i drop e le cifre chiave, Poppins per l'interfaccia e Instrument Serif per le grandi cifre finanziarie, cifre tabulari nelle tabelle.

Da dove vengono i dati

Commissioni, parametri di lancio, supply e composizione dei basket non sono mai scritti in un componente: stanno nella configurazione dell'app o arrivano dalla chain, e le voci delle commissioni vengono verificate all'avvio rispetto alle costanti generate da @stockfun/shared, che rispecchia i contratti. Una percentuale digitata in una pagina è il modo in cui un prodotto finisce per pubblicizzare uno schema che i contratti non implementano. Dal 2026-10-05 i numeri sono impostazioni dell'owner: il pacchetto condiviso ne contiene i valori di default, e un valore attuale viene letto dal contratto che lo detiene.

Lo stato del protocollo arriva dalla chain; non c'è alcun indexer. Un Worker, cairn-worker/, lo legge una sola volta per tutte le pagine aperte: uno snapshot, poi un WebSocket che invia solo le modifiche. Quando il Worker non funziona, la pagina legge la chain da sé tramite un RPC pubblico. I dati del wallet stesso, dai suoi saldi a ciò che può riscuotere, li legge il browser. Dal settimo ciclo di audit il Worker legge solo endpoint che dichiarano di servire la sua chain: uno su un'altra chain conta come fuori servizio, così un backup sulla rete sbagliata non può mai far sparire tutti i mercati. Dall'ottavo pubblica il suo ottavo schema di dati, che aggiunge il motivo per cui manca il prezzo di un'azione; un'app che legge uno schema più vecchio continua a funzionare, senza quel motivo. Dal nono ciclo di audit il watcher del Worker conserva ciò che ha appreso dei suoi RPC (quale endpoint sta fallendo e da quando, quanto attendere prima di riprovare, quale chain serve ciascuno) durante i momenti in cui il suo oggetto Cloudflare dorme tra due letture, senza mai memorizzare l'indirizzo di un endpoint: un'interruzione dell'RPC principale costa una sonda per pausa che raddoppia invece di tre richieste a ogni lettura, e una lettura sana non chiede nulla due volte. Registra anche il numero di blocco proprio di Robinhood Chain, dove prima registrava il blocco della chain su cui Robinhood Chain si regola.

L'audit dei testi

pnpm audit:copy analizza i sorgenti di backend, keeper, shared e contratti. Fallisce per una sola parola vietata o un solo divieto visivo verificabile staticamente. Viene eseguito su richiesta; non fa parte di pnpm build. Da quando la dapp precedente ha lasciato il repository non analizza alcun front end: nell'app, il vocabolario vietato è una regola di revisione, scritta nel suo PRODUCT.md.

Non è un linter di stile: il vocabolario vietato è un vincolo legale.