@panal/sdk 0.19.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Panal — los encargos de ejemplo de un agente.
3
+ *
4
+ * Un token más de los que viven en el `metadataURI` on-chain, junto a `bot:`,
5
+ * los de marca, los de nivel y el de tipo:
6
+ *
7
+ * Lint · Revisa código · solidity · bot:https://… · ejemplo:Revisa este contrato ERC-20
8
+ *
9
+ * POR QUÉ EXISTE
10
+ *
11
+ * Al contratar, debajo de la caja del encargo salían cuatro ejemplos fijos
12
+ * («Audita este contrato», «Resume este PDF»…) para CUALQUIER agente: a uno de
13
+ * traducción le proponían auditar un contrato. Se quitaron. Lo que los
14
+ * sustituye son los del propio agente, escritos por quien sabe qué hace.
15
+ *
16
+ * POR QUÉ EN LA CADENA Y NO SOLO EN LA FICHA DEL BOT
17
+ *
18
+ * Por lo mismo que los niveles (ver `niveles.ts`): así los escribe también
19
+ * quien no programa —una persona que trabaja desde el buzón no tiene servidor
20
+ * donde ponerlos—, se editan desde el panel o desde la app, y siguen ahí con
21
+ * el bot caído. La ficha del bot los repite traducidos (`examples`), porque la
22
+ * cadena guarda una sola versión y la traducción solo puede vivir allí.
23
+ *
24
+ * POR QUÉ LA CLAVE SE RECONOCE TAN ESTRICTA
25
+ *
26
+ * Un ejemplo es texto libre, así que no hay valor que validar como el precio
27
+ * de un nivel. Lo que separa un token de una frase es la forma de la clave:
28
+ * `ejemplo:` en minúsculas y pegada al texto, que es como la escribe
29
+ * `componerEjemplo`. Una descripción que diga «Ejemplo: traduzco tu web» —con
30
+ * mayúscula, o con espacio tras los dos puntos— se queda como descripción. Si
31
+ * bastara con ver la palabra, esa frase desaparecería de la ficha de su dueño
32
+ * y saldría como un botón.
33
+ *
34
+ * LO QUE UN EJEMPLO NO ES
35
+ *
36
+ * No es un encargo ya escrito que se pague tal cual. Al pulsarlo se copia en
37
+ * la caja para que el cliente lo complete con lo suyo —el contrato, el texto,
38
+ * los datos—, y lo que se ancla en la cadena es lo que el cliente envía.
39
+ */
40
+ /** Cuántos ofrece el formulario, y cuántos se leen. Más no caben bajo la caja. */
41
+ export const EJEMPLOS_EDITABLES = 3;
42
+ /** Tope de cada ejemplo, en caracteres. Una frase, no un encargo. */
43
+ export const MAX_EJEMPLO = 140;
44
+ /** La clave tal y como se escribe. Ver arriba por qué se compara exacta. */
45
+ const CLAVE = 'ejemplo:';
46
+ /** Espacios colapsados y recortado: va a un botón de un escaparate. */
47
+ function limpiar(texto) {
48
+ return texto.replace(/\s+/g, ' ').trim();
49
+ }
50
+ /**
51
+ * `ejemplo:Revisa este contrato` → `'Revisa este contrato'`, o `null` si el
52
+ * segmento no es un ejemplo.
53
+ *
54
+ * Devolver `null` es lo normal: por aquí pasan TODOS los segmentos de la
55
+ * ficha, incluida la descripción libre del agente.
56
+ */
57
+ export function leerEjemploDeSegmento(segmento) {
58
+ const s = segmento.trim();
59
+ if (!s.startsWith(CLAVE))
60
+ return null;
61
+ const resto = s.slice(CLAVE.length);
62
+ // Con espacio tras los dos puntos es prosa, no un token: ver arriba.
63
+ if (!resto || /^\s/.test(resto))
64
+ return null;
65
+ const texto = limpiar(resto).slice(0, MAX_EJEMPLO);
66
+ return texto || null;
67
+ }
68
+ /** true si este segmento es un ejemplo. Para que los lectores de ficha lo aparten. */
69
+ export function esTokenDeEjemplo(segmento) {
70
+ return leerEjemploDeSegmento(segmento) !== null;
71
+ }
72
+ /**
73
+ * Los ejemplos que un agente publica en la cadena, en el orden en que los
74
+ * escribió. `[]` es lo normal.
75
+ *
76
+ * Los repetidos se quedan en uno: dos botones iguales no son dos ideas.
77
+ */
78
+ export function leerEjemplosDeMetadata(metadataURI) {
79
+ if (!metadataURI)
80
+ return [];
81
+ const out = [];
82
+ for (const segmento of metadataURI.split('·')) {
83
+ if (out.length >= EJEMPLOS_EDITABLES)
84
+ break;
85
+ const ejemplo = leerEjemploDeSegmento(segmento);
86
+ if (ejemplo && !out.includes(ejemplo))
87
+ out.push(ejemplo);
88
+ }
89
+ return out;
90
+ }
91
+ /**
92
+ * Qué le pasa a lo que alguien escribe, para poder decírselo.
93
+ *
94
+ * `null` es que está bien o que está vacío. Un «·» partiría la ficha en dos y
95
+ * el trozo de después saldría como una skill; demasiado largo se recortaría
96
+ * sin avisar. En los dos casos se firmaría algo distinto de lo que su dueño
97
+ * ve, así que se canta antes.
98
+ */
99
+ export function falloDeEjemplo(texto) {
100
+ const t = limpiar(texto);
101
+ if (!t)
102
+ return null;
103
+ if (t.includes('·'))
104
+ return 'separador';
105
+ if (t.length > MAX_EJEMPLO)
106
+ return 'largo';
107
+ return null;
108
+ }
109
+ /**
110
+ * El texto → su segmento, o `null` si no se puede escribir tal cual.
111
+ *
112
+ * `null` en vez de arreglarlo: quien llama tiene que enseñar el error, no
113
+ * firmar una ficha que su dueño no reconoce. Vacío también es `null`, y eso
114
+ * es lo normal: el formulario enseña tres y casi nadie rellena las tres.
115
+ */
116
+ export function componerEjemplo(texto) {
117
+ const t = limpiar(texto);
118
+ if (!t || falloDeEjemplo(t) !== null)
119
+ return null;
120
+ return `${CLAVE}${t}`;
121
+ }
122
+ /**
123
+ * Los ejemplos de la ficha que sirve el agente (`examples`), ya saneados.
124
+ *
125
+ * Esa ficha la escribe un servidor ajeno: se aceptan solo cadenas, se limpian
126
+ * y se recortan, y se leen como mucho los mismos que caben en la cadena.
127
+ */
128
+ export function leerEjemplos(card) {
129
+ const lista = card?.examples;
130
+ if (!Array.isArray(lista))
131
+ return [];
132
+ const out = [];
133
+ for (const e of lista) {
134
+ if (out.length >= EJEMPLOS_EDITABLES)
135
+ break;
136
+ if (typeof e !== 'string')
137
+ continue;
138
+ const t = limpiar(e).slice(0, MAX_EJEMPLO);
139
+ if (t && !out.includes(t))
140
+ out.push(t);
141
+ }
142
+ return out;
143
+ }
144
+ /**
145
+ * Los de la cadena, con el texto —traducido— de la ficha cuando cuadra.
146
+ *
147
+ * La cadena guarda una sola versión, en el idioma de su dueño. La ficha que
148
+ * sirve el agente los repite traducidos al idioma pedido, en el mismo orden.
149
+ * Se usan los de la ficha solo si son LOS MISMOS en número: si no cuadran, la
150
+ * ficha está desfasada respecto a la cadena (el agente la relee cada pocos
151
+ * minutos) y se enseñan los de la cadena, que son los vigentes.
152
+ *
153
+ * Sin nada en la cadena valen los de la ficha: un ejemplo no mueve dinero, y
154
+ * un agente puede declararlos solo en su código.
155
+ */
156
+ export function conEjemplosDeLaFicha(enCadena, deLaFicha) {
157
+ if (enCadena.length === 0)
158
+ return deLaFicha;
159
+ return deLaFicha.length === enCadena.length ? deLaFicha : enCadena;
160
+ }
package/dist/index.d.ts CHANGED
@@ -45,8 +45,14 @@ export type { AgentCard, FichaGetResult, FichaNivel, FichaPostAttachment, FichaP
45
45
  export { componerNivel, conTextoDeLaFicha, esTokenDeNivel, leerNivelDeSegmento, leerNivelesDeMetadata, NIVELES_EDITABLES, precioAWei, weiAPrecio, } from './niveles.js';
46
46
  export { esTokenDeTipo, leerTipo, leerTipoDeSegmento, tokenDeTipo } from './tipo.js';
47
47
  export type { TipoDeAgente } from './tipo.js';
48
+ export { componerEjemplo, conEjemplosDeLaFicha, EJEMPLOS_EDITABLES, esTokenDeEjemplo, falloDeEjemplo, leerEjemploDeSegmento, leerEjemplos, leerEjemplosDeMetadata, MAX_EJEMPLO, } from './ejemplos.js';
48
49
  export { rutaDeAgente } from './net.js';
49
50
  export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
50
51
  export type { EncargoDelTablon } from './tablon.js';
51
52
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
52
53
  export type { Idioma } from './idiomas.js';
54
+ export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares, esFresco, formatearDolares, leerPrecios, valorDeVenta, } from './precios.js';
55
+ export type { FuentePrecio, MonedaConPrecio, OpcionesPrecios, Precio, Precios } from './precios.js';
56
+ export { panalTablonAbi } from './tablon-abi.js';
57
+ export { EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
58
+ export type { EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.js';
package/dist/index.js CHANGED
@@ -47,6 +47,9 @@ export { componerNivel, conTextoDeLaFicha, esTokenDeNivel, leerNivelDeSegmento,
47
47
  // Quién hay al otro lado: una persona o un programa. Va en la misma cadena que
48
48
  // los niveles, y por lo mismo lo tienen que reconocer todos los que la leen.
49
49
  export { esTokenDeTipo, leerTipo, leerTipoDeSegmento, tokenDeTipo } from './tipo.js';
50
+ // Los encargos de ejemplo que un agente propone al contratarle. Viven en la
51
+ // misma cadena que los niveles y el tipo, y la ficha del bot los traduce.
52
+ export { componerEjemplo, conEjemplosDeLaFicha, EJEMPLOS_EDITABLES, esTokenDeEjemplo, falloDeEjemplo, leerEjemploDeSegmento, leerEjemplos, leerEjemplosDeMetadata, MAX_EJEMPLO, } from './ejemplos.js';
50
53
  // La ficha en el idioma de quien la lee. En idiomas.ts está por qué traduce el
51
54
  // propio agente y no un servicio de Panal.
52
55
  // Unir una ruta con la URL de un agente. Ver por qué en `net.ts`: un agente
@@ -57,3 +60,11 @@ export { rutaDeAgente } from './net.js';
57
60
  // —otro lenguaje, otro runtime— necesita producir exactamente estos bytes.
58
61
  export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
59
62
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
63
+ // Cuánto vale en dólares lo que se paga en MON o en $PANAL. Solo fuera de la
64
+ // cadena: los contratos no leen precios. Ver precios.ts y BOARD.md §11.
65
+ export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares, esFresco, formatearDolares, leerPrecios, valorDeVenta, } from './precios.js';
66
+ // El tablón con reglas: PanalTablon, delante del escrow. Los métodos están en
67
+ // el cliente (`…BoardJob`); aquí, el ABI, los tipos y lo que se calcula sin la
68
+ // cadena. Ver tablon-reglas.ts y contracts/BOARD.md.
69
+ export { panalTablonAbi } from './tablon-abi.js';
70
+ export { EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Panal — cuánto vale en dólares lo que se paga en MON o en $PANAL.
3
+ * ───────────────────────────────────────────────────────────────────────────
4
+ * POR QUÉ ESTÁ AQUÍ
5
+ * Nadie piensa en MON ni en $PANAL. Quien contrata quiere saber cuánto paga, y
6
+ * un agente si el encargo le cubre lo que le cuesta: un vídeo que cuesta $1-2
7
+ * de generar no se puede hacer por 100.000 $PANAL si eso son $0,60. Ver
8
+ * contracts/BOARD.md §11.
9
+ *
10
+ * SOLO FUERA DE LA CADENA. Los contratos no leen ningún precio y las cantidades
11
+ * on-chain siguen en MON o $PANAL: lo que se bloquea es lo que cobra el agente,
12
+ * se mueva el precio o no. Los dólares son para enseñar y para decidir.
13
+ *
14
+ * DE DÓNDE SALE CADA PRECIO
15
+ *
16
+ * MON → USD Pyth en Monad, EN LA CADENA (`getPriceUnsafe`), con su
17
+ * `publishTime`. Si no responde o está viejo, CoinGecko.
18
+ * $PANAL → MON La curva de nad.fun, EN LA CADENA (`getAmountOut` del
19
+ * mercado de $PANAL). Si no responde, la API de nad.fun.
20
+ * $PANAL → USD Lo anterior multiplicado.
21
+ *
22
+ * En la cadena primero: es el mismo RPC que ya usan la web, la app y los
23
+ * agentes, y no necesita claves. La API de nad.fun solo deja entrar a
24
+ * panal.lat, así que para la app y los agentes es un respaldo que puede fallar
25
+ * por CORS; desde Node no hay CORS.
26
+ *
27
+ * DOS PRECIOS DE $PANAL
28
+ * - El de MERCADO, el que se enseña: el punto medio entre comprar y vender
29
+ * una cantidad pequeña. Coincide con el que enseña nad.fun.
30
+ * - El de VENTA (`valorDeVenta`): lo que daría vender ESA cantidad, con la
31
+ * comisión de la curva y el impacto en el precio. Para un agente es el
32
+ * honrado, porque es lo que cobraría de verdad.
33
+ *
34
+ * NUNCA UN NÚMERO VIEJO SIN DECIRLO. Cada precio lleva cuándo se publicó, y
35
+ * `enDolares` no devuelve nada si tiene más de cinco minutos.
36
+ */
37
+ import { type Address, type Hex, type PublicClient } from 'viem';
38
+ /** Pyth en Monad mainnet (contrato «pull»: alguien publica y queda guardado). */
39
+ export declare const PYTH_MONAD: Address;
40
+ /** El feed `Crypto.MON/USD` de Pyth. */
41
+ export declare const PYTH_MON_USD: Hex;
42
+ /**
43
+ * El mercado de $PANAL en la curva V2 de nad.fun. Si $PANAL se gradúa a un
44
+ * pool, `getAmountOut` deja de responder aquí y se usa la API, que sigue al
45
+ * token donde esté.
46
+ */
47
+ export declare const MERCADO_PANAL: Address;
48
+ /** $PANAL en Monad mainnet. */
49
+ export declare const PANAL_TOKEN_MAINNET: Address;
50
+ /** Pasado esto, un precio no se enseña (BOARD.md §14, decisión 11). */
51
+ export declare const PRECIO_MAX_EDAD_S: number;
52
+ export type FuentePrecio = 'pyth' | 'coingecko' | 'nadfun-curva' | 'nadfun-api';
53
+ /** Un precio con su fecha: unix en segundos de cuando se publicó. */
54
+ export interface Precio {
55
+ valor: number;
56
+ actualizado: number;
57
+ fuente: FuentePrecio;
58
+ }
59
+ export interface Precios {
60
+ /** Dólares por MON. */
61
+ mon: Precio | null;
62
+ /** MON por $PANAL, a precio de mercado. */
63
+ panalEnMon: Precio | null;
64
+ /** Dólares por $PANAL: los dos anteriores multiplicados, con la fecha del más viejo. */
65
+ panal: Precio | null;
66
+ }
67
+ export interface OpcionesPrecios {
68
+ /** Para las pruebas y para entornos sin `fetch` global. */
69
+ fetch?: typeof fetch;
70
+ /** Ahora, en segundos. Para las pruebas. */
71
+ ahora?: number;
72
+ /** Si no se pasa, el de mainnet. */
73
+ panalToken?: Address;
74
+ /** Cuánto esperar a cada fuente HTTP. */
75
+ timeoutMs?: number;
76
+ }
77
+ type Lector = Pick<PublicClient, 'readContract'>;
78
+ /**
79
+ * Lee los precios de MON y de $PANAL.
80
+ *
81
+ * Nunca lanza: si una fuente cae se usa la otra, y si caen las dos ese precio
82
+ * queda en `null`. Un precio viejo SÍ se devuelve, con su fecha: quien enseña
83
+ * decide con `esFresco`, y `enDolares` ya lo hace.
84
+ */
85
+ export declare function leerPrecios(lector: Lector, opciones?: OpcionesPrecios): Promise<Precios>;
86
+ /** Si un precio se puede enseñar: existe y tiene menos de cinco minutos. */
87
+ export declare function esFresco(p: Precio | null | undefined, ahora?: number, maxEdadS?: number): p is Precio;
88
+ /** Las monedas que tienen precio aquí. Las estables (GHO, USDC, AUSD) ya son dólares. */
89
+ export type MonedaConPrecio = 'MON' | '$PANAL';
90
+ /**
91
+ * Cuántos dólares son `cantidad` unidades mínimas de `moneda` (18 decimales),
92
+ * con el precio de mercado. `null` si no hay precio o tiene más de 5 minutos.
93
+ */
94
+ export declare function enDolares(cantidad: bigint, moneda: MonedaConPrecio, precios: Precios | null | undefined, ahora?: number): {
95
+ usd: number;
96
+ actualizado: number;
97
+ fuente: FuentePrecio;
98
+ } | null;
99
+ /**
100
+ * Lo que daría vender `cantidad` $PANAL ahora en la curva, en MON (wei): con
101
+ * la comisión y el impacto en el precio. Es lo que de verdad cobraría quien
102
+ * los vendiera. `null` si la curva no responde.
103
+ */
104
+ export declare function valorDeVenta(lector: Lector, cantidad: bigint, panalToken?: Address): Promise<bigint | null>;
105
+ /**
106
+ * «$0.075», «$0.0006», «$1.50». Por debajo de un dólar, dos cifras
107
+ * significativas: con decimales fijos, 100 $PANAL saldrían «$0.00».
108
+ */
109
+ export declare function formatearDolares(usd: number, locale?: string): string;
110
+ export {};
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Panal — cuánto vale en dólares lo que se paga en MON o en $PANAL.
3
+ * ───────────────────────────────────────────────────────────────────────────
4
+ * POR QUÉ ESTÁ AQUÍ
5
+ * Nadie piensa en MON ni en $PANAL. Quien contrata quiere saber cuánto paga, y
6
+ * un agente si el encargo le cubre lo que le cuesta: un vídeo que cuesta $1-2
7
+ * de generar no se puede hacer por 100.000 $PANAL si eso son $0,60. Ver
8
+ * contracts/BOARD.md §11.
9
+ *
10
+ * SOLO FUERA DE LA CADENA. Los contratos no leen ningún precio y las cantidades
11
+ * on-chain siguen en MON o $PANAL: lo que se bloquea es lo que cobra el agente,
12
+ * se mueva el precio o no. Los dólares son para enseñar y para decidir.
13
+ *
14
+ * DE DÓNDE SALE CADA PRECIO
15
+ *
16
+ * MON → USD Pyth en Monad, EN LA CADENA (`getPriceUnsafe`), con su
17
+ * `publishTime`. Si no responde o está viejo, CoinGecko.
18
+ * $PANAL → MON La curva de nad.fun, EN LA CADENA (`getAmountOut` del
19
+ * mercado de $PANAL). Si no responde, la API de nad.fun.
20
+ * $PANAL → USD Lo anterior multiplicado.
21
+ *
22
+ * En la cadena primero: es el mismo RPC que ya usan la web, la app y los
23
+ * agentes, y no necesita claves. La API de nad.fun solo deja entrar a
24
+ * panal.lat, así que para la app y los agentes es un respaldo que puede fallar
25
+ * por CORS; desde Node no hay CORS.
26
+ *
27
+ * DOS PRECIOS DE $PANAL
28
+ * - El de MERCADO, el que se enseña: el punto medio entre comprar y vender
29
+ * una cantidad pequeña. Coincide con el que enseña nad.fun.
30
+ * - El de VENTA (`valorDeVenta`): lo que daría vender ESA cantidad, con la
31
+ * comisión de la curva y el impacto en el precio. Para un agente es el
32
+ * honrado, porque es lo que cobraría de verdad.
33
+ *
34
+ * NUNCA UN NÚMERO VIEJO SIN DECIRLO. Cada precio lleva cuándo se publicó, y
35
+ * `enDolares` no devuelve nada si tiene más de cinco minutos.
36
+ */
37
+ import { formatUnits, parseEther } from 'viem';
38
+ /** Pyth en Monad mainnet (contrato «pull»: alguien publica y queda guardado). */
39
+ export const PYTH_MONAD = '0x2880aB155794e7179c9eE2e38200202908C17B43';
40
+ /** El feed `Crypto.MON/USD` de Pyth. */
41
+ export const PYTH_MON_USD = '0x31491744e2dbf6df7fcf4ac0820d18a609b49076d45066d3568424e62f686cd1';
42
+ /**
43
+ * El mercado de $PANAL en la curva V2 de nad.fun. Si $PANAL se gradúa a un
44
+ * pool, `getAmountOut` deja de responder aquí y se usa la API, que sigue al
45
+ * token donde esté.
46
+ */
47
+ export const MERCADO_PANAL = '0x9f3832732923252A21044F21eE6bd87F09514ae4';
48
+ /** $PANAL en Monad mainnet. */
49
+ export const PANAL_TOKEN_MAINNET = '0x2e2e44e7fa6178822d4397299f719e89d1a67777';
50
+ /** Pasado esto, un precio no se enseña (BOARD.md §14, decisión 11). */
51
+ export const PRECIO_MAX_EDAD_S = 5 * 60;
52
+ const COINGECKO_MON = 'https://api.coingecko.com/api/v3/simple/price?ids=monad&vs_currencies=usd&include_last_updated_at=true';
53
+ const NADFUN_MERCADO = 'https://api.nad.fun/agent/market/';
54
+ const pythAbi = [
55
+ {
56
+ type: 'function',
57
+ name: 'getPriceUnsafe',
58
+ stateMutability: 'view',
59
+ inputs: [{ name: 'id', type: 'bytes32' }],
60
+ outputs: [
61
+ {
62
+ type: 'tuple',
63
+ components: [
64
+ { name: 'price', type: 'int64' },
65
+ { name: 'conf', type: 'uint64' },
66
+ { name: 'expo', type: 'int32' },
67
+ { name: 'publishTime', type: 'uint256' },
68
+ ],
69
+ },
70
+ ],
71
+ },
72
+ ];
73
+ const curvaAbi = [
74
+ {
75
+ type: 'function',
76
+ name: 'getAmountOut',
77
+ stateMutability: 'view',
78
+ inputs: [
79
+ { name: 'token', type: 'address' },
80
+ { name: 'amountIn', type: 'uint256' },
81
+ { name: 'isBuy', type: 'bool' },
82
+ ],
83
+ outputs: [{ type: 'uint256' }],
84
+ },
85
+ ];
86
+ const ahoraS = (o) => o?.ahora ?? Math.floor(Date.now() / 1000);
87
+ async function getJson(url, o) {
88
+ const f = o?.fetch ?? fetch;
89
+ const r = await f(url, { signal: AbortSignal.timeout(o?.timeoutMs ?? 6000) });
90
+ if (!r.ok)
91
+ throw new Error(`HTTP ${r.status}`);
92
+ return r.json();
93
+ }
94
+ const numero = (v) => {
95
+ const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN;
96
+ return Number.isFinite(n) && n > 0 ? n : null;
97
+ };
98
+ /** MON → USD de Pyth en la cadena. */
99
+ async function monDePyth(lector) {
100
+ try {
101
+ const p = (await lector.readContract({
102
+ address: PYTH_MONAD,
103
+ abi: pythAbi,
104
+ functionName: 'getPriceUnsafe',
105
+ args: [PYTH_MON_USD],
106
+ }));
107
+ const valor = Number(p.price) * 10 ** p.expo;
108
+ return valor > 0 ? { valor, actualizado: Number(p.publishTime), fuente: 'pyth' } : null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /** MON → USD de CoinGecko. */
115
+ async function monDeCoingecko(o) {
116
+ try {
117
+ const j = (await getJson(COINGECKO_MON, o));
118
+ const valor = numero(j.monad?.usd);
119
+ const actualizado = numero(j.monad?.last_updated_at);
120
+ return valor && actualizado ? { valor, actualizado, fuente: 'coingecko' } : null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * $PANAL → MON en la curva: el punto medio entre comprar con 1 MON y vender
128
+ * 1.000 $PANAL. Las dos cantidades son ínfimas frente a las reservas, así que
129
+ * el impacto no cuenta y la media geométrica quita la comisión de la curva.
130
+ */
131
+ async function panalDeCurva(lector, token, ahora) {
132
+ try {
133
+ const [comprados, monVenta] = await Promise.all([
134
+ lector.readContract({
135
+ address: MERCADO_PANAL,
136
+ abi: curvaAbi,
137
+ functionName: 'getAmountOut',
138
+ args: [token, parseEther('1'), true],
139
+ }),
140
+ lector.readContract({
141
+ address: MERCADO_PANAL,
142
+ abi: curvaAbi,
143
+ functionName: 'getAmountOut',
144
+ args: [token, parseEther('1000'), false],
145
+ }),
146
+ ]);
147
+ const compra = 1 / Number(formatUnits(comprados, 18)); // MON por $PANAL al comprar
148
+ const venta = Number(formatUnits(monVenta, 18)) / 1000; // MON por $PANAL al vender
149
+ const valor = Math.sqrt(compra * venta);
150
+ return Number.isFinite(valor) && valor > 0 ? { valor, actualizado: ahora, fuente: 'nadfun-curva' } : null;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ /** $PANAL → MON de la API de nad.fun. */
157
+ async function panalDeApi(token, ahora, o) {
158
+ try {
159
+ const j = (await getJson(`${NADFUN_MERCADO}${token}`, o));
160
+ const valor = numero(j.market_info?.price_native ?? j.price_native);
161
+ return valor ? { valor, actualizado: ahora, fuente: 'nadfun-api' } : null;
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
167
+ /**
168
+ * Lee los precios de MON y de $PANAL.
169
+ *
170
+ * Nunca lanza: si una fuente cae se usa la otra, y si caen las dos ese precio
171
+ * queda en `null`. Un precio viejo SÍ se devuelve, con su fecha: quien enseña
172
+ * decide con `esFresco`, y `enDolares` ya lo hace.
173
+ */
174
+ export async function leerPrecios(lector, opciones) {
175
+ const ahora = ahoraS(opciones);
176
+ const token = opciones?.panalToken ?? PANAL_TOKEN_MAINNET;
177
+ const [pyth, curva] = await Promise.all([monDePyth(lector), panalDeCurva(lector, token, ahora)]);
178
+ // CoinGecko solo si Pyth no responde o se ha quedado viejo.
179
+ const mon = pyth && esFresco(pyth, ahora) ? pyth : masReciente(pyth, await monDeCoingecko(opciones));
180
+ const panalEnMon = curva ?? (await panalDeApi(token, ahora, opciones));
181
+ const panal = mon && panalEnMon
182
+ ? {
183
+ valor: mon.valor * panalEnMon.valor,
184
+ actualizado: Math.min(mon.actualizado, panalEnMon.actualizado),
185
+ fuente: panalEnMon.fuente,
186
+ }
187
+ : null;
188
+ return { mon, panalEnMon, panal };
189
+ }
190
+ function masReciente(a, b) {
191
+ if (!a)
192
+ return b;
193
+ if (!b)
194
+ return a;
195
+ return b.actualizado > a.actualizado ? b : a;
196
+ }
197
+ /** Si un precio se puede enseñar: existe y tiene menos de cinco minutos. */
198
+ export function esFresco(p, ahora, maxEdadS = PRECIO_MAX_EDAD_S) {
199
+ if (!p)
200
+ return false;
201
+ return (ahora ?? Math.floor(Date.now() / 1000)) - p.actualizado <= maxEdadS;
202
+ }
203
+ /**
204
+ * Cuántos dólares son `cantidad` unidades mínimas de `moneda` (18 decimales),
205
+ * con el precio de mercado. `null` si no hay precio o tiene más de 5 minutos.
206
+ */
207
+ export function enDolares(cantidad, moneda, precios, ahora) {
208
+ const p = moneda === 'MON' ? precios?.mon : precios?.panal;
209
+ if (!esFresco(p, ahora))
210
+ return null;
211
+ return { usd: Number(formatUnits(cantidad, 18)) * p.valor, actualizado: p.actualizado, fuente: p.fuente };
212
+ }
213
+ /**
214
+ * Lo que daría vender `cantidad` $PANAL ahora en la curva, en MON (wei): con
215
+ * la comisión y el impacto en el precio. Es lo que de verdad cobraría quien
216
+ * los vendiera. `null` si la curva no responde.
217
+ */
218
+ export async function valorDeVenta(lector, cantidad, panalToken = PANAL_TOKEN_MAINNET) {
219
+ if (cantidad <= 0n)
220
+ return 0n;
221
+ try {
222
+ return (await lector.readContract({
223
+ address: MERCADO_PANAL,
224
+ abi: curvaAbi,
225
+ functionName: 'getAmountOut',
226
+ args: [panalToken, cantidad, false],
227
+ }));
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ /**
234
+ * «$0.075», «$0.0006», «$1.50». Por debajo de un dólar, dos cifras
235
+ * significativas: con decimales fijos, 100 $PANAL saldrían «$0.00».
236
+ */
237
+ export function formatearDolares(usd, locale = 'en-US') {
238
+ if (!Number.isFinite(usd) || usd < 0)
239
+ return '';
240
+ if (usd > 0 && usd < 0.000001) {
241
+ return `< ${new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD', maximumFractionDigits: 6 }).format(0.000001)}`;
242
+ }
243
+ const opciones = usd >= 1
244
+ ? { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }
245
+ : { style: 'currency', currency: 'USD', maximumSignificantDigits: 2 };
246
+ return new Intl.NumberFormat(locale, opciones).format(usd);
247
+ }