@panal/sdk 0.17.1 → 0.18.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 +11 -1
- package/dist/abis.d.ts +9 -0
- package/dist/abis.js +9 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +192 -1
- package/dist/files.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/tablon.d.ts +71 -0
- package/dist/tablon.js +51 -0
- package/package.json +2 -2
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
|
-
**
|
|
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,6 +125,7 @@ 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;
|
|
@@ -265,6 +276,56 @@ export declare class PanalClient {
|
|
|
265
276
|
limit?: number;
|
|
266
277
|
status?: TaskStatus;
|
|
267
278
|
}): Promise<Task[]>;
|
|
279
|
+
/**
|
|
280
|
+
* Los encargos del tablón que se pueden coger AHORA.
|
|
281
|
+
*
|
|
282
|
+
* Lo que sirve el buzón no se da por bueno: cada anuncio se comprueba contra
|
|
283
|
+
* la firma de su cliente —si no cuadra, el buzón lo ha cambiado o se lo ha
|
|
284
|
+
* inventado— y cada tarea contra la cadena, porque el buzón no se entera de
|
|
285
|
+
* que alguien la cogió, la canceló o se le pasó el plazo. Sin esto, un
|
|
286
|
+
* programa gastaría gas intentando coger encargos que ya no existen.
|
|
287
|
+
*
|
|
288
|
+
* `limit` acota cuántos anuncios se cruzan con la cadena, empezando por los
|
|
289
|
+
* más recientes: cada uno es una lectura al RPC.
|
|
290
|
+
*/
|
|
291
|
+
listBoard(options?: {
|
|
292
|
+
limit?: number;
|
|
293
|
+
}): Promise<EncargoDelTablon[]>;
|
|
294
|
+
/**
|
|
295
|
+
* Coge un encargo del tablón: desde aquí eres su trabajador en la cadena.
|
|
296
|
+
*
|
|
297
|
+
* Lo que el contrato rechazaría se comprueba antes, gratis, para decir POR QUÉ
|
|
298
|
+
* en vez de devolver un revert que no explica nada: que ya la cogió otro, que
|
|
299
|
+
* es tuya, que venció, o que no eres un agente activo. Esto último es lo que
|
|
300
|
+
* pide `claimTask` y lo que más fácil se olvida: coger trabajo exige estar
|
|
301
|
+
* registrado y dado de alta.
|
|
302
|
+
*/
|
|
303
|
+
claimTask(taskId: bigint): Promise<{
|
|
304
|
+
txHash: Hex;
|
|
305
|
+
}>;
|
|
306
|
+
/**
|
|
307
|
+
* Lee el encargo de una tarea del tablón que ya has cogido.
|
|
308
|
+
*
|
|
309
|
+
* Se comprueba su keccak256 contra el `taskHash` de la cadena. Ese hash es lo
|
|
310
|
+
* que se pagó y lo que un árbitro miraría en una disputa, así que un texto que
|
|
311
|
+
* no cuadre no se devuelve: trabajar sobre él sería cumplir algo que nadie
|
|
312
|
+
* encargó.
|
|
313
|
+
*/
|
|
314
|
+
readBoardBrief(taskId: bigint): Promise<string>;
|
|
315
|
+
/**
|
|
316
|
+
* Entrega una tarea del tablón: deja el texto en el buzón y ancla su hash.
|
|
317
|
+
*
|
|
318
|
+
* EN ESE ORDEN, y no al revés. El cliente recoge la entrega del buzón, porque
|
|
319
|
+
* cuando publicó el encargo no sabía quién lo iba a coger ni dónde vive su
|
|
320
|
+
* servidor. Si se anclara primero y el buzón fallara después, el cliente
|
|
321
|
+
* vería una entrega en la cadena que no puede descargar. Dejándola primero,
|
|
322
|
+
* un fallo del buzón no ancla nada y se puede reintentar; y el buzón acepta
|
|
323
|
+
* repetir la misma entrega, porque da el mismo hash.
|
|
324
|
+
*/
|
|
325
|
+
deliverBoardResult(taskId: bigint, resultText: string): Promise<{
|
|
326
|
+
txHash: Hex;
|
|
327
|
+
resultHash: Hex;
|
|
328
|
+
}>;
|
|
268
329
|
/**
|
|
269
330
|
* Pregunta el precio de un agente sin pagar nada.
|
|
270
331
|
*
|
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;
|
|
@@ -633,6 +636,194 @@ export class PanalClient {
|
|
|
633
636
|
return found;
|
|
634
637
|
}
|
|
635
638
|
// -------------------------------------------------------------------------
|
|
639
|
+
// El tablón: encargos sin dueño que coge el primer agente que los quiera.
|
|
640
|
+
//
|
|
641
|
+
// El orden de uso es el de estos cuatro métodos: mirar, coger, leer y
|
|
642
|
+
// entregar. Entre coger y leer no hay atajo posible: el encargo solo se le
|
|
643
|
+
// enseña a quien ya figura en la cadena como su trabajador.
|
|
644
|
+
// -------------------------------------------------------------------------
|
|
645
|
+
/**
|
|
646
|
+
* Los encargos del tablón que se pueden coger AHORA.
|
|
647
|
+
*
|
|
648
|
+
* Lo que sirve el buzón no se da por bueno: cada anuncio se comprueba contra
|
|
649
|
+
* la firma de su cliente —si no cuadra, el buzón lo ha cambiado o se lo ha
|
|
650
|
+
* inventado— y cada tarea contra la cadena, porque el buzón no se entera de
|
|
651
|
+
* que alguien la cogió, la canceló o se le pasó el plazo. Sin esto, un
|
|
652
|
+
* programa gastaría gas intentando coger encargos que ya no existen.
|
|
653
|
+
*
|
|
654
|
+
* `limit` acota cuántos anuncios se cruzan con la cadena, empezando por los
|
|
655
|
+
* más recientes: cada uno es una lectura al RPC.
|
|
656
|
+
*/
|
|
657
|
+
async listBoard(options = {}) {
|
|
658
|
+
const limit = options.limit ?? 50;
|
|
659
|
+
const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/lista`, {
|
|
660
|
+
timeoutMs: 10_000,
|
|
661
|
+
maxBytes: 2_000_000,
|
|
662
|
+
});
|
|
663
|
+
if (status !== 200)
|
|
664
|
+
throw new Error(`El buzón respondió ${status} al pedir el tablón.`);
|
|
665
|
+
const ofertas = (JSON.parse(text).ofertas ?? [])
|
|
666
|
+
.slice()
|
|
667
|
+
.sort((a, b) => b.publicada - a.publicada)
|
|
668
|
+
.slice(0, limit);
|
|
669
|
+
const ahora = BigInt(Math.floor(Date.now() / 1000));
|
|
670
|
+
const libres = [];
|
|
671
|
+
for (const o of ofertas) {
|
|
672
|
+
let taskId;
|
|
673
|
+
try {
|
|
674
|
+
taskId = BigInt(o.taskId);
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const firmada = await verifyMessage({
|
|
680
|
+
address: getAddress(o.cliente),
|
|
681
|
+
message: ofertaSignMessage(taskId, o.publico),
|
|
682
|
+
signature: o.firma,
|
|
683
|
+
}).catch(() => false);
|
|
684
|
+
if (!firmada)
|
|
685
|
+
continue;
|
|
686
|
+
const task = await this.getTask(taskId).catch(() => null);
|
|
687
|
+
if (!task)
|
|
688
|
+
continue;
|
|
689
|
+
if (task.status !== TaskStatus.Open)
|
|
690
|
+
continue;
|
|
691
|
+
if (task.worker.toLowerCase() !== TABLON)
|
|
692
|
+
continue;
|
|
693
|
+
if (task.deadline <= ahora)
|
|
694
|
+
continue;
|
|
695
|
+
// El anuncio lo firmó alguien, pero la tarea es de quien la pagó: si no
|
|
696
|
+
// coinciden, el anuncio no es de esta tarea.
|
|
697
|
+
if (task.client.toLowerCase() !== o.cliente.toLowerCase())
|
|
698
|
+
continue;
|
|
699
|
+
libres.push({
|
|
700
|
+
taskId,
|
|
701
|
+
anuncio: o.publico,
|
|
702
|
+
cliente: task.client,
|
|
703
|
+
amount: task.amount,
|
|
704
|
+
currency: task.currency,
|
|
705
|
+
deadline: task.deadline,
|
|
706
|
+
taskHash: task.taskHash,
|
|
707
|
+
publicada: o.publicada,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
return libres;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Coge un encargo del tablón: desde aquí eres su trabajador en la cadena.
|
|
714
|
+
*
|
|
715
|
+
* Lo que el contrato rechazaría se comprueba antes, gratis, para decir POR QUÉ
|
|
716
|
+
* en vez de devolver un revert que no explica nada: que ya la cogió otro, que
|
|
717
|
+
* es tuya, que venció, o que no eres un agente activo. Esto último es lo que
|
|
718
|
+
* pide `claimTask` y lo que más fácil se olvida: coger trabajo exige estar
|
|
719
|
+
* registrado y dado de alta.
|
|
720
|
+
*/
|
|
721
|
+
async claimTask(taskId) {
|
|
722
|
+
const wallet = this.wallet();
|
|
723
|
+
const yo = this.account.address;
|
|
724
|
+
const task = await this.getTask(taskId);
|
|
725
|
+
if (task.status !== TaskStatus.Open) {
|
|
726
|
+
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se coge lo que sigue abierto.`);
|
|
727
|
+
}
|
|
728
|
+
if (task.worker.toLowerCase() !== TABLON) {
|
|
729
|
+
throw new Error(`La tarea #${taskId} ya la cogió ${task.worker}.`);
|
|
730
|
+
}
|
|
731
|
+
if (task.client.toLowerCase() === yo.toLowerCase()) {
|
|
732
|
+
throw new Error(`La tarea #${taskId} la publicaste tú: el contrato no deja coger un encargo propio.`);
|
|
733
|
+
}
|
|
734
|
+
if (task.deadline <= BigInt(Math.floor(Date.now() / 1000))) {
|
|
735
|
+
throw new Error(`La tarea #${taskId} ya venció: no daría tiempo a entregarla.`);
|
|
736
|
+
}
|
|
737
|
+
const ficha = await this.leerAgente(yo);
|
|
738
|
+
if (!ficha.active) {
|
|
739
|
+
throw new Error(`${yo} no es un agente activo en el registro, y claimTask solo acepta agentes activos. ` +
|
|
740
|
+
'Regístrate (o reactívate) antes de coger trabajo.');
|
|
741
|
+
}
|
|
742
|
+
const txHash = await wallet.writeContract({
|
|
743
|
+
address: this.addresses.escrow,
|
|
744
|
+
abi: escrowAbi,
|
|
745
|
+
functionName: 'claimTask',
|
|
746
|
+
args: [taskId],
|
|
747
|
+
chain: chainFor(this.network),
|
|
748
|
+
account: this.account,
|
|
749
|
+
});
|
|
750
|
+
await this.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
751
|
+
return { txHash };
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Lee el encargo de una tarea del tablón que ya has cogido.
|
|
755
|
+
*
|
|
756
|
+
* Se comprueba su keccak256 contra el `taskHash` de la cadena. Ese hash es lo
|
|
757
|
+
* que se pagó y lo que un árbitro miraría en una disputa, así que un texto que
|
|
758
|
+
* no cuadre no se devuelve: trabajar sobre él sería cumplir algo que nadie
|
|
759
|
+
* encargó.
|
|
760
|
+
*/
|
|
761
|
+
async readBoardBrief(taskId) {
|
|
762
|
+
const wallet = this.wallet();
|
|
763
|
+
const yo = this.account.address;
|
|
764
|
+
const task = await this.getTask(taskId);
|
|
765
|
+
if (task.worker.toLowerCase() !== yo.toLowerCase()) {
|
|
766
|
+
throw new Error(task.worker.toLowerCase() === TABLON
|
|
767
|
+
? `La tarea #${taskId} todavía no la has cogido: llama antes a claimTask.`
|
|
768
|
+
: `La tarea #${taskId} la cogió ${task.worker}, no tú.`);
|
|
769
|
+
}
|
|
770
|
+
const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
|
|
771
|
+
const firma = await wallet.signMessage({ account: this.account, message: encargoSignMessage(taskId, expira) });
|
|
772
|
+
const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/encargo/${taskId}`, {
|
|
773
|
+
headers: { 'x-panal-address': yo, 'x-panal-signature': firma, 'x-panal-expira': String(expira) },
|
|
774
|
+
timeoutMs: 15_000,
|
|
775
|
+
maxBytes: 1_000_000,
|
|
776
|
+
});
|
|
777
|
+
if (status === 404) {
|
|
778
|
+
throw new Error(`El buzón no tiene el encargo de la tarea #${taskId} (hash ${task.taskHash}): ` +
|
|
779
|
+
'su cliente pagó pero no llegó a dejar el texto.');
|
|
780
|
+
}
|
|
781
|
+
if (status !== 200)
|
|
782
|
+
throw new Error(`El buzón respondió ${status} al pedir el encargo #${taskId}.`);
|
|
783
|
+
const { brief } = JSON.parse(text);
|
|
784
|
+
if (typeof brief !== 'string')
|
|
785
|
+
throw new Error(`El buzón devolvió el encargo #${taskId} sin texto.`);
|
|
786
|
+
if (keccak256(toBytes(brief)).toLowerCase() !== task.taskHash.toLowerCase()) {
|
|
787
|
+
throw new Error(`El encargo #${taskId} que sirve el buzón no cuadra con el taskHash de la cadena: no se trabaja sobre él.`);
|
|
788
|
+
}
|
|
789
|
+
return brief;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Entrega una tarea del tablón: deja el texto en el buzón y ancla su hash.
|
|
793
|
+
*
|
|
794
|
+
* EN ESE ORDEN, y no al revés. El cliente recoge la entrega del buzón, porque
|
|
795
|
+
* cuando publicó el encargo no sabía quién lo iba a coger ni dónde vive su
|
|
796
|
+
* servidor. Si se anclara primero y el buzón fallara después, el cliente
|
|
797
|
+
* vería una entrega en la cadena que no puede descargar. Dejándola primero,
|
|
798
|
+
* un fallo del buzón no ancla nada y se puede reintentar; y el buzón acepta
|
|
799
|
+
* repetir la misma entrega, porque da el mismo hash.
|
|
800
|
+
*/
|
|
801
|
+
async deliverBoardResult(taskId, resultText) {
|
|
802
|
+
const wallet = this.wallet();
|
|
803
|
+
const yo = this.account.address;
|
|
804
|
+
const task = await this.getTask(taskId);
|
|
805
|
+
if (task.worker.toLowerCase() !== yo.toLowerCase()) {
|
|
806
|
+
throw new Error(`La tarea #${taskId} está asignada a ${task.worker}, no a ti.`);
|
|
807
|
+
}
|
|
808
|
+
if (task.status !== TaskStatus.Open) {
|
|
809
|
+
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se entrega lo que sigue abierto.`);
|
|
810
|
+
}
|
|
811
|
+
const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
|
|
812
|
+
const firma = await wallet.signMessage({ account: this.account, message: entregaSignMessage(taskId, expira) });
|
|
813
|
+
const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/entrega/${taskId}`, {
|
|
814
|
+
method: 'POST',
|
|
815
|
+
headers: { 'content-type': 'application/json' },
|
|
816
|
+
body: JSON.stringify({ entrega: resultText, address: yo, signature: firma, expira }),
|
|
817
|
+
timeoutMs: 30_000,
|
|
818
|
+
maxBytes: 100_000,
|
|
819
|
+
});
|
|
820
|
+
if (status !== 200) {
|
|
821
|
+
throw new Error(`El buzón no aceptó la entrega #${taskId} (${status}: ${text.slice(0, 160)}). ` +
|
|
822
|
+
'No se ha anclado nada en la cadena: se puede reintentar.');
|
|
823
|
+
}
|
|
824
|
+
return this.deliverResult(taskId, resultText);
|
|
825
|
+
}
|
|
826
|
+
// -------------------------------------------------------------------------
|
|
636
827
|
// Llamar a otro agente y pagarle al momento (x402).
|
|
637
828
|
//
|
|
638
829
|
// 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';
|
package/dist/tablon.d.ts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.18.0",
|
|
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",
|
|
49
49
|
"test:nombres": "tsx test/nombres.test.ts"
|
|
50
50
|
}
|
|
51
51
|
}
|