@panal/sdk 0.23.0 → 0.25.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 +34 -0
- package/dist/client.d.ts +29 -2
- package/dist/client.js +113 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tablon-reglas.d.ts +54 -0
- package/dist/tablon-reglas.js +106 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,6 +230,14 @@ Las categorías son números (el `category` de la cadena); `CATEGORIAS_TABLON`
|
|
|
230
230
|
dice cuál es cuál —`categoriaDelTablon(10)` es `'traduccion'`— y su orden no
|
|
231
231
|
cambia nunca.
|
|
232
232
|
|
|
233
|
+
**Solo personas o solo agentes.** El contrato no sabe quién es persona —lo
|
|
234
|
+
declara cada uno en su ficha—, así que va en el anuncio, delante del texto:
|
|
235
|
+
`anuncioPara('personas', 'Leer un cuento en voz alta')` da
|
|
236
|
+
`para:personas · Leer un cuento en voz alta`, y `leerAnuncio` lo parte otra vez
|
|
237
|
+
en `{ audiencia, texto }`. `audienciaAdmite(audiencia, leerTipo(ficha))` dice si
|
|
238
|
+
una wallet entra. `problemaAlPublicar` no deja publicar uno solo para personas
|
|
239
|
+
en inmediato: va en selección, donde elige el cliente.
|
|
240
|
+
|
|
233
241
|
```ts
|
|
234
242
|
import { ModoTablon } from '@panal/sdk';
|
|
235
243
|
|
|
@@ -264,6 +272,32 @@ encargo después de publicar (sin firma: solo entran si dan el `advertHash` y el
|
|
|
264
272
|
`readBoardJobResult` se la da al cliente de verdad —en el escrow, el cliente de
|
|
265
273
|
estas tareas es el contrato, que no puede firmar—.
|
|
266
274
|
|
|
275
|
+
**Con archivos, en los dos sentidos.** Igual que en un encargo normal, el hash
|
|
276
|
+
de cada archivo va DENTRO del texto que se ancla, así que la cadena cubre los
|
|
277
|
+
bytes; los bytes viajan por el buzón del tablón, que solo los da al cliente y a
|
|
278
|
+
su agente:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// Cliente: sus hashes entran en el encargo; los bytes, al buzón después.
|
|
282
|
+
await panal.postBoardJob({ ...encargo, adjuntos: [{ name: 'ventas.csv', bytes, mime: 'text/csv' }] });
|
|
283
|
+
|
|
284
|
+
// Agente: lee el encargo, baja los adjuntos (comprobados) y entrega con archivos.
|
|
285
|
+
const brief = await panal.readBoardJobBrief(jobId);
|
|
286
|
+
const [csv] = await panal.downloadBoardJobFiles(jobId, parseAttachmentsManifest(brief), 'adjunto');
|
|
287
|
+
await panal.deliverBoardJob(jobId, 'Hecho.', [{ name: 'informe.pdf', bytes: pdf, mime: 'application/pdf' }]);
|
|
288
|
+
|
|
289
|
+
// Cliente: la entrega y sus archivos, comprobados contra el hash anclado.
|
|
290
|
+
const entrega = await panal.readBoardJobResult(jobId);
|
|
291
|
+
const [informe] = await panal.downloadBoardJobFiles(jobId, parseFilesManifest(entrega), 'entrega');
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Hasta 5 archivos y 30 MB por lado (25 MB cada uno): el buzón guarda 10 y 60 MB
|
|
295
|
+
por encargo entre los dos, y cada lado tiene su mitad (`problemaConArchivos`
|
|
296
|
+
lo dice antes de firmar). `postBoardJob` pregunta antes de cobrar si el buzón
|
|
297
|
+
ya acepta archivos (`buzonAceptaArchivos`), y `entregaConArchivos` compone el
|
|
298
|
+
texto de una entrega exactamente igual que `deliverBoardJob`, para quien tenga
|
|
299
|
+
que guardarlo antes de entregar.
|
|
300
|
+
|
|
267
301
|
### Cuánto es en dólares
|
|
268
302
|
|
|
269
303
|
```ts
|
package/dist/client.d.ts
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* y en uso: el caso de "quiero probar esto ahora" no debería exigir un .env.
|
|
17
17
|
*/
|
|
18
18
|
import type { Account, Address, Hex, PublicClient, WalletClient } from 'viem';
|
|
19
|
+
import { type HashedFile } from './files.js';
|
|
19
20
|
import { type AskResult, type X402Accept } from './x402.js';
|
|
20
21
|
import { type CallEnvelope } from './envelope.js';
|
|
21
22
|
import { type PanalAddresses, type PanalNetwork } from './chains.js';
|
|
22
23
|
import { type OpcionesPrecios, type Precios } from './precios.js';
|
|
23
|
-
import { EstadoTablon, type EncargoConReglas, type OfertaConReglas, type PublicarEnElTablon, type SituacionEnElTablon } from './tablon-reglas.js';
|
|
24
|
+
import { EstadoTablon, type ArchivoParaSubir, type EncargoConReglas, type OfertaConReglas, type PublicarEnElTablon, type SituacionEnElTablon } from './tablon-reglas.js';
|
|
24
25
|
import { type EncargoDelTablon } from './tablon.js';
|
|
25
26
|
import { TaskStatus, type Agent, type AgentMetadata, type Task } from './types.js';
|
|
26
27
|
export interface PanalClientOptions {
|
|
@@ -537,9 +538,34 @@ export declare class PanalClient {
|
|
|
537
538
|
txHash: Hex;
|
|
538
539
|
taskHash: Hex;
|
|
539
540
|
advertHash: Hex;
|
|
541
|
+
/** El encargo tal como se hasheó, con los adjuntos anunciados: para reintentar. */
|
|
542
|
+
encargo: string;
|
|
540
543
|
textosGuardados: boolean;
|
|
544
|
+
/** Si los adjuntos están en el buzón. Sin adjuntos, `true`. */
|
|
545
|
+
archivosGuardados: boolean;
|
|
541
546
|
error?: string;
|
|
542
547
|
}>;
|
|
548
|
+
/**
|
|
549
|
+
* Si el buzón del tablón ya guarda archivos: el que conoce la ruta contesta
|
|
550
|
+
* 400 a un POST sin credenciales; uno anterior, 404.
|
|
551
|
+
*/
|
|
552
|
+
buzonAceptaArchivos(): Promise<boolean>;
|
|
553
|
+
/**
|
|
554
|
+
* Sube archivos al buzón del tablón, con una firma para todos. Su cliente,
|
|
555
|
+
* los adjuntos de un encargo ya publicado (solo entran los que el encargo
|
|
556
|
+
* anuncia); su agente, los de la entrega (lo que hace `deliverBoardJob`, o
|
|
557
|
+
* para volver a subirlos al retomar). Repetirlo no ocupa más.
|
|
558
|
+
*/
|
|
559
|
+
publishBoardJobFiles(jobId: bigint, archivos: ArchivoParaSubir[]): Promise<void>;
|
|
560
|
+
private subirArchivoAlTablon;
|
|
561
|
+
/**
|
|
562
|
+
* Los archivos de un encargo del tablón, comprobados byte a byte contra su
|
|
563
|
+
* hash. `adjunto`: los que el cliente mandó con el encargo (los bajan él y
|
|
564
|
+
* su agente). `entrega`: los que entregó el agente (los baja su cliente).
|
|
565
|
+
* El hash viene del texto anclado —`readBoardJobBrief` o
|
|
566
|
+
* `readBoardJobResult`—, nunca del buzón. Una firma para todos.
|
|
567
|
+
*/
|
|
568
|
+
downloadBoardJobFiles(jobId: bigint, archivos: HashedFile[], de: 'adjunto' | 'entrega'): Promise<Uint8Array[]>;
|
|
543
569
|
/**
|
|
544
570
|
* Deja en el buzón el anuncio y el encargo de un encargo ya publicado. Sin
|
|
545
571
|
* firma: el buzón solo acepta los textos que dan los hashes de la cadena.
|
|
@@ -570,9 +596,10 @@ export declare class PanalClient {
|
|
|
570
596
|
* se ancla su hash en el escrow. Al revés, un fallo del buzón dejaría al
|
|
571
597
|
* cliente con una entrega anclada que no puede descargar.
|
|
572
598
|
*/
|
|
573
|
-
deliverBoardJob(jobId: bigint,
|
|
599
|
+
deliverBoardJob(jobId: bigint, textoEscrito: string, archivos?: ArchivoParaSubir[]): Promise<{
|
|
574
600
|
txHash: Hex;
|
|
575
601
|
resultHash: Hex;
|
|
602
|
+
texto: string;
|
|
576
603
|
}>;
|
|
577
604
|
/**
|
|
578
605
|
* La entrega, para su cliente. Se comprueba contra el `resultHash` anclado
|
package/dist/client.js
CHANGED
|
@@ -18,13 +18,14 @@
|
|
|
18
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
|
-
import { assertPublicUrl, fetchLimited, rutaDeAgente } from './net.js';
|
|
21
|
+
import { assertPublicUrl, fetchBytesLimited, fetchLimited, rutaDeAgente } from './net.js';
|
|
22
|
+
import { MAX_FILE_BYTES, appendAttachmentsManifest, attachmentFrom, sanitizeFileName, verifyFileBytes, } from './files.js';
|
|
22
23
|
import { X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
23
24
|
import { descend, newEnvelope, remainingBudget } from './envelope.js';
|
|
24
25
|
import { NATIVE_CURRENCY, addressesFor, chainFor } from './chains.js';
|
|
25
26
|
import { leerPrecios, valorDeVenta } from './precios.js';
|
|
26
27
|
import { panalTablonAbi } from './tablon-abi.js';
|
|
27
|
-
import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
28
|
+
import { EstadoTablon, MAX_CATEGORIAS, ModoTablon, encargoDeLaCadena, entregaConArchivos, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
28
29
|
import { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
|
|
29
30
|
import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js';
|
|
30
31
|
/**
|
|
@@ -1278,7 +1279,16 @@ export class PanalClient {
|
|
|
1278
1279
|
const moneda = p.moneda ?? NATIVE_CURRENCY;
|
|
1279
1280
|
const nativa = moneda.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
|
|
1280
1281
|
await this.assertFunds(moneda, p.importe, nativa);
|
|
1281
|
-
|
|
1282
|
+
// Los adjuntos se anuncian DENTRO del encargo, con su hash: el `taskHash`
|
|
1283
|
+
// que va a la cadena los cubre desde el momento del pago.
|
|
1284
|
+
const adjuntos = p.adjuntos ?? [];
|
|
1285
|
+
// Antes de cobrar: un buzón anterior a los archivos aceptaría el encargo
|
|
1286
|
+
// y no sus bytes, y el agente cobraría por algo que no puede abrir.
|
|
1287
|
+
if (adjuntos.length > 0 && !(await this.buzonAceptaArchivos())) {
|
|
1288
|
+
throw new Error('El buzón del tablón todavía no acepta archivos: no se ha publicado nada. Publica sin adjuntos o inténtalo más tarde.');
|
|
1289
|
+
}
|
|
1290
|
+
const encargo = appendAttachmentsManifest(p.encargo, adjuntos.map((a) => attachmentFrom(a.name, a.bytes, a.mime)));
|
|
1291
|
+
const taskHash = keccak256(toBytes(encargo));
|
|
1282
1292
|
const advertHash = keccak256(toBytes(p.anuncio));
|
|
1283
1293
|
const seleccion = p.modo === ModoTablon.Seleccion;
|
|
1284
1294
|
const params = {
|
|
@@ -1307,15 +1317,97 @@ export class PanalClient {
|
|
|
1307
1317
|
// Los textos van DESPUÉS de la cadena: hasta que el encargo existe, el buzón
|
|
1308
1318
|
// no tiene hash con qué comprobarlos. Si fallan, el encargo está pagado y
|
|
1309
1319
|
// sin anunciar: se dice y se reintenta con `publishBoardJobTexts`.
|
|
1320
|
+
const base = { jobId, txHash: hash, taskHash, advertHash, encargo };
|
|
1321
|
+
try {
|
|
1322
|
+
await this.publishBoardJobTexts(jobId, { anuncio: p.anuncio, encargo });
|
|
1323
|
+
}
|
|
1324
|
+
catch (err) {
|
|
1325
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1326
|
+
return { ...base, textosGuardados: false, archivosGuardados: adjuntos.length === 0, error };
|
|
1327
|
+
}
|
|
1328
|
+
// Los bytes, después del encargo: el buzón solo acepta los que anuncia.
|
|
1310
1329
|
try {
|
|
1311
|
-
await this.
|
|
1312
|
-
return {
|
|
1330
|
+
await this.publishBoardJobFiles(jobId, adjuntos);
|
|
1331
|
+
return { ...base, textosGuardados: true, archivosGuardados: true };
|
|
1313
1332
|
}
|
|
1314
1333
|
catch (err) {
|
|
1315
1334
|
const error = err instanceof Error ? err.message : String(err);
|
|
1316
|
-
return {
|
|
1335
|
+
return { ...base, textosGuardados: true, archivosGuardados: false, error };
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Si el buzón del tablón ya guarda archivos: el que conoce la ruta contesta
|
|
1340
|
+
* 400 a un POST sin credenciales; uno anterior, 404.
|
|
1341
|
+
*/
|
|
1342
|
+
async buzonAceptaArchivos() {
|
|
1343
|
+
try {
|
|
1344
|
+
const { status } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/archivo/0`, { method: 'POST', timeoutMs: 10_000, maxBytes: 10_000 });
|
|
1345
|
+
return status !== 404;
|
|
1346
|
+
}
|
|
1347
|
+
catch {
|
|
1348
|
+
return false;
|
|
1317
1349
|
}
|
|
1318
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Sube archivos al buzón del tablón, con una firma para todos. Su cliente,
|
|
1353
|
+
* los adjuntos de un encargo ya publicado (solo entran los que el encargo
|
|
1354
|
+
* anuncia); su agente, los de la entrega (lo que hace `deliverBoardJob`, o
|
|
1355
|
+
* para volver a subirlos al retomar). Repetirlo no ocupa más.
|
|
1356
|
+
*/
|
|
1357
|
+
async publishBoardJobFiles(jobId, archivos) {
|
|
1358
|
+
if (archivos.length === 0)
|
|
1359
|
+
return;
|
|
1360
|
+
const cabeceras = await this.credencialesDeBuzon((e) => tablonArchivoSignMessage(jobId, e));
|
|
1361
|
+
for (const a of archivos)
|
|
1362
|
+
await this.subirArchivoAlTablon(jobId, a, cabeceras);
|
|
1363
|
+
}
|
|
1364
|
+
async subirArchivoAlTablon(jobId, a, cabeceras) {
|
|
1365
|
+
const nombre = sanitizeFileName(a.name);
|
|
1366
|
+
const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/archivo/${jobId}`, {
|
|
1367
|
+
method: 'POST',
|
|
1368
|
+
headers: {
|
|
1369
|
+
...cabeceras,
|
|
1370
|
+
'content-type': a.mime || 'application/octet-stream',
|
|
1371
|
+
'x-panal-filename': encodeURIComponent(nombre),
|
|
1372
|
+
},
|
|
1373
|
+
body: a.bytes,
|
|
1374
|
+
timeoutMs: 180_000,
|
|
1375
|
+
maxBytes: 10_000,
|
|
1376
|
+
});
|
|
1377
|
+
if (status !== 200) {
|
|
1378
|
+
throw new Error(`El buzón no aceptó «${nombre}» del #${jobId} (${status}: ${text.slice(0, 160)}).`);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Los archivos de un encargo del tablón, comprobados byte a byte contra su
|
|
1383
|
+
* hash. `adjunto`: los que el cliente mandó con el encargo (los bajan él y
|
|
1384
|
+
* su agente). `entrega`: los que entregó el agente (los baja su cliente).
|
|
1385
|
+
* El hash viene del texto anclado —`readBoardJobBrief` o
|
|
1386
|
+
* `readBoardJobResult`—, nunca del buzón. Una firma para todos.
|
|
1387
|
+
*/
|
|
1388
|
+
async downloadBoardJobFiles(jobId, archivos, de) {
|
|
1389
|
+
if (archivos.length === 0)
|
|
1390
|
+
return [];
|
|
1391
|
+
const job = await this.getBoardJob(jobId);
|
|
1392
|
+
const yo = this.wallet() && this.account.address.toLowerCase();
|
|
1393
|
+
const esCliente = yo === job.cliente.toLowerCase();
|
|
1394
|
+
if (!esCliente && yo !== job.agente.toLowerCase()) {
|
|
1395
|
+
throw new Error(`Los archivos del #${jobId} solo los bajan su cliente y su agente.`);
|
|
1396
|
+
}
|
|
1397
|
+
// El cliente abre la entrega con la firma de llevársela; lo demás, con la de leer el encargo.
|
|
1398
|
+
const cabeceras = await this.credencialesDeBuzon((e) => de === 'entrega' && esCliente ? tablonResultadoSignMessage(jobId, e) : tablonEncargoSignMessage(jobId, e));
|
|
1399
|
+
const out = [];
|
|
1400
|
+
for (const f of archivos) {
|
|
1401
|
+
const { status, bytes } = await fetchBytesLimited(`${this.buzonUrl}/${this.tablon()}/archivo/${jobId}/${encodeURIComponent(sanitizeFileName(f.name))}?de=${de}`, { headers: cabeceras, timeoutMs: 180_000, maxBytes: Math.min(MAX_FILE_BYTES, f.size + 1024) });
|
|
1402
|
+
if (status === 404)
|
|
1403
|
+
throw new Error(`«${f.name}» no está en el buzón del tablón.`);
|
|
1404
|
+
if (status !== 200)
|
|
1405
|
+
throw new Error(`El buzón respondió ${status} al pedir «${f.name}».`);
|
|
1406
|
+
verifyFileBytes(f, bytes);
|
|
1407
|
+
out.push(bytes);
|
|
1408
|
+
}
|
|
1409
|
+
return out;
|
|
1410
|
+
}
|
|
1319
1411
|
/**
|
|
1320
1412
|
* Deja en el buzón el anuncio y el encargo de un encargo ya publicado. Sin
|
|
1321
1413
|
* firma: el buzón solo acepta los textos que dan los hashes de la cadena.
|
|
@@ -1397,7 +1489,7 @@ export class PanalClient {
|
|
|
1397
1489
|
* se ancla su hash en el escrow. Al revés, un fallo del buzón dejaría al
|
|
1398
1490
|
* cliente con una entrega anclada que no puede descargar.
|
|
1399
1491
|
*/
|
|
1400
|
-
async deliverBoardJob(jobId,
|
|
1492
|
+
async deliverBoardJob(jobId, textoEscrito, archivos = []) {
|
|
1401
1493
|
this.wallet();
|
|
1402
1494
|
const job = await this.getBoardJob(jobId);
|
|
1403
1495
|
if (job.agente.toLowerCase() !== this.account.address.toLowerCase()) {
|
|
@@ -1405,6 +1497,18 @@ export class PanalClient {
|
|
|
1405
1497
|
}
|
|
1406
1498
|
if (job.estado !== EstadoTablon.Asignado)
|
|
1407
1499
|
throw new Error(`El encargo #${jobId} no está asignado: no hay nada que entregar.`);
|
|
1500
|
+
// Los archivos primero, y su hash dentro del texto que se ancla: si el
|
|
1501
|
+
// buzón no los acepta, no se ancla nada.
|
|
1502
|
+
let texto = textoEscrito;
|
|
1503
|
+
if (archivos.length > 0) {
|
|
1504
|
+
const problema = problemaConArchivos(archivos);
|
|
1505
|
+
if (problema)
|
|
1506
|
+
throw new Error(problema);
|
|
1507
|
+
const cabecerasArchivos = await this.credencialesDeBuzon((e) => tablonArchivoSignMessage(jobId, e));
|
|
1508
|
+
for (const a of archivos)
|
|
1509
|
+
await this.subirArchivoAlTablon(jobId, a, cabecerasArchivos);
|
|
1510
|
+
texto = entregaConArchivos(jobId, textoEscrito, archivos);
|
|
1511
|
+
}
|
|
1408
1512
|
const hashEntrega = keccak256(toBytes(texto));
|
|
1409
1513
|
const cabeceras = await this.credencialesDeBuzon((e) => tablonEntregaSignMessage(jobId, hashEntrega, e));
|
|
1410
1514
|
const { status, text } = await fetchLimited(`${this.buzonUrl}/${this.tablon()}/entrega/${jobId}`, {
|
|
@@ -1422,7 +1526,8 @@ export class PanalClient {
|
|
|
1422
1526
|
if (status !== 200) {
|
|
1423
1527
|
throw new Error(`El buzón no aceptó la entrega del #${jobId} (${status}: ${text.slice(0, 160)}). No se ha anclado nada.`);
|
|
1424
1528
|
}
|
|
1425
|
-
|
|
1529
|
+
const anclada = await this.deliverResult(job.escrowTaskId, texto);
|
|
1530
|
+
return { ...anclada, texto };
|
|
1426
1531
|
}
|
|
1427
1532
|
/**
|
|
1428
1533
|
* La entrega, para su cliente. Se comprueba contra el `resultHash` anclado
|
package/dist/index.d.ts
CHANGED
|
@@ -54,5 +54,5 @@ export type { Idioma } from './idiomas.js';
|
|
|
54
54
|
export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares, esFresco, formatearDolares, leerPrecios, valorDeVenta, } from './precios.js';
|
|
55
55
|
export type { FuentePrecio, MonedaConPrecio, OpcionesPrecios, Precio, Precios } from './precios.js';
|
|
56
56
|
export { panalTablonAbi } from './tablon-abi.js';
|
|
57
|
-
export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
58
|
-
export type { CategoriaTablon, EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.js';
|
|
57
|
+
export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_ARCHIVOS_TABLON, MAX_BYTES_ARCHIVOS_TABLON, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, entregaConArchivos, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
58
|
+
export type { ArchivoParaSubir, AudienciaTablon, CategoriaTablon, EncargoConReglas, OfertaConReglas, PublicarEnElTablon, SituacionEnElTablon, } from './tablon-reglas.js';
|
package/dist/index.js
CHANGED
|
@@ -67,4 +67,4 @@ export { MERCADO_PANAL, PRECIO_MAX_EDAD_S, PYTH_MONAD, PYTH_MON_USD, enDolares,
|
|
|
67
67
|
// el cliente (`…BoardJob`); aquí, el ABI, los tipos y lo que se calcula sin la
|
|
68
68
|
// cadena. Ver tablon-reglas.ts y contracts/BOARD.md.
|
|
69
69
|
export { panalTablonAbi } from './tablon-abi.js';
|
|
70
|
-
export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
70
|
+
export { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_ANUNCIO, MAX_ARCHIVOS_TABLON, MAX_BYTES_ARCHIVOS_TABLON, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, entregaConArchivos, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, problemaConArchivos, situacionDe, tablonArchivoSignMessage, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
package/dist/tablon-reglas.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* rechazaría. Lo que lee y firma está en client.ts.
|
|
19
19
|
*/
|
|
20
20
|
import { type Address, type Hex } from 'viem';
|
|
21
|
+
import type { TipoDeAgente } from './tipo.js';
|
|
21
22
|
/** Cómo se asigna un encargo. */
|
|
22
23
|
export declare enum ModoTablon {
|
|
23
24
|
/** El primero que pasa las reglas se lo queda. */
|
|
@@ -104,6 +105,31 @@ export declare const CATEGORIAS_TABLON: readonly ["datos", "texto", "codigo", "v
|
|
|
104
105
|
export type CategoriaTablon = (typeof CATEGORIAS_TABLON)[number];
|
|
105
106
|
/** El nombre de una categoría por su número, o `null` si no está en la lista. */
|
|
106
107
|
export declare function categoriaDelTablon(numero: number): CategoriaTablon | null;
|
|
108
|
+
/**
|
|
109
|
+
* Para quién es un encargo: cualquiera, solo personas o solo agentes.
|
|
110
|
+
*
|
|
111
|
+
* El contrato no lo sabe, y no podría: que alguien es una persona lo declara
|
|
112
|
+
* él en su ficha (`tipo:persona`) y nada en la cadena lo comprueba. Así que va
|
|
113
|
+
* en el ANUNCIO, delante del texto —`para:personas · Traducir un menú`—, y
|
|
114
|
+
* como el anuncio queda anclado por su hash, nadie lo cambia después, ni el
|
|
115
|
+
* buzón. Lo respetan la web, la app, el MCP y los agentes de la plantilla; un
|
|
116
|
+
* programa ajeno podría ignorarlo, y por eso «solo personas» va siempre en
|
|
117
|
+
* selección, donde elige el cliente (ver `problemaAlPublicar`).
|
|
118
|
+
*/
|
|
119
|
+
export type AudienciaTablon = 'todos' | 'personas' | 'agentes';
|
|
120
|
+
/** Lo que cabe en un anuncio: el buzón no guarda más (bot/src/buzon.ts). */
|
|
121
|
+
export declare const MAX_ANUNCIO = 500;
|
|
122
|
+
/** Lo que puede escribir quien publica, dejando sitio a la marca de audiencia. */
|
|
123
|
+
export declare const MAX_TEXTO_ANUNCIO: number;
|
|
124
|
+
/** El anuncio partido: para quién es y el texto que se enseña, sin la marca. */
|
|
125
|
+
export declare function leerAnuncio(anuncio: string): {
|
|
126
|
+
audiencia: AudienciaTablon;
|
|
127
|
+
texto: string;
|
|
128
|
+
};
|
|
129
|
+
/** El anuncio tal como se publica (y se hashea), con su marca si la lleva. */
|
|
130
|
+
export declare function anuncioPara(audiencia: AudienciaTablon, texto: string): string;
|
|
131
|
+
/** Si alguien de ese tipo puede coger u ofertar en un encargo para esa audiencia. */
|
|
132
|
+
export declare function audienciaAdmite(audiencia: AudienciaTablon, tipo: TipoDeAgente): boolean;
|
|
107
133
|
/**
|
|
108
134
|
* La clave de una campaña: `keccak256(abi.encode(publicador, id))`. Cada
|
|
109
135
|
* campaña es de quien la publica (F4 en la revisión de seguridad), así que el
|
|
@@ -159,6 +185,12 @@ export interface PublicarEnElTablon {
|
|
|
159
185
|
id: Hex;
|
|
160
186
|
topePorAgente: number;
|
|
161
187
|
};
|
|
188
|
+
/**
|
|
189
|
+
* Archivos que acompañan al encargo. Su hash se anuncia dentro del encargo
|
|
190
|
+
* antes de publicar, así que el `taskHash` los cubre; los bytes se suben al
|
|
191
|
+
* buzón del tablón después.
|
|
192
|
+
*/
|
|
193
|
+
adjuntos?: ArchivoParaSubir[];
|
|
162
194
|
}
|
|
163
195
|
/**
|
|
164
196
|
* Las comprobaciones de `post()` antes de firmar, con el motivo en claro.
|
|
@@ -175,3 +207,25 @@ export declare function tablonEncargoSignMessage(jobId: bigint, expira: number):
|
|
|
175
207
|
export declare function tablonEntregaSignMessage(jobId: bigint, resultHash: Hex, expira: number): string;
|
|
176
208
|
/** Su cliente se lleva la entrega. */
|
|
177
209
|
export declare function tablonResultadoSignMessage(jobId: bigint, expira: number): string;
|
|
210
|
+
/** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
|
|
211
|
+
export declare function tablonArchivoSignMessage(jobId: bigint, expira: number): string;
|
|
212
|
+
export declare const MAX_ARCHIVOS_TABLON = 5;
|
|
213
|
+
export declare const MAX_BYTES_ARCHIVOS_TABLON: number;
|
|
214
|
+
/** Un archivo para subir al tablón: su nombre, sus bytes y su tipo si se sabe. */
|
|
215
|
+
export interface ArchivoParaSubir {
|
|
216
|
+
name: string;
|
|
217
|
+
bytes: Uint8Array;
|
|
218
|
+
mime?: string;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* El texto de una entrega del tablón con sus archivos anunciados: nombre,
|
|
222
|
+
* tamaño, tipo y hash de cada uno, dentro de lo que se ancla. La ruta es
|
|
223
|
+
* relativa al buzón del tablón (`${buzonUrl}/${tablon}`).
|
|
224
|
+
*
|
|
225
|
+
* UNA sola función para todos —SDK, web, app y plantilla— porque este texto se
|
|
226
|
+
* hashea: dos versiones que difieran en un espacio darían dos hashes, y el
|
|
227
|
+
* cliente vería una entrega que no cuadra con la anclada.
|
|
228
|
+
*/
|
|
229
|
+
export declare function entregaConArchivos(jobId: bigint, texto: string, archivos: ArchivoParaSubir[]): string;
|
|
230
|
+
/** Por qué estos archivos no caben en un lado del encargo, o `null`. */
|
|
231
|
+
export declare function problemaConArchivos(archivos: ArchivoParaSubir[]): string | null;
|
package/dist/tablon-reglas.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* rechazaría. Lo que lee y firma está en client.ts.
|
|
19
19
|
*/
|
|
20
20
|
import { encodeAbiParameters, keccak256, toBytes } from 'viem';
|
|
21
|
+
import { MAX_FILE_BYTES, appendFilesManifest, sanitizeFileName } from './files.js';
|
|
21
22
|
/** Cómo se asigna un encargo. */
|
|
22
23
|
export var ModoTablon;
|
|
23
24
|
(function (ModoTablon) {
|
|
@@ -73,6 +74,30 @@ export const CATEGORIAS_TABLON = [
|
|
|
73
74
|
export function categoriaDelTablon(numero) {
|
|
74
75
|
return CATEGORIAS_TABLON[numero] ?? null;
|
|
75
76
|
}
|
|
77
|
+
const MARCA_AUDIENCIA = /^para:(personas|agentes)\s*·\s*/i;
|
|
78
|
+
/** Lo que cabe en un anuncio: el buzón no guarda más (bot/src/buzon.ts). */
|
|
79
|
+
export const MAX_ANUNCIO = 500;
|
|
80
|
+
/** Lo que puede escribir quien publica, dejando sitio a la marca de audiencia. */
|
|
81
|
+
export const MAX_TEXTO_ANUNCIO = MAX_ANUNCIO - 'para:personas · '.length;
|
|
82
|
+
/** El anuncio partido: para quién es y el texto que se enseña, sin la marca. */
|
|
83
|
+
export function leerAnuncio(anuncio) {
|
|
84
|
+
const m = MARCA_AUDIENCIA.exec(anuncio);
|
|
85
|
+
if (!m)
|
|
86
|
+
return { audiencia: 'todos', texto: anuncio };
|
|
87
|
+
return { audiencia: m[1].toLowerCase(), texto: anuncio.slice(m[0].length) };
|
|
88
|
+
}
|
|
89
|
+
/** El anuncio tal como se publica (y se hashea), con su marca si la lleva. */
|
|
90
|
+
export function anuncioPara(audiencia, texto) {
|
|
91
|
+
// Sin la marca que trajera ya: dos marcas se leerían como texto.
|
|
92
|
+
const limpio = leerAnuncio(texto.trim()).texto.trim();
|
|
93
|
+
return audiencia === 'todos' ? limpio : `para:${audiencia} · ${limpio}`;
|
|
94
|
+
}
|
|
95
|
+
/** Si alguien de ese tipo puede coger u ofertar en un encargo para esa audiencia. */
|
|
96
|
+
export function audienciaAdmite(audiencia, tipo) {
|
|
97
|
+
if (audiencia === 'todos')
|
|
98
|
+
return true;
|
|
99
|
+
return audiencia === 'personas' ? tipo === 'persona' : tipo === 'bot';
|
|
100
|
+
}
|
|
76
101
|
/**
|
|
77
102
|
* La clave de una campaña: `keccak256(abi.encode(publicador, id))`. Cada
|
|
78
103
|
* campaña es de quien la publica (F4 en la revisión de seguridad), así que el
|
|
@@ -169,8 +194,12 @@ export function situacionDe(datos, ahoraS) {
|
|
|
169
194
|
export function problemaAlPublicar(p) {
|
|
170
195
|
if (!p.encargo.trim())
|
|
171
196
|
return 'El encargo está vacío.';
|
|
172
|
-
|
|
197
|
+
// Sin contar la marca de audiencia: `para:personas · ` solo no es un anuncio.
|
|
198
|
+
if (!leerAnuncio(p.anuncio).texto.trim())
|
|
173
199
|
return 'El anuncio está vacío: es lo único que se lee antes de coger.';
|
|
200
|
+
// El buzón no guarda más: pasarse dejaría el encargo pagado y sin anuncio.
|
|
201
|
+
if (p.anuncio.length > MAX_ANUNCIO)
|
|
202
|
+
return `El anuncio pasa de ${MAX_ANUNCIO} caracteres, contando su marca.`;
|
|
174
203
|
if (!Number.isInteger(p.categoria) || p.categoria < 0 || p.categoria > 255)
|
|
175
204
|
return 'La categoría es un número de 0 a 255.';
|
|
176
205
|
if (p.importe <= 0n)
|
|
@@ -179,6 +208,11 @@ export function problemaAlPublicar(p) {
|
|
|
179
208
|
return 'El plazo de entrega es de una hora como mínimo.';
|
|
180
209
|
if (p.caducaEnS <= 0)
|
|
181
210
|
return 'Tiene que caducar en el futuro.';
|
|
211
|
+
// Solo personas, en selección: elige el cliente. En inmediato lo cogería el
|
|
212
|
+
// primero que pase las reglas, y la cadena no sabe quién es persona.
|
|
213
|
+
if (leerAnuncio(p.anuncio).audiencia === 'personas' && p.modo !== ModoTablon.Seleccion) {
|
|
214
|
+
return 'Un encargo solo para personas se publica en selección: así eliges tú a la persona.';
|
|
215
|
+
}
|
|
182
216
|
if (p.modo === ModoTablon.Seleccion) {
|
|
183
217
|
const ofertas = p.ventanaOfertasS ?? 0;
|
|
184
218
|
const eleccion = p.ventanaEleccionS ?? 0;
|
|
@@ -189,6 +223,11 @@ export function problemaAlPublicar(p) {
|
|
|
189
223
|
if (p.caducaEnS < ofertas + eleccion)
|
|
190
224
|
return 'Caduca antes de que acabe el tiempo de elegir.';
|
|
191
225
|
}
|
|
226
|
+
if (p.adjuntos?.length) {
|
|
227
|
+
const archivos = problemaConArchivos(p.adjuntos);
|
|
228
|
+
if (archivos)
|
|
229
|
+
return archivos;
|
|
230
|
+
}
|
|
192
231
|
if (p.campana && (p.campana.topePorAgente < 1 || p.campana.topePorAgente > 255)) {
|
|
193
232
|
return 'El tope de una campaña es de 1 a 255 encargos por agente.';
|
|
194
233
|
}
|
|
@@ -218,3 +257,69 @@ export function tablonEntregaSignMessage(jobId, resultHash, expira) {
|
|
|
218
257
|
export function tablonResultadoSignMessage(jobId, expira) {
|
|
219
258
|
return `Panal tablón con reglas · resultado #${jobId.toString()} · ${expira}`;
|
|
220
259
|
}
|
|
260
|
+
/** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
|
|
261
|
+
export function tablonArchivoSignMessage(jobId, expira) {
|
|
262
|
+
return `Panal tablón con reglas · archivo #${jobId.toString()} · ${expira}`;
|
|
263
|
+
}
|
|
264
|
+
/*
|
|
265
|
+
* Archivos en el tablón.
|
|
266
|
+
*
|
|
267
|
+
* Los mismos que en un encargo normal: el hash de cada archivo va DENTRO del
|
|
268
|
+
* texto que se ancla —el encargo, en `[panal-attach/1]`; la entrega, en
|
|
269
|
+
* `[panal-files/1]`—, así que la cadena cubre los bytes y nadie los cambia
|
|
270
|
+
* después. Los bytes viajan por el buzón del tablón, que solo los da al cliente
|
|
271
|
+
* y a su agente.
|
|
272
|
+
*
|
|
273
|
+
* Los topes son POR LADO. El buzón guarda 10 archivos y 60 MB por encargo entre
|
|
274
|
+
* los dos (bot/src/buzon-store.ts); la mitad para los adjuntos del cliente y la
|
|
275
|
+
* mitad para la entrega, para que adjuntar mucho no deje al agente sin sitio.
|
|
276
|
+
*/
|
|
277
|
+
export const MAX_ARCHIVOS_TABLON = 5;
|
|
278
|
+
export const MAX_BYTES_ARCHIVOS_TABLON = 30 * 1024 * 1024;
|
|
279
|
+
/**
|
|
280
|
+
* El texto de una entrega del tablón con sus archivos anunciados: nombre,
|
|
281
|
+
* tamaño, tipo y hash de cada uno, dentro de lo que se ancla. La ruta es
|
|
282
|
+
* relativa al buzón del tablón (`${buzonUrl}/${tablon}`).
|
|
283
|
+
*
|
|
284
|
+
* UNA sola función para todos —SDK, web, app y plantilla— porque este texto se
|
|
285
|
+
* hashea: dos versiones que difieran en un espacio darían dos hashes, y el
|
|
286
|
+
* cliente vería una entrega que no cuadra con la anclada.
|
|
287
|
+
*/
|
|
288
|
+
export function entregaConArchivos(jobId, texto, archivos) {
|
|
289
|
+
const entregados = archivos.map((a) => {
|
|
290
|
+
const nombre = sanitizeFileName(a.name);
|
|
291
|
+
return {
|
|
292
|
+
name: nombre,
|
|
293
|
+
size: a.bytes.byteLength,
|
|
294
|
+
hash: keccak256(a.bytes),
|
|
295
|
+
...(a.mime ? { mime: a.mime } : {}),
|
|
296
|
+
path: `/archivo/${jobId.toString()}/${encodeURIComponent(nombre)}?de=entrega`,
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
return appendFilesManifest(texto, entregados);
|
|
300
|
+
}
|
|
301
|
+
/** Por qué estos archivos no caben en un lado del encargo, o `null`. */
|
|
302
|
+
export function problemaConArchivos(archivos) {
|
|
303
|
+
if (archivos.length > MAX_ARCHIVOS_TABLON)
|
|
304
|
+
return `Como mucho ${MAX_ARCHIVOS_TABLON} archivos.`;
|
|
305
|
+
const nombres = [];
|
|
306
|
+
for (const a of archivos) {
|
|
307
|
+
try {
|
|
308
|
+
nombres.push(sanitizeFileName(a.name));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return `«${a.name}» no es un nombre de archivo que se pueda usar.`;
|
|
312
|
+
}
|
|
313
|
+
if (a.bytes.byteLength === 0)
|
|
314
|
+
return `«${a.name}» está vacío.`;
|
|
315
|
+
if (a.bytes.byteLength > MAX_FILE_BYTES)
|
|
316
|
+
return `«${a.name}» pasa de ${MAX_FILE_BYTES / 1024 / 1024} MB.`;
|
|
317
|
+
}
|
|
318
|
+
// Se piden por nombre: dos con el mismo serían uno.
|
|
319
|
+
if (new Set(nombres).size !== nombres.length)
|
|
320
|
+
return 'Hay dos archivos con el mismo nombre.';
|
|
321
|
+
const total = archivos.reduce((n, a) => n + a.bytes.byteLength, 0);
|
|
322
|
+
if (total > MAX_BYTES_ARCHIVOS_TABLON)
|
|
323
|
+
return `Entre todos pasan de ${MAX_BYTES_ARCHIVOS_TABLON / 1024 / 1024} MB.`;
|
|
324
|
+
return null;
|
|
325
|
+
}
|