@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/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
@@ -0,0 +1,97 @@
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 declare const EJEMPLOS_EDITABLES = 3;
42
+ /** Tope de cada ejemplo, en caracteres. Una frase, no un encargo. */
43
+ export declare const MAX_EJEMPLO = 140;
44
+ /**
45
+ * `ejemplo:Revisa este contrato` → `'Revisa este contrato'`, o `null` si el
46
+ * segmento no es un ejemplo.
47
+ *
48
+ * Devolver `null` es lo normal: por aquí pasan TODOS los segmentos de la
49
+ * ficha, incluida la descripción libre del agente.
50
+ */
51
+ export declare function leerEjemploDeSegmento(segmento: string): string | null;
52
+ /** true si este segmento es un ejemplo. Para que los lectores de ficha lo aparten. */
53
+ export declare function esTokenDeEjemplo(segmento: string): boolean;
54
+ /**
55
+ * Los ejemplos que un agente publica en la cadena, en el orden en que los
56
+ * escribió. `[]` es lo normal.
57
+ *
58
+ * Los repetidos se quedan en uno: dos botones iguales no son dos ideas.
59
+ */
60
+ export declare function leerEjemplosDeMetadata(metadataURI: string | null | undefined): string[];
61
+ /**
62
+ * Qué le pasa a lo que alguien escribe, para poder decírselo.
63
+ *
64
+ * `null` es que está bien o que está vacío. Un «·» partiría la ficha en dos y
65
+ * el trozo de después saldría como una skill; demasiado largo se recortaría
66
+ * sin avisar. En los dos casos se firmaría algo distinto de lo que su dueño
67
+ * ve, así que se canta antes.
68
+ */
69
+ export declare function falloDeEjemplo(texto: string): 'separador' | 'largo' | null;
70
+ /**
71
+ * El texto → su segmento, o `null` si no se puede escribir tal cual.
72
+ *
73
+ * `null` en vez de arreglarlo: quien llama tiene que enseñar el error, no
74
+ * firmar una ficha que su dueño no reconoce. Vacío también es `null`, y eso
75
+ * es lo normal: el formulario enseña tres y casi nadie rellena las tres.
76
+ */
77
+ export declare function componerEjemplo(texto: string): string | null;
78
+ /**
79
+ * Los ejemplos de la ficha que sirve el agente (`examples`), ya saneados.
80
+ *
81
+ * Esa ficha la escribe un servidor ajeno: se aceptan solo cadenas, se limpian
82
+ * y se recortan, y se leen como mucho los mismos que caben en la cadena.
83
+ */
84
+ export declare function leerEjemplos(card: unknown): string[];
85
+ /**
86
+ * Los de la cadena, con el texto —traducido— de la ficha cuando cuadra.
87
+ *
88
+ * La cadena guarda una sola versión, en el idioma de su dueño. La ficha que
89
+ * sirve el agente los repite traducidos al idioma pedido, en el mismo orden.
90
+ * Se usan los de la ficha solo si son LOS MISMOS en número: si no cuadran, la
91
+ * ficha está desfasada respecto a la cadena (el agente la relee cada pocos
92
+ * minutos) y se enseñan los de la cadena, que son los vigentes.
93
+ *
94
+ * Sin nada en la cadena valen los de la ficha: un ejemplo no mueve dinero, y
95
+ * un agente puede declararlos solo en su código.
96
+ */
97
+ export declare function conEjemplosDeLaFicha(enCadena: string[], deLaFicha: string[]): string[];