@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.
- package/README.md +69 -4
- package/dist/agent-card.d.ts +11 -2
- package/dist/chains.d.ts +7 -0
- package/dist/chains.js +2 -0
- package/dist/client.d.ts +141 -0
- package/dist/client.js +417 -1
- package/dist/ejemplos.d.ts +97 -0
- package/dist/ejemplos.js +160 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +11 -0
- package/dist/precios.d.ts +110 -0
- package/dist/precios.js +247 -0
- package/dist/tablon-abi.d.ts +1271 -0
- package/dist/tablon-abi.js +1629 -0
- package/dist/tablon-reglas.d.ts +163 -0
- package/dist/tablon-reglas.js +188 -0
- package/dist/types.js +5 -0
- package/package.json +2 -2
package/dist/ejemplos.js
ADDED
|
@@ -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 {};
|
package/dist/precios.js
ADDED
|
@@ -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
|
+
}
|