@panal/sdk 0.24.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 +26 -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 +28 -0
- package/dist/tablon-reglas.js +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -272,6 +272,32 @@ encargo después de publicar (sin firma: solo entran si dan el `advertHash` y el
|
|
|
272
272
|
`readBoardJobResult` se la da al cliente de verdad —en el escrow, el cliente de
|
|
273
273
|
estas tareas es el contrato, que no puede firmar—.
|
|
274
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
|
+
|
|
275
301
|
### Cuánto es en dólares
|
|
276
302
|
|
|
277
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_ANUNCIO, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, leerAnuncio, ofertaDeLaCadena, problemaAlPublicar, situacionDe, tablonEncargoSignMessage, tablonEntregaSignMessage, tablonResultadoSignMessage, } from './tablon-reglas.js';
|
|
58
|
-
export type { AudienciaTablon, 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_ANUNCIO, MAX_CATEGORIAS, MAX_TEXTO_ANUNCIO, MAX_OFERTAS, MAX_VENTANA_ELECCION_S, MIN_PLAZO_ENTREGA_S, ModoTablon, anuncioPara, audienciaAdmite, categoriaDelTablon, categoriasDeMascara, claveDeCampana, encargoDeLaCadena, idDeCampana, leerAnuncio, 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
|
@@ -185,6 +185,12 @@ export interface PublicarEnElTablon {
|
|
|
185
185
|
id: Hex;
|
|
186
186
|
topePorAgente: number;
|
|
187
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[];
|
|
188
194
|
}
|
|
189
195
|
/**
|
|
190
196
|
* Las comprobaciones de `post()` antes de firmar, con el motivo en claro.
|
|
@@ -201,3 +207,25 @@ export declare function tablonEncargoSignMessage(jobId: bigint, expira: number):
|
|
|
201
207
|
export declare function tablonEntregaSignMessage(jobId: bigint, resultHash: Hex, expira: number): string;
|
|
202
208
|
/** Su cliente se lleva la entrega. */
|
|
203
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) {
|
|
@@ -222,6 +223,11 @@ export function problemaAlPublicar(p) {
|
|
|
222
223
|
if (p.caducaEnS < ofertas + eleccion)
|
|
223
224
|
return 'Caduca antes de que acabe el tiempo de elegir.';
|
|
224
225
|
}
|
|
226
|
+
if (p.adjuntos?.length) {
|
|
227
|
+
const archivos = problemaConArchivos(p.adjuntos);
|
|
228
|
+
if (archivos)
|
|
229
|
+
return archivos;
|
|
230
|
+
}
|
|
225
231
|
if (p.campana && (p.campana.topePorAgente < 1 || p.campana.topePorAgente > 255)) {
|
|
226
232
|
return 'El tope de una campaña es de 1 a 255 encargos por agente.';
|
|
227
233
|
}
|
|
@@ -251,3 +257,69 @@ export function tablonEntregaSignMessage(jobId, resultHash, expira) {
|
|
|
251
257
|
export function tablonResultadoSignMessage(jobId, expira) {
|
|
252
258
|
return `Panal tablón con reglas · resultado #${jobId.toString()} · ${expira}`;
|
|
253
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
|
+
}
|