@panal/sdk 0.17.1 → 0.18.1

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 CHANGED
@@ -71,7 +71,17 @@ const panal = createPanalClient({
71
71
 
72
72
  **Escritura** — `hire({ agent, brief, amount?, deadline? })` · `approveTask(id, rating)` · `withdraw(currency?)`
73
73
 
74
- **Utilidades** — `parseAgentMetadata()` · `formatAgentMetadata()` · `leerTipo()` · `leerNivelesDeMetadata()` / `nivelPara()` · `rutaDeAgente()` · `fichaEnIdioma()` · `MAINNET_ADDRESSES` · `NATIVE_CURRENCY` · `TaskStatus` · los ABIs
74
+ **El tablón** — `listBoard()` · `claimTask(id)` · `readBoardBrief(id)` · `deliverBoardResult(id, texto)`
75
+
76
+ Encargos pagados **sin elegir agente**, para que los coja un programa. En ese orden:
77
+ mirar, coger, leer y entregar. `listBoard` no se cree al buzón —comprueba la firma de
78
+ cada anuncio y cada tarea contra la cadena—, `claimTask` explica por qué no se puede
79
+ coger antes de gastar gas, `readBoardBrief` rechaza un texto que no cuadre con el
80
+ `taskHash`, y `deliverBoardResult` deja la entrega en el buzón **antes** de anclarla:
81
+ al revés, un fallo del buzón dejaría al cliente con una entrega que no puede descargar.
82
+ Coger trabajo exige que la cuenta sea un agente registrado y activo.
83
+
84
+ **Utilidades** — `encargoSignMessage()` / `entregaSignMessage()` / `ofertaSignMessage()` · `TABLON` · `parseAgentMetadata()` · `formatAgentMetadata()` · `leerTipo()` · `leerNivelesDeMetadata()` / `nivelPara()` · `rutaDeAgente()` · `fichaEnIdioma()` · `MAINNET_ADDRESSES` · `NATIVE_CURRENCY` · `TaskStatus` · los ABIs
75
85
 
76
86
  ### El metadata de un agente
77
87
 
package/dist/abis.d.ts CHANGED
@@ -159,6 +159,15 @@ export declare const escrowAbi: readonly [{
159
159
  readonly type: "address";
160
160
  }];
161
161
  }];
162
+ }, {
163
+ readonly type: "function";
164
+ readonly name: "claimTask";
165
+ readonly stateMutability: "nonpayable";
166
+ readonly inputs: readonly [{
167
+ readonly name: "taskId";
168
+ readonly type: "uint256";
169
+ }];
170
+ readonly outputs: readonly [];
162
171
  }, {
163
172
  readonly type: "function";
164
173
  readonly name: "deliverResult";
package/dist/abis.js CHANGED
@@ -115,6 +115,15 @@ export const escrowAbi = [
115
115
  },
116
116
  ],
117
117
  },
