@panal/sdk 0.20.0 → 0.22.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/dist/client.js CHANGED
@@ -15,13 +15,16 @@
15
15
  * Sin configuración apunta a Monad mainnet, que es donde Panal está desplegado
16
16
  * y en uso: el caso de "quiero probar esto ahora" no debería exigir un .env.
17
17
  */
18
- import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, toBytes, verifyMessage } from 'viem';
18
+ import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, parseEventLogs, toBytes, verifyMessage } from 'viem';
19
19
  import { erc20Abi, escrowAbi, namesAbi, registryAbi } from './abis.js';
20
20
  import { leerX402 } from './agent-card.js';
21
21
  import { assertPublicUrl, fetchLimited, rutaDeAgente } from './net.js';
22
22
  import { X402Error, payAndAsk, quoteAsk } from './x402.js';
23
23
  import { descend, newEnvelope, remainingBudget } from './envelope.js';
24
24
  import { NATIVE_CURRENCY, addressesFor, chainFor } from './chains.js';
25
+ import { leerPrecios, valorDeVenta } from './precios.js';
26
+ import { panalTablonAbi } from './tablon-abi.js';
27
+ import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
25
28
  import { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
26
29
  import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js';
27
30
  /**
@@ -36,6 +39,13 @@ const TOPE_GAS_RETIRADA = 300000n;
36
39
  * lo da `eth_estimateGas`, que es fiable; el que inflaba era `eth_fillTransaction`.
37
40
  */
38
41
  const TOPE_GAS_FICHA = 30000000n;
42
+ /**
43
+ * El tope de gas de lo que se firma en PanalTablon. Coger cuesta ~330.000 y
44
+ * aprobar ~230.000; elegir borra hasta 50 ofertas y `autoAssign` las recorre,
45
+ * así que el techo es alto. Como en los demás: solo corta un disparate, y lo
46
+ * que se firma es la estimación + 10 %.
47
+ */
48
+ const TOPE_GAS_TABLON = 3000000n;
39
49
  /** Cuántos agentes se leen por llamada al registry. */
40
50
  const REGISTRY_PAGE = 50n;
41
51
  /** Tope duro de agentes recorridos, por si el registro crece mucho. */
@@ -454,6 +464,26 @@ export class PanalClient {
454
464
  args: [currency, getAddress(account)],
455
465
  }));
456
466
  }
467
+ /**
468
+ * Cuánto valen hoy MON y $PANAL en dólares, cada precio con su fecha.
469
+ * Solo en mainnet: en testnet no hay precios que valgan.
470
+ * Ver precios.ts; para convertir una cantidad, `enDolares`.
471
+ */
472
+ async precios(opciones) {
473
+ if (this.network !== 'mainnet')
474
+ return { mon: null, panalEnMon: null, panal: null };
475
+ return leerPrecios(this.publicClient, { panalToken: this.addresses.panalToken, ...opciones });
476
+ }
477
+ /**
478
+ * Lo que daría vender `cantidad` $PANAL ahora en la curva de nad.fun, en MON
479
+ * (wei), con comisión e impacto. Es lo que de verdad cobraría un agente que
480
+ * los vendiera; `null` si la curva no responde.
481
+ */
482
+ async valorDeVentaPanal(cantidad) {
483
+ if (this.network !== 'mainnet')
484
+ return null;
485
+ return valorDeVenta(this.publicClient, cantidad, this.addresses.panalToken);
486
+ }
457
487
  // -------------------------------------------------------------------------
458
488
  // Escritura
459
489
  // -------------------------------------------------------------------------
@@ -1124,6 +1154,392 @@ export class PanalClient {
1124
1154
  return hash;
1125
1155
  }
1126
1156
  /** Comprueba el saldo antes de firmar, para fallar con un mensaje legible. */
