La dapp
La app del repositorio es cairn-app/: un front end en Next.js 16 (App Router) con
Tailwind v4, y wagmi y viem para la wallet, servido en el puerto 3100 en local. Sustituyó
a la anterior dapp React + Vite el 2026-10-02. Lleva el nuevo nombre del proyecto,
Cairn.fun, con el que el token del protocolo es $CAIRN; este libro conserva el nombre
StockFun.
La app funciona con datos de demostración, marcados como «Illustrative», hasta que se configure un despliegue, y no hay nada desplegado. Esta página la describe tal como está.
Las pantallas
| Ruta | Pantalla |
|---|---|
/ |
Landing: el próximo drop, la cinta de mercados, el registro público de los drops, la calculadora de comisiones, los baskets, el token del protocolo, las preguntas frecuentes |
/markets |
Descubrimiento: pulso de la tesorería, la franja del token del protocolo, filtros, orden y búsqueda, la tabla de mercados |
/market/[slug] |
Detalle del mercado: gráfico, panel del drop, trades, panel de trading con el reparto de comisiones antes de firmar, progreso de la banda 1, contratos, la posición de la wallet |
/launch |
Formulario de lanzamiento: identidad, selector de basket, vista previa en directo, lista de comprobación de irreversibilidad. Desde el octavo bucle de auditoría solo ofrece los baskets leídos de la cadena, y vuelve a comprobar el elegido frente a la cadena antes de enviar |
/claim |
La reclamación del airdrop por los holders, ventana por ventana, y las comisiones del creador |
/drops |
Todos los drops, ventana por ventana |
/docs |
Mecanismo, comisiones, baskets, drops, el token del protocolo, límites |
El Treasury Ratio, obsoleto desde la decisión del airdrop, que la dapp anterior aún mostraba el 2026-09-28, no aparece en la app del repositorio. La métrica que lo sustituye aún está por decidir.
La pantalla de reclamación
Desde el 2026-10-05, /claim muestra todas las ventanas que la wallet conectada puede
reclamar, mercado por mercado: el final de la ventana, sus acciones, los montos y su valor
en dólares. Los montos vienen solo del contrato del airdrop (claimable), que se vuelve a
leer cada 60 segundos mientras la página está abierta y después de cada transacción de la
wallet. Una acción que le falta al contrato del airdrop tras una transferencia de
emergencia se marca como «awaiting settlement» y queda fuera de la reclamación.
Debajo de la lista vienen un total y «Claim all», que envía claimMany en lotes de cinco
ventanas (diez hasta que la actualización Glamsterdam de Ethereum llegó a Sepolia el
2026-10-06, más abajo), y claim con las demás acciones para una ventana que tiene una
acción a la espera de liquidación. Cada llamada se simula primero: un lote que fallaría
se divide en
sus ventanas, y una ventana en sus demás acciones, de modo que una ventana o una acción
que no se puede pagar ahora se omite y se nombra, sin hundir nunca el resto. Una
transacción cuyo recibo no se puede leer conserva su hash y aparece como enviada, con su
confirmación aún sin leer; no se vuelve a enviar nada, y la página la sigue hasta que
ella, o una transacción en su lugar, se mine: desde el décimo bucle de auditoría, una
aceleración en la wallet se toma por la propia reclamación, con «View tx» apuntando a
ella, mientras que una cancelación o cualquier otro reemplazo la da por no hecha, con
«View tx» sobre lo que se minó (más abajo). El resultado indica lo que se pagó, las
acciones diferidas, que siguen adeudadas, y las ventanas omitidas, con su motivo.
Desde el 2026-10-06:
- Solo el rechazo de la wallet detiene una ejecución, y desde el décimo bucle de auditoría también una cancelación o un reemplazo en la wallet. Una transacción que falla se cuenta («did not go through»), lo que cubría sigue en la lista, y las demás transacciones se envían
- Margen para una acción aún en camino. Un
claimManypaga todas las acciones que una ventana lista en el momento en que se ejecuta, y a una ventana aún abierta a entregas se le puede abonar una acción más entre la estimación y la inclusión: la app añade 400 000 de gas por cada acción del basket que la ventana aún no lista (100 000 hasta la actualización Glamsterdam). El gas no utilizado no se cobra - Gas dimensionado para Glamsterdam. Sepolia activó la actualización Glamsterdam de Ethereum el 2026-10-06, que hace que un nuevo slot de almacenamiento cueste unas cinco veces su gas anterior; mainnet aún no tenía fecha. Cada acción que paga una reclamación puede escribir tres slots nuevos, así que la app deja 400 000 de gas por cada acción aún en camino y reclama cinco ventanas por transacción, cuando diez ventanas de cinco acciones podrían requerir hasta unos 14 millones de gas
- El gas propio de cada escritura. Con la actualización, un slot de almacenamiento escrito desde cero cuesta unos 110 000 de gas, y un trade puede encontrarse con escrituras que su estimación nunca vio: las acumulaciones de comisiones del hook vaciadas por una reclamación justo antes (dos de esas reclamaciones las puede llamar cualquiera), la marca del supply de la hora siguiente, el propio registro del trader movido en el mismo bloque. Desde el noveno bucle de auditoría, la app da a cada escritura su estimación más 50 000 de gas y, a un trade, estimado en el último bloque, también el gas de cada una de esas escrituras que aún puede producirse, leído en ese mismo bloque (112 000 por acumulación, 150 000 por la marca, 135 000 por el registro; todos ellos cuando falla una lectura). Una escritura cuya estimación falla sale con un límite fijo dimensionado a partir de su caso más pesado, nunca con la propia estimación de la wallet, que no tiene margen; un lanzamiento entonces no se envía, y la página dice que se vuelva a intentar. Hasta entonces, cada escritura recibía 150 000 de gas por encima de su estimación (35 000 antes de la actualización). Una wallet reserva el límite multiplicado por su tarifa antes de firmar; solo se cobra el gas utilizado
- Una acción no pagada en la última reclamación. Una acción que una reclamación difirió porque su token rechazó la transferencia a esta wallet (una congelación del emisor, por ejemplo) queda guardada en el navegador, aparece como «not paid at your last claim» y se deja fuera de «Claim all». Cada reclamación la intenta primero por separado y la vuelve a incluir en cuanto su reclamación saldría adelante; cuando no se debe nada más, el botón dice «Try the assets not paid again»
- Sin comprobar, nunca rechazada. Una simulación solo cuenta cuando el nodo dice por qué fallaría la llamada. Una ventana cuya comprobación encontró un error de RPC sigue en la lista como aún no comprobada, y cuando no respondió ninguna simulación, no se envía nada
- Sin leer no equivale a nada. Un mercado cuyas cifras no se pudieron leer se nombra, nunca se muestra como si no debiera nada: «Nothing to claim» solo aparece cuando se leyeron todos los mercados, y la sección del creador dice que sus mercados no se pudieron leer en lugar de «You haven't launched a market»
Las comisiones del creador se reclaman en la misma página, mercado por mercado.
Cifras no disponibles
Desde el 2026-10-05, una tesorería cuyo vault no responde a sus propias funciones de lectura, tras un upgrade defectuoso por ejemplo, muestra «Figures unavailable» en lugar de sus cifras: en la fila y la tarjeta de la lista de mercados, en el banner del token del protocolo y en la página del mercado. Sus acciones y su ETH se siguen mostrando, y el total de la página de mercados indica cuántas tesorerías deja fuera. La app lo sabe por la Lens, que lee cada vault en su propia llamada: consulta Arquitectura.
Desde el 2026-10-06, el ETH que el hook y el lock de liquidez guardan para un vault, tras un pago que el vault rechazó, forma parte de su tesorería, de su valor y de los totales: una línea «ETH owed», con una nota que indica que aún no está en el vault y que entra en él en cuanto el vault lo acepta. Se lee en el hook y en el lock, así que se mantiene al día incluso mientras el vault no responde. Un vault que no responde y nunca se ha leído toma el rail que implica la configuración del bridge del protocolo, y el panel del drop lo indica: dónde están sus acciones se deduce, y sus activos pueden aparecer incompletos. El mercado del token del protocolo sigue en pantalla tal como se leyó por última vez, marcado como desactualizado, cuando la Lens no puede leer su tesorería, en lugar de aparecer como no lanzado.
Lo que muestra la página del mercado
Desde el 2026-10-06:
- Un pool recuperado no tiene gráfico. Una vez que el modo de cierre del lock ha sacado la liquidez de un pool, cualquiera puede mover su precio gratis. La página ya no mostraba para él ni precio ni trading; ahora tampoco muestra variación en 24 horas ni gráfico, y explica por qué, y el Worker deja de muestrear su precio
- La parte de un trade para las acciones es lo que las propias líneas del impuesto de ese trade enviaron a la tesorería, con los ajustes que pagó, nunca los de hoy; «—» cuando no se conoce
- La capitalización de mercado cuenta el supply en circulación, el total menos lo que tiene la dirección de burn, tanto en la cabecera como en el gráfico; desde el séptimo bucle de auditoría muestra «—», nunca $0, cuando no se pudo leer el supply
- Un saldo que no se pudo leer nunca es cero: el panel de posición dice que no se pudo leer, y el panel de trading muestra el último saldo leído, marcado «last read», sin rechazar una venta por encima de él; la simulación de la transacción rechaza un descubierto real
- El mercado del token del protocolo, mantenido tal como se leyó por última vez, lo indica. Mientras la Lens no puede leer su tesorería, su precio y su capitalización de mercado se marcan «(last read)» allí donde aparecen, sin variación en 24 horas, y su gráfico dice «Last read» en lugar de «Now». El Worker no registra ningún precio para él hasta que la Lens vuelve a responder, así que la interrupción no añade ningún punto al gráfico. Su impuesto actual se desconoce: la página no muestra ninguna etiqueta de anti-snipe, y el panel de trading muestra el impuesto normal vigente, sin línea de excedente. Las cifras propias de su token se siguen leyendo en el token, así que «Burned by buyback» en la landing se mantiene al día; su capitalización de mercado es la última leída. Desde el séptimo bucle de auditoría, la interrupción aparece en el gráfico como un hueco, con el último precio leído fechado por su antigüedad, y las cifras del token, se haya leído el mercado o se mantenga tal como se leyó, toman los últimos valores leídos cuando no responden, nunca ceros ni un nombre vacío; un contrato del protocolo que falla tras un upgrade defectuoso ya no las deja en blanco
Desde el séptimo bucle de auditoría, el 2026-10-06:
- El gráfico sitúa cada punto en su momento. Cada muestra de precio se coloca donde cae su hora en el intervalo: 1H, 24H y 7D terminan ahora, y «All» empieza en la primera muestra, nunca en función de la antigüedad del mercado. Un punto bajo el cursor dice su antigüedad («3h 20m ago»), la línea se corta donde faltan muestras (una interrupción, una noche en la que nadie tenía la página abierta), y en reposo el gráfico muestra el precio del mercado, «Now», o «Last read» para un mercado mantenido tal como se leyó por última vez. Hasta entonces, los puntos se repartían de forma uniforme y se fechaban por su posición, así que el último precio leído antes de una interrupción aparecía como actual
- El volumen de 24 horas muestra «—» mientras no se puede contar. Cuando las lecturas de los logs de trades por parte del Worker siguen fallando, su ventana de trades deja de avanzar; el volumen, por mercado y en los totales, muestra entonces «—» en lugar de una cifra que se reduciría como si el trading se hubiera detenido, y vuelve con la primera lectura que se pone al día
Desde el octavo bucle de auditoría, el 2026-10-06:
- Una wallet de la whitelist ve el impuesto normal. Durante la ventana anti-snipe de un mercado, una wallet conectada que está en la whitelist de ese mercado paga el impuesto normal a través del router de StockFun, por el que opera el panel de trading: el panel ya no le muestra ningún excedente de anti-snipe, y explica por qué en una nota. La cotización ya era correcta; la línea de excedente la contradecía. Mientras se desconocen la whitelist o la wallet, el panel muestra el impuesto sin las exenciones
- Por qué una acción no tiene precio. En Robinhood Chain, el oráculo retiene el precio de una acción durante una operación corporativa, y el de todas las acciones mientras el secuenciador está caído o acaba de volver, una comprobación desactivada hasta que Chainlink publique un feed de disponibilidad para la cadena (ver El rail de Robinhood). Donde falta el valor de una acción por ese motivo, la app lo dice: «No price for NVDA right now: corporate action in progress», o el secuenciador de Robinhood Chain caído, en recuperación o con su estado desconocido, debajo de la tabla del basket del panel del drop y de la tarjeta del drop de la landing, y junto a la acción en la página de reclamación. La tarjeta de la landing mostraba una acción así a $0.00; ahora muestra «—»
- El volumen de 24 horas cuenta cada bloque una sola vez. Una lectura del Worker respondida por un nodo unos bloques por detrás del de la lectura anterior ya no hace que la ventana de trades cuente dos veces los mismos bloques. En esa lectura pueden faltar en el feed los trades más recientes; la lectura siguiente los restablece
Desde el noveno bucle de auditoría, el 2026-10-06:
- Un bote que es una estimación lo indica. Cuando parte de una tesorería no se puede leer o valorar (una acción cuyo precio retiene el oráculo, un feed ETH/USD desactualizado, un vault espejo mantenido tal como se leyó por última vez), su cifra cuenta lo que se pudo valorar y ahora lleva un «+» dondequiera que aparezca: las filas y las tarjetas de la lista de mercados, la tarjeta del token del protocolo, el pulso de la tesorería, la landing, el panel del drop y la parte de un holder. La lista de mercados indica que un bote así se ordena por lo que se pudo valorar, y el pulso indica cuántas tesorerías contó de esa manera. Hasta entonces, solo el panel del drop decía «an estimate»
- Una aprobación que no se pudo leer no es cero. Una venta a través del router de StockFun necesita primero la autorización de gasto del router. Una lectura fallida de ella contaba antes como ninguna: a un holder que ya había aprobado se le pedía aprobar de nuevo, con dos firmas, y un segundo fallo enviaba una aprobación para nada. El panel de trading dice ahora que la aprobación no se pudo leer y no cotiza nada, y no envía ni una aprobación ni una venta hasta que la lee. Desde el décimo bucle de auditoría, la venta justo después de una aprobación lee la autorización de gasto, se cotiza y se comprueba en un bloque no anterior al de la aprobación (más abajo), así que un nodo con un bloque de retraso ya no puede responder cero
Desde el décimo bucle de auditoría, el 2026-10-06:
- Una transacción que la wallet cancela nunca se muestra como hecha. Una wallet puede cancelar una transacción pendiente (una transferencia de nada a uno mismo con el mismo nonce) o acelerarla (la misma llamada con una tarifa más alta). La app lee ahora lo que se minó en su lugar: una aceleración se toma por la propia acción, con «View tx» apuntando a ella; una cancelación o cualquier otro reemplazo da el flujo por no hecho, con «Cancelled in your wallet.» o «Replaced by another transaction in your wallet.» y «View tx» sobre lo que se minó, y detiene una ejecución de reclamaciones como lo hace el rechazo de la wallet. Hasta entonces, un trade cancelado mostraba «Done», una aprobación cancelada contaba como dada, una reclamación cancelada de las comisiones del creador mostraba «Claimed» y ocultaba el ETH durante la sesión, y un lanzamiento cancelado mostraba «$TICKER is live.» con la dirección de token inventada de la demo; una transacción acelerada después de la espera habitual de tres minutos nunca se confirmaba, y el panel seguía bloqueado hasta que se recargaba la página
- Seguida después de la espera. Una vez agotada la espera habitual, a los tres minutos, la app sigue la transacción por su nonce: cuando el recuento de transacciones enviadas de la wallet ha pasado ese nonce y la transacción no tiene recibo, busca lo que se minó con ese nonce, nunca en un bloque anterior al envío, y aplica la misma regla. Una cancelación o un reemplazo solo se lee a partir de una transacción realmente encontrada en un bloque, nunca a partir de un recibo que falta
- Un lanzamiento solo está en vivo cuando la cadena lo dice. El lanzamiento lee la creación del mercado en el propio evento de la factory; sin él, vuelve al formulario con el enlace de la transacción, y la dirección de la demo nunca aparece fuera de la demo
- El bloque de tu última transacción, durante un minuto. Durante 60 segundos después de que se mine una de sus propias transacciones, la app comprueba, estima y cotiza lo que envía a continuación en esa cadena en un bloque no anterior a ese, y lee allí la autorización de gasto de una venta. Un endpoint con balanceo de carga puede responder desde un nodo con un bloque de retraso: la venta justo después de su aprobación se rechazaba allí («Approve it first»), y un segundo intento podía enviar una segunda aprobación. Un nodo que no ha alcanzado ese bloque cuenta ahora como retrasado y se le vuelve a preguntar; si ninguno lo alcanza en unos segundos, no se envía nada. Para una venta justo después de su aprobación, la página dice entonces «Your approval went through, but no quote could be read from the pool, so the sale was not sent…» (su cotización es la primera lectura hecha allí); una escritura cuya propia comprobación no puede alcanzar el bloque (una aprobación, una reclamación, un lanzamiento) dice «This could not be checked against the block of your last transaction just now, so nothing was sent. Try again in a moment.»
- Lo que queda. Un falso «Replaced» sigue siendo posible en un caso estrecho, cuando se cumple todo esto: ningún nodo mostró la venta, así que la app dedujo su nonce del recuento de la wallet; un nodo por detrás de los demás, aunque no por detrás del bloque leído antes del envío, respondió el recuento; otra transacción de la misma wallet, que ese nodo no había visto, tomó el nonce deducido; y la venta, enviada a través de un relay privado, seguía sin minarse después de los tres minutos y un periodo de gracia de unos 36 segundos. Una cancelación nunca se muestra como hecha, y nada queda bloqueado
El lanzamiento
El formulario de lanzamiento solo ofrece los baskets leídos del registro de la cadena, con la numeración propia del registro: antes de la primera lectura no ofrece ninguno («Reading the baskets from the chain…»), y antes de que se desplieguen los contratos indica que el lanzamiento se abrirá en cuanto estén activos. Justo antes de enviar, vuelve a leer de la factory tanto la comisión de creación como el basket elegido, su nombre y sus acciones con sus pesos, y se niega a enviar, sin enviar nada, si alguno de los dos difiere de lo que muestra; el mensaje nombra el basket que la cadena tiene con ese número. Hasta el octavo bucle de auditoría, el formulario ofrecía los baskets configurados, con sus números configurados, antes de su primera lectura, y un despliegue cuyo registro numera sus baskets de otra manera podría haber lanzado un mercado sobre un basket distinto del mostrado. El formulario paga solo la comisión de creación: no incluye ninguna compra propia del creador, ni nombra ninguna whitelist anti-snipe, y lo indica (la factory acepta ambas en una llamada directa: consulta Lanzar un mercado). La confirmación nombra el basket comprobado al enviar.
Dirección de arte
Las reglas de diseño de la app están en su propio DESIGN.md: un tema claro y cálido
sobre un lienzo blanco, terracota para los drops y las cifras clave, Poppins para la
interfaz e Instrument Serif para las grandes cifras financieras, cifras tabulares en las
tablas.
De dónde viene cada cosa
Las comisiones, los parámetros de lanzamiento, el supply y la composición de los baskets
nunca se escriben en un componente: están en la configuración de la app o vienen de la
cadena, y las líneas de comisiones se comprueban al arrancar frente a las constantes
generadas a partir de @stockfun/shared, que refleja los contratos. Un porcentaje
tecleado en una página es la forma en que un producto acaba anunciando un esquema de
comisiones que los contratos no implementan. Desde el 2026-10-05, esas cifras son ajustes
del owner: el paquete compartido guarda sus valores por defecto, y el valor vigente se lee
del contrato que lo guarda.
El estado del protocolo viene de la cadena; no hay indexador. Un Worker, cairn-worker/,
lo lee una sola vez para todas las páginas abiertas: una instantánea y luego un WebSocket
que solo envía los cambios. Cuando el Worker falla, la página lee la cadena por sí misma a
través de un RPC público. Los datos propios de la wallet, desde sus saldos hasta lo que
puede reclamar, los lee el navegador. Desde el séptimo bucle de auditoría, el Worker solo
lee endpoints que dicen servir su cadena: uno de otra cadena cuenta como caído, así que un
respaldo en la red equivocada nunca puede hacer desaparecer todos los mercados. Desde el
octavo publica su octavo esquema de datos, que añade el motivo por el que falta el precio
de una acción; una app que lee un esquema anterior sigue funcionando, sin ese motivo.
Desde el noveno bucle de auditoría, el watcher del Worker conserva lo que aprendió de sus
RPC (qué endpoint falla y desde cuándo, cuánto tiempo esperar antes de reintentar, qué
cadena sirve cada uno) durante los momentos en que su objeto de Cloudflare duerme entre
dos lecturas, sin guardar nunca la dirección de un endpoint: una caída del RPC principal
cuesta un sondeo por cada pausa que se duplica en lugar de tres peticiones en cada
lectura, y una lectura sana no pregunta nada dos veces. También registra el número de
bloque propio de Robinhood Chain, donde antes registraba el bloque de la cadena sobre la
que liquida Robinhood Chain.
La auditoría de textos
pnpm audit:copy escanea el código fuente del backend, el keeper, shared y los contratos.
Falla ante una sola palabra prohibida o una sola prohibición visual que se pueda comprobar
de forma estática. Se ejecuta a demanda; no forma parte de pnpm build. Desde que la dapp
anterior salió del repositorio no escanea ningún front end: en la app, el vocabulario
prohibido es una regla de revisión, escrita en su PRODUCT.md.
No es un linter de estilo: el vocabulario prohibido es una restricción legal.