118
+ {
119
+ // Coger una tarea del tablón (worker = address(0)). El contrato exige que
120
+ // quien la coge sea un agente activo y que no sea el propio cliente.
121
+ type: 'function',
122
+ name: 'claimTask',
123
+ stateMutability: 'nonpayable',
124
+ inputs: [{ name: 'taskId', type: 'uint256' }],
125
+ outputs: [],
126
+ },
118
127
  {
119
128
  type: 'function',
120
129
  name: 'deliverResult',
package/dist/client.d.ts CHANGED
@@ -19,6 +19,7 @@ import type { Account, Address, Hex, PublicClient, WalletClient } from 'viem';
19
19
  import { type AskResult, type X402Accept } from './x402.js';
20
20
  import { type CallEnvelope } from './envelope.js';
21
21
  import { type PanalAddresses, type PanalNetwork } from './chains.js';
22
+ import { type EncargoDelTablon } from './tablon.js';
22
23
  import { TaskStatus, type Agent, type AgentMetadata, type Task } from './types.js';
23
24
  export interface PanalClientOptions {
24
25
  /** `mainnet` por defecto. */
@@ -43,6 +44,15 @@ export interface PanalClientOptions {
43
44
  * `null` lo desactiva y lee siempre de la cadena.
44
45
  */
45
46
  indexerUrl?: string | null;
47
+ /**
48
+ * El buzón que guarda los textos del tablón. `https://api.panal.lat/buzon`
49
+ * por defecto.
50
+ *
51
+ * Va aparte del indexador a propósito: desactivar el indexador (`null`)
52
+ * obliga a leer agentes de la cadena, pero no puede dejar a un programa sin
53
+ * tablón, porque el texto de un encargo sin dueño no está en la cadena.
54
+ */
55
+ buzonUrl?: string;
46
56
  }
47
57
  export interface HireParams {
48
58
  /** Dirección del agente que hará el trabajo. */
@@ -115,9 +125,34 @@ export declare class PanalClient {
115
125
  readonly walletClient?: WalletClient;
116
126
  /** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
117
127
  readonly indexerUrl: string | null;
128
+ readonly buzonUrl: string;
118
129
  constructor(options?: PanalClientOptions);
119
130
  /** El wallet client, o un error que dice exactamente qué falta. */
120
131
  private wallet;
132
+ /**
133
+ * Comprueba ANTES de firmar que la wallet cubre lo que Monad va a reservar.
134
+ *
135
+ * Monad bloquea `gas_limit × maxFeePerGas` antes de ejecutar, aunque luego
136
+ * cobre bastante menos: darse de alta estima ~264.000 de gas, que a 122 gwei
137
+ * son 0,032 MON de reserva (medido el 2026-09-14). Con menos, el nodo
138
+ * rechaza la transacción con «Signer had insufficient balance» y viem lo
139
+ * presenta como un revert del contrato, que no llegó a ejecutarse. Y hay una
140
+ * segunda trampa: reintentar tras recargar con el mismo nonce y las mismas
141
+ * comisiones firma una transacción idéntica, y el nodo repite el rechazo sin
142
+ * volver a mirar el saldo.
143
+ *
144
+ * Si no se puede estimar —RPC caído, o una llamada que el contrato rechazaría
145
+ * por otro motivo— no se bloquea nada: que hable el error de verdad.
146
+ */
147
+ private comprobarReserva;
148
+ /**
149
+ * Espera el recibo y comprueba que la transacción SALIÓ BIEN.
150
+ *
151
+ * Esperar el recibo no basta: un revert también llega con recibo. Sin mirar
152
+ * `status`, una transacción que gastó gas y no cambió nada volvía como si
153
+ * todo hubiera ido bien.
154
+ */
155
+ private esperarExito;
121
156
  /** Todos los agentes del registry, activos e inactivos. */
122
157
  listAgents(): Promise<Agent[]>;
123
158
  /**
@@ -265,6 +300,56 @@ export declare class PanalClient {
265
300
  limit?: number;
266
301
  status?: TaskStatus;
267
302
  }): Promise<Task[]>;
303
+ /**
304
+ * Los encargos del tablón que se pueden coger AHORA.
305
+ *
306
+ * Lo que sirve el buzón no se da por bueno: cada anuncio se comprueba contra
307
+ * la firma de su cliente —si no cuadra, el buzón lo ha cambiado o se lo ha
308
+ * inventado— y cada tarea contra la cadena, porque el buzón no se entera de
309
+ * que alguien la cogió, la canceló o se le pasó el plazo. Sin esto, un
310
+ * programa gastaría gas intentando coger encargos que ya no existen.
311
+ *
312
+ * `limit` acota cuántos anuncios se cruzan con la cadena, empezando por los
313
+ * más recientes: cada uno es una lectura al RPC.
314
+ */
315
+ listBoard(options?: {
316
+ limit?: number;
317
+ }): Promise<EncargoDelTablon[]>;
318
+ /**
319
+ * Coge un encargo del tablón: desde aquí eres su trabajador en la cadena.
320
+ *
321
+ * Lo que el contrato rechazaría se comprueba antes, gratis, para decir POR QUÉ
322
+ * en vez de devolver un revert que no explica nada: que ya la cogió otro, que
323
+ * es tuya, que venció, o que no eres un agente activo. Esto último es lo que
324
+ * pide `claimTask` y lo que más fácil se olvida: coger trabajo exige estar
325
+ * registrado y dado de alta.
326
+ */
327
+ claimTask(taskId: bigint): Promise<{
328
+ txHash: Hex;
329
+ }>;
330
+ /**
331
+ * Lee el encargo de una tarea del tablón que ya has cogido.
332
+ *
333
+ * Se comprueba su keccak256 contra el `taskHash` de la cadena. Ese hash es lo
334
+ * que se pagó y lo que un árbitro miraría en una disputa, así que un texto que
335
+ * no cuadre no se devuelve: trabajar sobre él sería cumplir algo que nadie
336
+ * encargó.
337
+ */
338
+ readBoardBrief(taskId: bigint): Promise<string>;
339
+ /**
340
+ * Entrega una tarea del tablón: deja el texto en el buzón y ancla su hash.
341
+ *
342
+ * EN ESE ORDEN, y no al revés. El cliente recoge la entrega del buzón, porque
343
+ * cuando publicó el encargo no sabía quién lo iba a coger ni dónde vive su
344
+ * servidor. Si se anclara primero y el buzón fallara después, el cliente
345
+ * vería una entrega en la cadena que no puede descargar. Dejándola primero,
346
+ * un fallo del buzón no ancla nada y se puede reintentar; y el buzón acepta
347
+ * repetir la misma entrega, porque da el mismo hash.
348
+ */
349
+ deliverBoardResult(taskId: bigint, resultText: string): Promise<{
350
+ txHash: Hex;
351
+ resultHash: Hex;
352
+ }>;
268
353
  /**
269
354
  * Pregunta el precio de un agente sin pagar nada.
270
355
  *
package/dist/client.js CHANGED
@@ -15,13 +15,14 @@
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 } from 'viem';
18
+ import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, 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 { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
25
26
  import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js';
26
27
  /** Cuántos agentes se leen por llamada al registry. */
27
28
  const REGISTRY_PAGE = 50n;
@@ -100,6 +101,7 @@ export class PanalClient {
100
101
  walletClient;
101
102
  /** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
102
103
  indexerUrl;
104
+ buzonUrl;
103
105
  constructor(options = {}) {
104
106
  this.network = options.network ?? 'mainnet';
105
107
  const chain = chainFor(this.network);
@@ -109,6 +111,7 @@ export class PanalClient {
109
111
  'Usa network: "mainnet", o pasa `addresses` con los tuyos.');
110
112
  }
111
113
  this.indexerUrl = options.indexerUrl === undefined ? 'https://api.panal.lat' : options.indexerUrl;
114
+ this.buzonUrl = (options.buzonUrl ?? BUZON_URL).replace(/\/+$/, '');
112
115
  const transport = http(options.rpcUrl ?? chain.rpcUrls.default.http[0]);
113
116
  this.publicClient = createPublicClient({ chain, transport });
114
117
  this.account = options.account;
@@ -123,6 +126,55 @@ export class PanalClient {
123
126
  }
124
127
  return this.walletClient;
125
128
  }
129
+ /**
130
+ * Comprueba ANTES de firmar que la wallet cubre lo que Monad va a reservar.
131
+ *
132
+ * Monad bloquea `gas_limit × maxFeePerGas` antes de ejecutar, aunque luego
133
+ * cobre bastante menos: darse de alta estima ~264.000 de gas, que a 122 gwei
134
+ * son 0,032 MON de reserva (medido el 2026-09-14). Con menos, el nodo
135
+ * rechaza la transacción con «Signer had insufficient balance» y viem lo
136
+ * presenta como un revert del contrato, que no llegó a ejecutarse. Y hay una
137
+ * segunda trampa: reintentar tras recargar con el mismo nonce y las mismas
138
+ * comisiones firma una transacción idéntica, y el nodo repite el rechazo sin
139
+ * volver a mirar el saldo.
140
+ *
141
+ * Si no se puede estimar —RPC caído, o una llamada que el contrato rechazaría
142
+ * por otro motivo— no se bloquea nada: que hable el error de verdad.
143
+ */
144
+ async comprobarReserva(que, llamada) {
145
+ let gas;
146
+ let maxFeePerGas;
147
+ let saldo;
148
+ try {
149
+ [gas, { maxFeePerGas }, saldo] = await Promise.all([
150
+ this.publicClient.estimateContractGas({ ...llamada, account: this.account }),
151
+ this.publicClient.estimateFeesPerGas(),
152
+ this.publicClient.getBalance({ address: this.account.address }),
153
+ ]);
154
+ }
155
+ catch {
156
+ return;
157
+ }
158
+ const reserva = gas * maxFeePerGas + (llamada.value ?? 0n);
159
+ if (saldo < reserva) {
160
+ throw new Error(`${que}: Monad reserva ${formatEther(reserva)} MON por adelantado (límite de gas × precio máximo), ` +
161
+ `aunque luego cobre menos. ${this.account.address} tiene ${formatEther(saldo)} MON: ` +
162
+ `faltan ${formatEther(reserva - saldo)} MON. No se ha enviado nada.`);
163
+ }
164
+ }
165
+ /**
166
+ * Espera el recibo y comprueba que la transacción SALIÓ BIEN.
167
+ *
168
+ * Esperar el recibo no basta: un revert también llega con recibo. Sin mirar
169
+ * `status`, una transacción que gastó gas y no cambió nada volvía como si
170
+ * todo hubiera ido bien.
171
+ */
172
+ async esperarExito(hash, que) {
173
+ const recibo = await this.publicClient.waitForTransactionReceipt({ hash });
174
+ if (recibo.status !== 'success') {
175
+ throw new Error(`${que} revirtió (tx ${hash}): se cobró el gas y no cambió nada.`);
176
+ }
177
+ }
126
178
  // -------------------------------------------------------------------------
127
179
  // Lectura
128
180
  // -------------------------------------------------------------------------
@@ -523,15 +575,21 @@ export class PanalClient {
523
575
  */
524
576
  async registerAgent(params) {
525
577
  const wallet = this.wallet();
526
- const hash = await wallet.writeContract({
578
+ const llamada = {
527
579
  address: this.addresses.registry,
528
580
  abi: registryAbi,
529
581
  functionName: 'registerAgent',
530
582
  args: [formatAgentMetadata(params.metadata), params.pricePerTask, params.currency ?? NATIVE_CURRENCY],
583
+ };
584
+ await this.comprobarReserva('No se puede dar de alta', llamada);
585
+ const hash = await wallet.writeContract({
586
+ ...llamada,
587
+ abi: registryAbi,
588
+ functionName: 'registerAgent',
531
589
  chain: chainFor(this.network),
532
590
  account: this.account,
533
591
  });
534
- await this.publicClient.waitForTransactionReceipt({ hash });
592
+ await this.esperarExito(hash, 'El alta');
535
593
  return hash;
536
594
  }
537
595
  /** Cambia el nombre, la descripción, las skills o el endpoint publicados. */
@@ -605,7 +663,7 @@ export class PanalClient {
605
663
  chain: chainFor(this.network),
606
664
  account: this.account,
607
665
  });
608
- await this.publicClient.waitForTransactionReceipt({ hash: txHash });
666
+ await this.esperarExito(txHash, `La entrega de #${taskId}`);
609
667
  return { txHash, resultHash };
610
668
  }
611
669
  /**
@@ -633,6 +691,194 @@ export class PanalClient {
633
691
  return found;
634
692
  }
635
693
  // -------------------------------------------------------------------------
694
+ // El tablón: encargos sin dueño que coge el primer agente que los quiera.
695
+ //
696
+ // El orden de uso es el de estos cuatro métodos: mirar, coger, leer y
697
+ // entregar. Entre coger y leer no hay atajo posible: el encargo solo se le
698
+ // enseña a quien ya figura en la cadena como su trabajador.
699
+ // -------------------------------------------------------------------------
700
+ /**
701
+ * Los encargos del tablón que se pueden coger AHORA.
702
+ *
703
+ * Lo que sirve el buzón no se da por bueno: cada anuncio se comprueba contra
704
+ * la firma de su cliente —si no cuadra, el buzón lo ha cambiado o se lo ha
705
+ * inventado— y cada tarea contra la cadena, porque el buzón no se entera de
706
+ * que alguien la cogió, la canceló o se le pasó el plazo. Sin esto, un
707
+ * programa gastaría gas intentando coger encargos que ya no existen.
708
+ *
709
+ * `limit` acota cuántos anuncios se cruzan con la cadena, empezando por los
710
+ * más recientes: cada uno es una lectura al RPC.
711
+ */
712
+ async listBoard(options = {}) {
713
+ const limit = options.limit ?? 50;
714
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/lista`, {
715
+ timeoutMs: 10_000,
716
+ maxBytes: 2_000_000,
717
+ });
718
+ if (status !== 200)
719
+ throw new Error(`El buzón respondió ${status} al pedir el tablón.`);
720
+ const ofertas = (JSON.parse(text).ofertas ?? [])
721
+ .slice()
722
+ .sort((a, b) => b.publicada - a.publicada)
723
+ .slice(0, limit);
724
+ const ahora = BigInt(Math.floor(Date.now() / 1000));
725
+ const libres = [];
726
+ for (const o of ofertas) {
727
+ let taskId;
728
+ try {
729
+ taskId = BigInt(o.taskId);
730
+ }
731
+ catch {
732
+ continue;
733
+ }
734
+ const firmada = await verifyMessage({
735
+ address: getAddress(o.cliente),
736
+ message: ofertaSignMessage(taskId, o.publico),
737
+ signature: o.firma,
738
+ }).catch(() => false);
739
+ if (!firmada)
740
+ continue;
741
+ const task = await this.getTask(taskId).catch(() => null);
742
+ if (!task)
743
+ continue;
744
+ if (task.status !== TaskStatus.Open)
745
+ continue;
746
+ if (task.worker.toLowerCase() !== TABLON)
747
+ continue;
748
+ if (task.deadline <= ahora)
749
+ continue;
750
+ // El anuncio lo firmó alguien, pero la tarea es de quien la pagó: si no
751
+ // coinciden, el anuncio no es de esta tarea.
752
+ if (task.client.toLowerCase() !== o.cliente.toLowerCase())
753
+ continue;
754
+ libres.push({
755
+ taskId,
756
+ anuncio: o.publico,
757
+ cliente: task.client,
758
+ amount: task.amount,
759
+ currency: task.currency,
760
+ deadline: task.deadline,
761
+ taskHash: task.taskHash,
762
+ publicada: o.publicada,
763
+ });
764
+ }
765
+ return libres;
766
+ }
767
+ /**
768
+ * Coge un encargo del tablón: desde aquí eres su trabajador en la cadena.
769
+ *
770
+ * Lo que el contrato rechazaría se comprueba antes, gratis, para decir POR QUÉ
771
+ * en vez de devolver un revert que no explica nada: que ya la cogió otro, que
772
+ * es tuya, que venció, o que no eres un agente activo. Esto último es lo que
773
+ * pide `claimTask` y lo que más fácil se olvida: coger trabajo exige estar
774
+ * registrado y dado de alta.
775
+ */
776
+ async claimTask(taskId) {
777
+ const wallet = this.wallet();
778
+ const yo = this.account.address;
779
+ const task = await this.getTask(taskId);
780
+ if (task.status !== TaskStatus.Open) {
781
+ throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se coge lo que sigue abierto.`);
782
+ }
783
+ if (task.worker.toLowerCase() !== TABLON) {
784
+ throw new Error(`La tarea #${taskId} ya la cogió ${task.worker}.`);
785
+ }
786
+ if (task.client.toLowerCase() === yo.toLowerCase()) {
787
+ throw new Error(`La tarea #${taskId} la publicaste tú: el contrato no deja coger un encargo propio.`);
788
+ }
789
+ if (task.deadline <= BigInt(Math.floor(Date.now() / 1000))) {
790
+ throw new Error(`La tarea #${taskId} ya venció: no daría tiempo a entregarla.`);
791
+ }
792
+ const ficha = await this.leerAgente(yo);
793
+ if (!ficha.active) {
794
+ throw new Error(`${yo} no es un agente activo en el registro, y claimTask solo acepta agentes activos. ` +
795
+ 'Regístrate (o reactívate) antes de coger trabajo.');
796
+ }
797
+ const txHash = await wallet.writeContract({
798
+ address: this.addresses.escrow,
799
+ abi: escrowAbi,
800
+ functionName: 'claimTask',
801
+ args: [taskId],
802
+ chain: chainFor(this.network),
803
+ account: this.account,
804
+ });
805
+ await this.esperarExito(txHash, `Coger la tarea #${taskId}`);
806
+ return { txHash };
807
+ }
808
+ /**
809
+ * Lee el encargo de una tarea del tablón que ya has cogido.
810
+ *
811
+ * Se comprueba su keccak256 contra el `taskHash` de la cadena. Ese hash es lo
812
+ * que se pagó y lo que un árbitro miraría en una disputa, así que un texto que
813
+ * no cuadre no se devuelve: trabajar sobre él sería cumplir algo que nadie
814
+ * encargó.
815
+ */
816
+ async readBoardBrief(taskId) {
817
+ const wallet = this.wallet();
818
+ const yo = this.account.address;
819
+ const task = await this.getTask(taskId);
820
+ if (task.worker.toLowerCase() !== yo.toLowerCase()) {
821
+ throw new Error(task.worker.toLowerCase() === TABLON
822
+ ? `La tarea #${taskId} todavía no la has cogido: llama antes a claimTask.`
823
+ : `La tarea #${taskId} la cogió ${task.worker}, no tú.`);
824
+ }
825
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
826
+ const firma = await wallet.signMessage({ account: this.account, message: encargoSignMessage(taskId, expira) });
827
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/encargo/${taskId}`, {
828
+ headers: { 'x-panal-address': yo, 'x-panal-signature': firma, 'x-panal-expira': String(expira) },
829
+ timeoutMs: 15_000,
830
+ maxBytes: 1_000_000,
831
+ });
832
+ if (status === 404) {
833
+ throw new Error(`El buzón no tiene el encargo de la tarea #${taskId} (hash ${task.taskHash}): ` +
834
+ 'su cliente pagó pero no llegó a dejar el texto.');
835
+ }
836
+ if (status !== 200)
837
+ throw new Error(`El buzón respondió ${status} al pedir el encargo #${taskId}.`);
838
+ const { brief } = JSON.parse(text);
839
+ if (typeof brief !== 'string')
840
+ throw new Error(`El buzón devolvió el encargo #${taskId} sin texto.`);
841
+ if (keccak256(toBytes(brief)).toLowerCase() !== task.taskHash.toLowerCase()) {
842
+ throw new Error(`El encargo #${taskId} que sirve el buzón no cuadra con el taskHash de la cadena: no se trabaja sobre él.`);
843
+ }
844
+ return brief;
845
+ }
846
+ /**
847
+ * Entrega una tarea del tablón: deja el texto en el buzón y ancla su hash.
848
+ *
849
+ * EN ESE ORDEN, y no al revés. El cliente recoge la entrega del buzón, porque
850
+ * cuando publicó el encargo no sabía quién lo iba a coger ni dónde vive su
851
+ * servidor. Si se anclara primero y el buzón fallara después, el cliente
852
+ * vería una entrega en la cadena que no puede descargar. Dejándola primero,
853
+ * un fallo del buzón no ancla nada y se puede reintentar; y el buzón acepta
854
+ * repetir la misma entrega, porque da el mismo hash.
855
+ */
856
+ async deliverBoardResult(taskId, resultText) {
857
+ const wallet = this.wallet();
858
+ const yo = this.account.address;
859
+ const task = await this.getTask(taskId);
860
+ if (task.worker.toLowerCase() !== yo.toLowerCase()) {
861
+ throw new Error(`La tarea #${taskId} está asignada a ${task.worker}, no a ti.`);
862
+ }
863
+ if (task.status !== TaskStatus.Open) {
864
+ throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se entrega lo que sigue abierto.`);
865
+ }
866
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
867
+ const firma = await wallet.signMessage({ account: this.account, message: entregaSignMessage(taskId, expira) });
868
+ const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/entrega/${taskId}`, {
869
+ method: 'POST',
870
+ headers: { 'content-type': 'application/json' },
871
+ body: JSON.stringify({ entrega: resultText, address: yo, signature: firma, expira }),
872
+ timeoutMs: 30_000,
873
+ maxBytes: 100_000,
874
+ });
875
+ if (status !== 200) {
876
+ throw new Error(`El buzón no aceptó la entrega #${taskId} (${status}: ${text.slice(0, 160)}). ` +
877
+ 'No se ha anclado nada en la cadena: se puede reintentar.');
878
+ }
879
+ return this.deliverResult(taskId, resultText);
880
+ }
881
+ // -------------------------------------------------------------------------
636
882
  // Llamar a otro agente y pagarle al momento (x402).
637
883
  //
638
884
  // Esto es lo que permite que un agente contrate a otro sin humano de por
package/dist/files.js CHANGED
@@ -65,6 +65,7 @@ export function sanitizeFileName(name) {
65
65
  // y `..\\..\\c.pdf` acaban los dos en `c.pdf`.
66
66
  const base = name.split(/[/\\]/).pop() ?? '';
67
67
  const limpio = base
68
+ // eslint-disable-next-line no-control-regex -- son justo los que hay que quitar
68
69
  .replace(/[\u0000-\u001f\u007f]/g, '') // caracteres de control
69
70
  .replace(/^\.+/, '') // nada de nombres que empiezan por punto: '..' incluido
70
71
  .trim();
package/dist/index.d.ts CHANGED
@@ -44,5 +44,7 @@ export { componerNivel, conTextoDeLaFicha, esTokenDeNivel, leerNivelDeSegmento,
44
44
  export { esTokenDeTipo, leerTipo, leerTipoDeSegmento, tokenDeTipo } from './tipo.js';
45
45
  export type { TipoDeAgente } from './tipo.js';
46
46
  export { rutaDeAgente } from './net.js';
47
+ export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
48
+ export type { EncargoDelTablon } from './tablon.js';
47
49
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
48
50
  export type { Idioma } from './idiomas.js';
package/dist/index.js CHANGED
@@ -51,4 +51,8 @@ export { esTokenDeTipo, leerTipo, leerTipoDeSegmento, tokenDeTipo } from './tipo
51
51
  // Unir una ruta con la URL de un agente. Ver por qué en `net.ts`: un agente
52
52
  // puede vivir en un subcamino, y `new URL('/x', base)` se lo come.
53
53
  export { rutaDeAgente } from './net.js';
54
+ // El tablón: encargos sin dueño para que los coja un programa. Los mensajes de
55
+ // firma se exportan porque cualquiera que hable con el buzón sin este cliente
56
+ // —otro lenguaje, otro runtime— necesita producir exactamente estos bytes.
57
+ export { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
54
58
  export { fichaEnIdioma, IDIOMAS, NOMBRE_IDIOMA, normalizarIdioma } from './idiomas.js';
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Panal — el tablón: encargos publicados SIN dueño, para que los coja un programa.
3
+ * ───────────────────────────────────────────────────────────────────────────
4
+ * POR QUÉ ESTÁ AQUÍ
5
+ * El escrow acepta `createTask(worker = address(0))` y `claimTask` desde que se
6
+ * desplegó, y la web tiene un tablón donde un humano con ratón publica y coge.
7
+ * Lo que no existía era la misma puerta para un PROGRAMA: el tablón se pensó
8
+ * para que un agente autónomo tomara trabajo sin que nadie hiciera clic, y sin
9
+ * el SDK solo podía hacerlo alguien delante de un navegador.
10
+ *
11
+ * DÓNDE VIVE CADA COSA
12
+ * La cadena guarda cuánto paga, cuándo vence y el HASH del encargo. El texto
13
+ * vive en el buzón (`bot/src/buzon.ts`), colgado de la dirección cero como si
14
+ * fuera un agente más:
15
+ *
16
+ * GET <buzón>/0x000…000/lista los anuncios, sin firma
17
+ * GET <buzón>/0x000…000/encargo/:taskId el encargo, solo para quien lo cogió
18
+ * POST <buzón>/0x000…000/entrega/:taskId lo entregado, para que lo recoja el cliente
19
+ *
20
+ * DOS TEXTOS, Y NO ES REDUNDANCIA. El ANUNCIO se lee sin coger nada y lo firma
21
+ * el cliente, así que el buzón no lo puede cambiar. El ENCARGO solo lo ve quien
22
+ * ya lo ha cogido, y su keccak256 es el `taskHash` de la cadena.
23
+ *
24
+ * Los mensajes de firma de este archivo tienen que ser IDÉNTICOS a los del
25
+ * buzón, byte a byte: una sola diferencia y todas las firmas salen «inválidas»
26
+ * sin que nada diga por qué.
27
+ */
28
+ import type { Address, Hex } from 'viem';
29
+ /** El tablón cuelga de la dirección cero: es de todos y de nadie. */
30
+ export declare const TABLON: Address;
31
+ /** Dónde está el buzón que guarda los textos del tablón. */
32
+ export declare const BUZON_URL = "https://api.panal.lat/buzon";
33
+ /**
34
+ * Cuánto vale como mucho una firma de lectura o de entrega, en segundos.
35
+ *
36
+ * El buzón rechaza cualquier `expira` más allá de 15 minutos. Se firma con
37
+ * menos margen para no rozar el límite si el reloj de quien firma va adelantado.
38
+ */
39
+ export declare const VENTANA_FIRMA_S: number;
40
+ /** Lo que firma el cliente al publicar: el anuncio, atado a su tarea. */
41
+ export declare function ofertaSignMessage(taskId: bigint, publico: string): string;
42
+ /** Lo que firma el trabajador para leer el encargo que ha cogido. */
43
+ export declare function encargoSignMessage(taskId: bigint, expira: number): string;
44
+ /** Lo que firma el trabajador para dejar su entrega en el buzón. */
45
+ export declare function entregaSignMessage(taskId: bigint, expira: number): string;
46
+ /** Un encargo del tablón que se puede coger ahora mismo. */
47
+ export interface EncargoDelTablon {
48
+ taskId: bigint;
49
+ /** El anuncio: lo que el cliente escribió PARA que se lea. No es el encargo. */
50
+ anuncio: string;
51
+ /** Quien lo publicó y firmó el anuncio. */
52
+ cliente: Address;
53
+ /** Lo que paga, en las unidades mínimas de `currency`. */
54
+ amount: bigint;
55
+ /** `address(0)` = MON nativo; si no, el token. */
56
+ currency: Address;
57
+ /** Hasta cuándo se puede entregar, en segundos unix. */
58
+ deadline: bigint;
59
+ /** El hash del encargo de verdad: lo que habrá que cumplir al cogerlo. */
60
+ taskHash: Hex;
61
+ /** Cuándo se publicó el anuncio en el buzón, en milisegundos. */
62
+ publicada: number;
63
+ }
64
+ /** Una oferta tal como la sirve el buzón, antes de cruzarla con la cadena. */
65
+ export interface OfertaCruda {
66
+ taskId: string;
67
+ publico: string;
68
+ cliente: string;
69
+ firma: string;
70
+ publicada: number;
71
+ }
package/dist/tablon.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Panal — el tablón: encargos publicados SIN dueño, para que los coja un programa.
3
+ * ───────────────────────────────────────────────────────────────────────────
4
+ * POR QUÉ ESTÁ AQUÍ
5
+ * El escrow acepta `createTask(worker = address(0))` y `claimTask` desde que se
6
+ * desplegó, y la web tiene un tablón donde un humano con ratón publica y coge.
7
+ * Lo que no existía era la misma puerta para un PROGRAMA: el tablón se pensó
8
+ * para que un agente autónomo tomara trabajo sin que nadie hiciera clic, y sin
9
+ * el SDK solo podía hacerlo alguien delante de un navegador.
10
+ *
11
+ * DÓNDE VIVE CADA COSA
12
+ * La cadena guarda cuánto paga, cuándo vence y el HASH del encargo. El texto
13
+ * vive en el buzón (`bot/src/buzon.ts`), colgado de la dirección cero como si
14
+ * fuera un agente más:
15
+ *
16
+ * GET <buzón>/0x000…000/lista los anuncios, sin firma
17
+ * GET <buzón>/0x000…000/encargo/:taskId el encargo, solo para quien lo cogió
18
+ * POST <buzón>/0x000…000/entrega/:taskId lo entregado, para que lo recoja el cliente
19
+ *
20
+ * DOS TEXTOS, Y NO ES REDUNDANCIA. El ANUNCIO se lee sin coger nada y lo firma
21
+ * el cliente, así que el buzón no lo puede cambiar. El ENCARGO solo lo ve quien
22
+ * ya lo ha cogido, y su keccak256 es el `taskHash` de la cadena.
23
+ *
24
+ * Los mensajes de firma de este archivo tienen que ser IDÉNTICOS a los del
25
+ * buzón, byte a byte: una sola diferencia y todas las firmas salen «inválidas»
26
+ * sin que nada diga por qué.
27
+ */
28
+ import { keccak256, toBytes } from 'viem';
29
+ /** El tablón cuelga de la dirección cero: es de todos y de nadie. */
30
+ export const TABLON = '0x0000000000000000000000000000000000000000';
31
+ /** Dónde está el buzón que guarda los textos del tablón. */
32
+ export const BUZON_URL = 'https://api.panal.lat/buzon';
33
+ /**
34
+ * Cuánto vale como mucho una firma de lectura o de entrega, en segundos.
35
+ *
36
+ * El buzón rechaza cualquier `expira` más allá de 15 minutos. Se firma con
37
+ * menos margen para no rozar el límite si el reloj de quien firma va adelantado.
38
+ */
39
+ export const VENTANA_FIRMA_S = 10 * 60;
40
+ /** Lo que firma el cliente al publicar: el anuncio, atado a su tarea. */
41
+ export function ofertaSignMessage(taskId, publico) {
42
+ return `Panal tablón #${taskId.toString()} · ${keccak256(toBytes(publico))}`;
43
+ }
44
+ /** Lo que firma el trabajador para leer el encargo que ha cogido. */
45
+ export function encargoSignMessage(taskId, expira) {
46
+ return `Panal encargo #${taskId.toString()} · ${expira}`;
47
+ }
48
+ /** Lo que firma el trabajador para dejar su entrega en el buzón. */
49
+ export function entregaSignMessage(taskId, expira) {
50
+ return `Panal entrega #${taskId.toString()} · ${expira}`;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.json",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts",
48
+ "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts && tsx test/tablon.test.ts && tsx test/reserva.test.ts",
49
49
  "test:nombres": "tsx test/nombres.test.ts"
50
50
  }
51
51
  }