1157
+ // -------------------------------------------------------------------------
1158
+ // El tablón con reglas (PanalTablon). Ver tablon-reglas.ts y BOARD.md.
1159
+ // -------------------------------------------------------------------------
1160
+ /** Si PanalTablon existe en esta red. Hasta que se despliegue, no. */
1161
+ get boardJobsAvailable() {
1162
+ return this.addresses.tablon !== NATIVE_CURRENCY;
1163
+ }
1164
+ /** La dirección de PanalTablon, o un error que dice que aún no existe. */
1165
+ tablon() {
1166
+ if (!this.boardJobsAvailable) {
1167
+ throw new Error(`PanalTablon todavía no está desplegado en ${this.network}. Para probarlo, pasa su dirección en \`addresses.tablon\`.`);
1168
+ }
1169
+ return this.addresses.tablon;
1170
+ }
1171
+ leerTablon(functionName, args = []) {
1172
+ return this.publicClient.readContract({
1173
+ address: this.tablon(),
1174
+ abi: panalTablonAbi,
1175
+ functionName,
1176
+ args,
1177
+ });
1178
+ }
1179
+ async getBoardJob(jobId) {
1180
+ return encargoDeLaCadena(jobId, await this.leerTablon('getJob', [jobId]));
1181
+ }
1182
+ async getBoardJobCount() {
1183
+ return this.leerTablon('jobCount');
1184
+ }
1185
+ /**
1186
+ * Los últimos encargos, del más nuevo al más viejo; por defecto solo los
1187
+ * publicados. Se leen de diez en diez: el RPC público limita.
1188
+ */
1189
+ async listBoardJobs(opciones = {}) {
1190
+ const total = await this.getBoardJobCount();
1191
+ const cuantos = BigInt(Math.max(1, Math.min(opciones.limit ?? 50, 200)));
1192
+ const ids = [];
1193
+ for (let i = total - 1n; i >= 0n && i >= total - cuantos; i--)
1194
+ ids.push(i);
1195
+ const out = [];
1196
+ for (let k = 0; k < ids.length; k += 10) {
1197
+ out.push(...(await Promise.all(ids.slice(k, k + 10).map((id) => this.getBoardJob(id)))));
1198
+ }
1199
+ const estado = opciones.estado ?? EstadoTablon.Publicado;
1200
+ return estado === 'todos' ? out : out.filter((j) => j.estado === estado);
1201
+ }
1202
+ async getBoardOffers(jobId) {
1203
+ const lista = await this.leerTablon('offers', [jobId]);
1204
+ return lista.map(ofertaDeLaCadena);
1205
+ }
1206
+ /** Si un agente puede coger (inmediato) u ofertar (selección) ahora, y por qué no. */
1207
+ async canTakeBoardJob(agent, jobId) {
1208
+ const [ok, motivo] = await this.leerTablon('canTake', [getAddress(agent), jobId]);
1209
+ return { ok, motivo };
1210
+ }
1211
+ /** Su nivel, lo que le queda hoy, si está suspendido y sus categorías. */
1212
+ async boardStanding(agent) {
1213
+ const a = getAddress(agent);
1214
+ const [[nivel, cupo], stats, novato, mascara] = await Promise.all([
1215
+ this.leerTablon('levelOf', [a]),
1216
+ this.leerTablon('agentStats', [a]),
1217
+ this.leerTablon('newcomerJobs'),
1218
+ this.leerTablon('categoryMask', [a]),
1219
+ ]);
1220
+ return situacionDe({
1221
+ nivel: Number(nivel),
1222
+ cupoDiario: Number(cupo),
1223
+ stats: {
1224
+ day: Number(stats[0]),
1225
+ takenToday: Number(stats[1]),
1226
+ open: Number(stats[2]),
1227
+ newcomerUsed: Number(stats[3]),
1228
+ suspendedUntil: Number(stats[4]),
1229
+ },
1230
+ encargosDeNovato: Number(novato),
1231
+ mascara,
1232
+ }, Math.floor(Date.now() / 1000));
1233
+ }
1234
+ /** Lo que PanalTablon le debe a un cliente en una moneda: se cobra con `claimBoardRefund`. */
1235
+ async getBoardRefund(client, currency = NATIVE_CURRENCY) {
1236
+ return this.leerTablon('owed', [getAddress(client), currency]);
1237
+ }
1238
+ /**
1239
+ * Firma en PanalTablon con el gas fijado y espera a que salga bien. Si el
1240
+ * contrato lo rechazaría, la estimación ya falla con el error por su nombre
1241
+ * (`NotAllowed`, `OverTotalCap`…) y no se envía nada.
1242
+ */
1243
+ async firmarEnTablon(que, functionName, args, value = 0n) {
1244
+ const wallet = this.wallet();
1245
+ const llamada = { address: this.tablon(), abi: panalTablonAbi, functionName, args, value };
1246
+ await this.comprobarReserva(que, llamada);
1247
+ const hash = await wallet.writeContract({
1248
+ ...llamada,
1249
+ gas: await this.gasFijo(que, llamada, TOPE_GAS_TABLON),
1250
+ chain: chainFor(this.network),
1251
+ account: this.account,
1252
+ });
1253
+ const recibo = await this.publicClient.waitForTransactionReceipt({ hash });
1254
+ if (recibo.status !== 'success')
1255
+ throw new Error(`${que} revirtió (tx ${hash}): se cobró el gas y no cambió nada.`);
1256
+ return { hash, recibo };
1257
+ }
1258
+ /** `approve` exacto a PanalTablon para un pago en token. Nunca infinito. */
1259
+ async aprobarAlTablon(token, importe) {
1260
+ const aFirmar = { address: token, abi: erc20Abi, functionName: 'approve', args: [this.tablon(), importe] };
1261
+ const hash = await this.wallet().writeContract({
1262
+ ...aFirmar,
1263
+ gas: await this.gasFijo('El approve', aFirmar),
1264
+ chain: chainFor(this.network),
1265
+ account: this.account,
1266
+ });
1267
+ await this.esperarExito(hash, 'El approve');
1268
+ }
1269
+ /**
1270
+ * Publica un encargo en el tablón con reglas. El encargo y el anuncio NO se
1271
+ * suben aquí: a la cadena va su keccak256, y los textos se dejan en el
1272
+ * buzón, que los comprueba contra esos hashes.
1273
+ */
1274
+ async postBoardJob(p) {
1275
+ const problema = problemaAlPublicar(p);
1276
+ if (problema)
1277
+ throw new Error(problema);
1278
+ const moneda = p.moneda ?? NATIVE_CURRENCY;
1279
+ const nativa = moneda.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
1280
+ await this.assertFunds(moneda, p.importe, nativa);
1281
+ const taskHash = keccak256(toBytes(p.encargo));
1282
+ const advertHash = keccak256(toBytes(p.anuncio));
1283
+ const seleccion = p.modo === ModoTablon.Seleccion;
1284
+ const params = {
1285
+ taskHash,
1286
+ advertHash,
1287
+ category: p.categoria,
1288
+ currency: moneda,
1289
+ amount: p.importe,
1290
+ workDuration: BigInt(p.plazoEntregaS),
1291
+ expiresAt: BigInt(Math.floor(Date.now() / 1000) + p.caducaEnS),
1292
+ mode: p.modo,
1293
+ applyWindow: BigInt(seleccion ? (p.ventanaOfertasS ?? 0) : 0),
1294
+ chooseWindow: BigInt(seleccion ? (p.ventanaEleccionS ?? 0) : 0),
1295
+ experiencedOnly: p.soloConExperiencia ?? false,
1296
+ relist: p.republicar ?? false,
1297
+ campaignId: p.campana?.id ?? `0x${'0'.repeat(64)}`,
1298
+ campaignCapPerAgent: p.campana?.topePorAgente ?? 0,
1299
+ };
1300
+ if (!nativa)
1301
+ await this.aprobarAlTablon(moneda, p.importe);
1302
+ const { hash, recibo } = await this.firmarEnTablon('La publicación', 'post', [params], nativa ? p.importe : 0n);
1303
+ const [ev] = parseEventLogs({ abi: panalTablonAbi, logs: recibo.logs, eventName: 'JobPosted' });
1304
+ if (!ev)
1305
+ throw new Error(`La publicación salió (tx ${hash}) pero no se encontró el evento JobPosted.`);
1306
+ const jobId = ev.args.jobId;
1307
+ // Los textos van DESPUÉS de la cadena: hasta que el encargo existe, el buzón
1308
+ // no tiene hash con qué comprobarlos. Si fallan, el encargo está pagado y
1309
+ // sin anunciar: se dice y se reintenta con `publishBoardJobTexts`.
1310
+ try {
1311
+ await this.publishBoardJobTexts(jobId, { anuncio: p.anuncio, encargo: p.encargo });
1312
+ return { jobId, txHash: hash, taskHash, advertHash, textosGuardados: true };
1313
+ }
1314
+ catch (err) {
1315
+ const error = err instanceof Error ? err.message : String(err);
1316
+ return { jobId, txHash: hash, taskHash, advertHash, textosGuardados: false, error };
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Deja en el buzón el anuncio y el encargo de un encargo ya publicado. Sin
1321
+ * firma: el buzón solo acepta los textos que dan los hashes de la cadena.
1322
+ * Repetirlo no molesta.
1323
+ */
1324
+ async publishBoardJobTexts(jobId, textos) {
1325
+ const base = `${this.buzonUrl}/${this.tablon()}`;
1326
+ for (const [ruta, campo, texto] of [
1327
+ ['anuncio', 'anuncio', textos.anuncio],
1328
+ ['brief', 'brief', textos.encargo],
1329
+ ]) {
1330
+ const { status, text } = await fetchLimited(`${base}/${ruta}/${jobId}`, {
1331
+ method: 'POST',
1332
+ headers: { 'content-type': 'application/json' },
1333
+ body: JSON.stringify({ [campo]: texto }),
1334
+ timeoutMs: 15_000,
1335
+ maxBytes: 10_000,
1336
+ });
1337
+ if (status !== 200) {
1338
+ throw new Error(`El buzón no aceptó el ${campo === 'brief' ? 'encargo' : 'anuncio'} del #${jobId} (${status}: ${text.slice(0, 160)}).`);
1339
+ }
1340
+ }
1341
+ }
1342
+ /** Lo que se firma para leer del buzón: dirección, firma y caducidad. */
1343
+ async credencialesDeBuzon(mensaje) {
1344
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
1345
+ const firma = await this.wallet().signMessage({ account: this.account, message: mensaje(expira) });
1346
+ return { 'x-panal-address': this.account.address, 'x-panal-signature': firma, 'x-panal-expira': String(expira) };
1347
+ }
1348
+ /**
1349
+ * El anuncio de cada encargo publicado, comprobado contra su `advertHash`.
1350
+ * Un anuncio que no cuadre no se devuelve: el buzón no puede inventarse uno.
1351
+ */
1352
+ async listBoardJobAdverts(opciones = {}) {
1353
+ const [encargos, respuesta] = await Promise.all([
1354
+ this.listBoardJobs({ limit: opciones.limit }),
1355
+ fetchLimited(`${this.buzonUrl}/${this.tablon()}/lista`, { timeoutMs: 15_000, maxBytes: 2_000_000 }),
1356
+ ]);
1357
+ if (respuesta.status !== 200)
1358
+ throw new Error(`El buzón respondió ${respuesta.status} al pedir los anuncios.`);
1359
+ const { anuncios = [] } = JSON.parse(respuesta.text);
1360
+ const porId = new Map(anuncios.map((a) => [a.jobId, a.anuncio]));
1361
+ return encargos.flatMap((j) => {
1362
+ const anuncio = porId.get(j.id.toString());
1363
+ if (typeof anuncio !== 'string')
1364
+ return [];
1365
+ if (keccak256(toBytes(anuncio)).toLowerCase() !== j.advertHash.toLowerCase())
1366
+ return [];
1367
+ return [{ ...j, anuncio }];
1368
+ });
1369
+ }
1370
+ /**
1371
+ * El encargo privado. Lo leen su cliente y su agente en cuanto lo tiene, y
1372
+ * se comprueba contra el `taskHash` de la cadena antes de devolverlo.
1373
+ */
1374
+ async readBoardJobBrief(jobId) {
1375
+ const job = await this.getBoardJob(jobId);
1376
+ const yo = this.account?.address.toLowerCase();
1377
+ if (yo !== job.cliente.toLowerCase() && yo !== job.agente.toLowerCase()) {
1378
+ throw new Error(`El encargo #${jobId} solo lo leen su cliente y el agente que lo tenga.`);
1379
+ }
1380
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/encargo/${jobId}`, {
1381
+ headers: await this.credencialesDeBuzon((e) => tablonEncargoSignMessage(jobId, e)),
1382
+ timeoutMs: 15_000,
1383
+ maxBytes: 1_000_000,
1384
+ });
1385
+ if (status === 404)
1386
+ throw new Error(`El buzón no tiene el encargo #${jobId}: su cliente no llegó a dejarlo.`);
1387
+ if (status !== 200)
1388
+ throw new Error(`El buzón respondió ${status} al pedir el encargo #${jobId}.`);
1389
+ const { brief } = JSON.parse(text);
1390
+ if (typeof brief !== 'string' || keccak256(toBytes(brief)).toLowerCase() !== job.taskHash.toLowerCase()) {
1391
+ throw new Error(`El encargo #${jobId} del buzón no cuadra con el taskHash de la cadena: no se trabaja sobre él.`);
1392
+ }
1393
+ return brief;
1394
+ }
1395
+ /**
1396
+ * Entregar un encargo del tablón con reglas: se deja en el buzón y DESPUÉS
1397
+ * se ancla su hash en el escrow. Al revés, un fallo del buzón dejaría al
1398
+ * cliente con una entrega anclada que no puede descargar.
1399
+ */
1400
+ async deliverBoardJob(jobId, texto) {
1401
+ this.wallet();
1402
+ const job = await this.getBoardJob(jobId);
1403
+ if (job.agente.toLowerCase() !== this.account.address.toLowerCase()) {
1404
+ throw new Error(`El encargo #${jobId} lo tiene ${job.agente}, no tú.`);
1405
+ }
1406
+ if (job.estado !== EstadoTablon.Asignado)
1407
+ throw new Error(`El encargo #${jobId} no está asignado: no hay nada que entregar.`);
1408
+ const hashEntrega = keccak256(toBytes(texto));
1409
+ const cabeceras = await this.credencialesDeBuzon((e) => tablonEntregaSignMessage(jobId, hashEntrega, e));
1410
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/entrega/${jobId}`, {
1411
+ method: 'POST',
1412
+ headers: { 'content-type': 'application/json' },
1413
+ body: JSON.stringify({
1414
+ entrega: texto,
1415
+ address: cabeceras['x-panal-address'],
1416
+ signature: cabeceras['x-panal-signature'],
1417
+ expira: Number(cabeceras['x-panal-expira']),
1418
+ }),
1419
+ timeoutMs: 30_000,
1420
+ maxBytes: 100_000,
1421
+ });
1422
+ if (status !== 200) {
1423
+ throw new Error(`El buzón no aceptó la entrega del #${jobId} (${status}: ${text.slice(0, 160)}). No se ha anclado nada.`);
1424
+ }
1425
+ return this.deliverResult(job.escrowTaskId, texto);
1426
+ }
1427
+ /**
1428
+ * La entrega, para su cliente. Se comprueba contra el `resultHash` anclado
1429
+ * en el escrow: el buzón no puede cambiarla.
1430
+ */
1431
+ async readBoardJobResult(jobId) {
1432
+ const job = await this.getBoardJob(jobId);
1433
+ if (this.account?.address.toLowerCase() !== job.cliente.toLowerCase()) {
1434
+ throw new Error(`La entrega del #${jobId} solo la descarga su cliente.`);
1435
+ }
1436
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/result/${jobId}`, {
1437
+ headers: await this.credencialesDeBuzon((e) => tablonResultadoSignMessage(jobId, e)),
1438
+ timeoutMs: 15_000,
1439
+ maxBytes: 1_000_000,
1440
+ });
1441
+ if (status === 404)
1442
+ throw new Error(`El encargo #${jobId} todavía no tiene entrega en el buzón.`);
1443
+ if (status !== 200)
1444
+ throw new Error(`El buzón respondió ${status} al pedir la entrega del #${jobId}.`);
1445
+ const { resultText } = JSON.parse(text);
1446
+ const tarea = await this.getTask(job.escrowTaskId);
1447
+ if (typeof resultText !== 'string' || keccak256(toBytes(resultText)).toLowerCase() !== tarea.resultHash.toLowerCase()) {
1448
+ throw new Error(`La entrega del #${jobId} no cuadra con el resultHash anclado en el escrow.`);
1449
+ }
1450
+ return resultText;
1451
+ }
1452
+ /** Coger un encargo inmediato. Antes de firmar pregunta si se puede, y por qué no. */
1453
+ async takeBoardJob(jobId) {
1454
+ this.wallet();
1455
+ const { ok, motivo } = await this.canTakeBoardJob(this.account.address, jobId);
1456
+ if (!ok)
1457
+ throw new Error(`No se puede coger el encargo #${jobId}: ${motivo}.`);
1458
+ const { hash } = await this.firmarEnTablon('Coger el encargo', 'take', [jobId]);
1459
+ return { txHash: hash, escrowTaskId: (await this.getBoardJob(jobId)).escrowTaskId };
1460
+ }
1461
+ /**
1462
+ * Ofertar en un encargo de selección: precio y plazo (0 = el del encargo).
1463
+ * Volver a llamar cambia la oferta. Un precio por encima del presupuesto vale:
1464
+ * solo lo puede elegir el cliente, poniendo la diferencia.
1465
+ */
1466
+ async offerOnBoardJob(jobId, precio, plazoEntregaS = 0) {
1467
+ this.wallet();
1468
+ const { ok, motivo } = await this.canTakeBoardJob(this.account.address, jobId);
1469
+ if (!ok)
1470
+ throw new Error(`No se puede ofertar en el encargo #${jobId}: ${motivo}.`);
1471
+ return (await this.firmarEnTablon('La oferta', 'applyFor', [jobId, precio, BigInt(plazoEntregaS)])).hash;
1472
+ }
1473
+ async withdrawBoardOffer(jobId) {
1474
+ return (await this.firmarEnTablon('Retirar la oferta', 'withdrawOffer', [jobId])).hash;
1475
+ }
1476
+ /**
1477
+ * El cliente elige una oferta. Si pide más que el presupuesto, se manda la
1478
+ * diferencia exacta (en $PANAL, con su `approve` exacto); si pide menos, la
1479
+ * diferencia queda para cobrar con `claimBoardRefund`.
1480
+ */
1481
+ async chooseBoardOffer(jobId, agent) {
1482
+ const [job, ofertas] = await Promise.all([this.getBoardJob(jobId), this.getBoardOffers(jobId)]);
1483
+ const oferta = ofertas.find((o) => o.agente.toLowerCase() === agent.toLowerCase());
1484
+ if (!oferta)
1485
+ throw new Error(`${agent} no tiene oferta en el encargo #${jobId}.`);
1486
+ const diferencia = oferta.precio > job.importe ? oferta.precio - job.importe : 0n;
1487
+ const nativa = job.moneda.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
1488
+ if (diferencia > 0n) {
1489
+ await this.assertFunds(job.moneda, diferencia, nativa);
1490
+ if (!nativa)
1491
+ await this.aprobarAlTablon(job.moneda, diferencia);
1492
+ }
1493
+ const { hash } = await this.firmarEnTablon('Elegir la oferta', 'choose', [jobId, getAddress(agent)], nativa ? diferencia : 0n);
1494
+ return { txHash: hash, escrowTaskId: (await this.getBoardJob(jobId)).escrowTaskId };
1495
+ }
1496
+ /** Asignar un encargo de selección cuyo cliente no eligió. Lo puede llamar cualquiera. */
1497
+ async autoAssignBoardJob(jobId) {
1498
+ return (await this.firmarEnTablon('La asignación', 'autoAssign', [jobId])).hash;
1499
+ }
1500
+ /** El cliente aprueba la entrega (1-5) y el encargo se cierra. */
1501
+ async approveBoardJob(jobId, rating) {
1502
+ if (!Number.isInteger(rating) || rating < 1 || rating > 5)
1503
+ throw new Error('La valoración es un entero de 1 a 5.');
1504
+ return (await this.firmarEnTablon('La aprobación', 'approve', [jobId, rating])).hash;
1505
+ }
1506
+ async disputeBoardJob(jobId) {
1507
+ return (await this.firmarEnTablon('La disputa', 'dispute', [jobId])).hash;
1508
+ }
1509
+ /**
1510
+ * Cerrar las cuentas de un encargo. Si el escrow se puede adelantar
1511
+ * —cancelar sin entrega pasado el plazo, liberar a las 72 h, devolver una
1512
+ * disputa atascada— lo hace primero. Lo puede llamar cualquiera.
1513
+ */
1514
+ async settleBoardJob(jobId) {
1515
+ return (await this.firmarEnTablon('El cierre', 'settle', [jobId])).hash;
1516
+ }
1517
+ /** El cliente retira un encargo que nadie tiene todavía. */
1518
+ async withdrawBoardJob(jobId) {
1519
+ return (await this.firmarEnTablon('Retirar el encargo', 'withdrawJob', [jobId])).hash;
1520
+ }
1521
+ async expireBoardJob(jobId) {
1522
+ return (await this.firmarEnTablon('La caducidad', 'expire', [jobId])).hash;
1523
+ }
1524
+ /** Cobrar lo que PanalTablon le debe a esta cuenta en una moneda. */
1525
+ async claimBoardRefund(currency = NATIVE_CURRENCY) {
1526
+ this.wallet();
1527
+ const importe = await this.getBoardRefund(this.account.address, currency);
1528
+ if (importe === 0n)
1529
+ throw new Error('No hay nada que cobrar en esa moneda.');
1530
+ const { hash } = await this.firmarEnTablon('El cobro', 'claim', [currency]);
1531
+ return { txHash: hash, importe };
1532
+ }
1533
+ /** Las categorías del agente en el tablón: hasta tres, sin repetir. */
1534
+ async setBoardCategories(categorias) {
1535
+ if (categorias.length > MAX_CATEGORIAS)
1536
+ throw new Error(`Hasta ${MAX_CATEGORIAS} categorías.`);
1537
+ if (new Set(categorias).size !== categorias.length)
1538
+ throw new Error('Hay categorías repetidas.');
1539
+ if (categorias.some((c) => !Number.isInteger(c) || c < 0 || c > 255))
1540
+ throw new Error('Las categorías van de 0 a 255.');
1541
+ return (await this.firmarEnTablon('Las categorías', 'setCategories', [categorias])).hash;
1542
+ }
1127
1543
  async assertFunds(currency, amount, isNative) {
1128
1544
  const owner = this.account.address;
1129
1545
  const balance = isNative
package/dist/index.d.ts CHANGED
@@ -51,3 +51,8 @@ export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMess
51
51
  export type { EncargoDelTablon } from './tablon.js';
52
52
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
53
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
@@ -60,3 +60,11 @@ export { rutaDeAgente } from './net.js';
60
60
  // —otro lenguaje, otro runtime— necesita producir exactamente estos bytes.
61
61
  export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
62
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 {};