@panal/sdk 0.18.3 → 0.19.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/LICENSE +21 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +47 -42
- package/dist/currencies.d.ts +73 -0
- package/dist/currencies.js +112 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/x402-server.d.ts +94 -3
- package/dist/x402-server.js +290 -41
- package/dist/x402.d.ts +8 -2
- package/dist/x402.js +109 -35
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Panal and Kreno (founder: Gustavo Chura C.)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/client.d.ts
CHANGED
|
@@ -152,8 +152,10 @@ export declare class PanalClient {
|
|
|
152
152
|
* Hace falta porque viem no estima: le pide al nodo que rellene la
|
|
153
153
|
* transacción (`eth_fillTransaction`), el de Monad a veces infla el límite, y
|
|
154
154
|
* Monad cobra el límite ENTERO. Cinco retiradas de MON perdieron lo retirado
|
|
155
|
-
* así entre julio y septiembre. Se pasa explícito a
|
|
156
|
-
* cuenta local en nombre de un programa que nadie está mirando
|
|
155
|
+
* así entre julio y septiembre. Se pasa explícito a TODO lo que firma una
|
|
156
|
+
* cuenta local en nombre de un programa que nadie está mirando: contratar,
|
|
157
|
+
* aprobar, entregar, disputar, darse de alta. Antes solo la retirada y el
|
|
158
|
+
* tablón, y un agente entrega en cada encargo.
|
|
157
159
|
*/
|
|
158
160
|
private gasFijo;
|
|
159
161
|
/**
|
|
@@ -445,6 +447,13 @@ export declare class PanalClient {
|
|
|
445
447
|
* cuesta ni una petición.
|
|
446
448
|
*/
|
|
447
449
|
skillsPermitidas?: string[];
|
|
450
|
+
/**
|
|
451
|
+
* En qué moneda se paga, y por tanto en qué unidades va `maxSpend`.
|
|
452
|
+
* Por defecto $PANAL. Solo se cotiza con quien cobre en ESTA: comparar
|
|
453
|
+
* precios de monedas distintas era comparar 1 USDC con 1 wei de $PANAL,
|
|
454
|
+
* y un tope en una moneda no dice nada de otra.
|
|
455
|
+
*/
|
|
456
|
+
asset?: Address;
|
|
448
457
|
}): Promise<AskResult & {
|
|
449
458
|
agent: Address;
|
|
450
459
|
skill: string;
|
package/dist/client.js
CHANGED
|
@@ -29,6 +29,13 @@ import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js
|
|
|
29
29
|
* $PANAL 103.511. Ver `withdraw()` para lo que pasó sin este tope.
|
|
30
30
|
*/
|
|
31
31
|
const TOPE_GAS_RETIRADA = 300000n;
|
|
32
|
+
/**
|
|
33
|
+
* El tope para dar de alta o cambiar la ficha. Guardar texto en la cadena
|
|
34
|
+
* cuesta por cada 32 bytes, y una ficha con el logo dentro es grande: aquí el
|
|
35
|
+
* tope solo está para cortar un disparate, no para medir lo normal. Lo normal
|
|
36
|
+
* lo da `eth_estimateGas`, que es fiable; el que inflaba era `eth_fillTransaction`.
|
|
37
|
+
*/
|
|
38
|
+
const TOPE_GAS_FICHA = 30000000n;
|
|
32
39
|
/** Cuántos agentes se leen por llamada al registry. */
|
|
33
40
|
const REGISTRY_PAGE = 50n;
|
|
34
41
|
/** Tope duro de agentes recorridos, por si el registro crece mucho. */
|
|
@@ -174,13 +181,15 @@ export class PanalClient {
|
|
|
174
181
|
* Hace falta porque viem no estima: le pide al nodo que rellene la
|
|
175
182
|
* transacción (`eth_fillTransaction`), el de Monad a veces infla el límite, y
|
|
176
183
|
* Monad cobra el límite ENTERO. Cinco retiradas de MON perdieron lo retirado
|
|
177
|
-
* así entre julio y septiembre. Se pasa explícito a
|
|
178
|
-
* cuenta local en nombre de un programa que nadie está mirando
|
|
184
|
+
* así entre julio y septiembre. Se pasa explícito a TODO lo que firma una
|
|
185
|
+
* cuenta local en nombre de un programa que nadie está mirando: contratar,
|
|
186
|
+
* aprobar, entregar, disputar, darse de alta. Antes solo la retirada y el
|
|
187
|
+
* tablón, y un agente entrega en cada encargo.
|
|
179
188
|
*/
|
|
180
|
-
async gasFijo(que, llamada) {
|
|
189
|
+
async gasFijo(que, llamada, tope = TOPE_GAS_RETIRADA) {
|
|
181
190
|
const estimado = await this.publicClient.estimateContractGas({ ...llamada, account: this.account });
|
|
182
191
|
const gas = (estimado * 11n + 9n) / 10n;
|
|
183
|
-
if (gas >
|
|
192
|
+
if (gas > tope) {
|
|
184
193
|
throw new Error(`${que}: la estimación de gas salió en ${estimado}, muy por encima de lo normal. ` +
|
|
185
194
|
'Monad cobra el límite entero: no se ha enviado nada.');
|
|
186
195
|
}
|
|
@@ -475,22 +484,19 @@ export class PanalClient {
|
|
|
475
484
|
if (!isNative) {
|
|
476
485
|
// approve por el importe exacto, no infinito: si el escrow tuviera un
|
|
477
486
|
// fallo, la exposición se limita a este encargo.
|
|
487
|
+
const aFirmar = { address: agent.currency, abi: erc20Abi, functionName: 'approve', args: [this.addresses.escrow, amount] };
|
|
478
488
|
const approveHash = await wallet.writeContract({
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
functionName: 'approve',
|
|
482
|
-
args: [this.addresses.escrow, amount],
|
|
489
|
+
...aFirmar,
|
|
490
|
+
gas: await this.gasFijo('El approve', aFirmar),
|
|
483
491
|
chain: chainFor(this.network),
|
|
484
492
|
account: this.account,
|
|
485
493
|
});
|
|
486
494
|
await this.publicClient.waitForTransactionReceipt({ hash: approveHash });
|
|
487
495
|
}
|
|
496
|
+
const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'createTask', args: [agent.address, taskHash, deadline, agent.currency, amount], value: isNative ? amount : 0n };
|
|
488
497
|
const txHash = await wallet.writeContract({
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
functionName: 'createTask',
|
|
492
|
-
args: [agent.address, taskHash, deadline, agent.currency, amount],
|
|
493
|
-
value: isNative ? amount : 0n,
|
|
498
|
+
...aFirmar,
|
|
499
|
+
gas: await this.gasFijo('El encargo', aFirmar),
|
|
494
500
|
chain: chainFor(this.network),
|
|
495
501
|
account: this.account,
|
|
496
502
|
});
|
|
@@ -517,11 +523,10 @@ export class PanalClient {
|
|
|
517
523
|
if (task.status !== TaskStatus.Delivered) {
|
|
518
524
|
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se aprueba lo entregado.`);
|
|
519
525
|
}
|
|
526
|
+
const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'approveAndRelease', args: [taskId, rating] };
|
|
520
527
|
const hash = await wallet.writeContract({
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
functionName: 'approveAndRelease',
|
|
524
|
-
args: [taskId, rating],
|
|
528
|
+
...aFirmar,
|
|
529
|
+
gas: await this.gasFijo('La aprobación', aFirmar),
|
|
525
530
|
chain: chainFor(this.network),
|
|
526
531
|
account: this.account,
|
|
527
532
|
});
|
|
@@ -550,11 +555,10 @@ export class PanalClient {
|
|
|
550
555
|
if (task.status !== TaskStatus.Open) {
|
|
551
556
|
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se cancela lo que sigue abierto.`);
|
|
552
557
|
}
|
|
558
|
+
const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'cancelTask', args: [taskId] };
|
|
553
559
|
const hash = await wallet.writeContract({
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
functionName: 'cancelTask',
|
|
557
|
-
args: [taskId],
|
|
560
|
+
...aFirmar,
|
|
561
|
+
gas: await this.gasFijo('La cancelación', aFirmar),
|
|
558
562
|
chain: chainFor(this.network),
|
|
559
563
|
account: this.account,
|
|
560
564
|
});
|
|
@@ -578,11 +582,10 @@ export class PanalClient {
|
|
|
578
582
|
if (task.status !== TaskStatus.Delivered) {
|
|
579
583
|
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se disputa lo entregado.`);
|
|
580
584
|
}
|
|
585
|
+
const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'openDispute', args: [taskId] };
|
|
581
586
|
const hash = await wallet.writeContract({
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
functionName: 'openDispute',
|
|
585
|
-
args: [taskId],
|
|
587
|
+
...aFirmar,
|
|
588
|
+
gas: await this.gasFijo('La disputa', aFirmar),
|
|
586
589
|
chain: chainFor(this.network),
|
|
587
590
|
account: this.account,
|
|
588
591
|
});
|
|
@@ -610,6 +613,7 @@ export class PanalClient {
|
|
|
610
613
|
...llamada,
|
|
611
614
|
abi: registryAbi,
|
|
612
615
|
functionName: 'registerAgent',
|
|
616
|
+
gas: await this.gasFijo('El alta', llamada, TOPE_GAS_FICHA),
|
|
613
617
|
chain: chainFor(this.network),
|
|
614
618
|
account: this.account,
|
|
615
619
|
});
|
|
@@ -619,11 +623,10 @@ export class PanalClient {
|
|
|
619
623
|
/** Cambia el nombre, la descripción, las skills o el endpoint publicados. */
|
|
620
624
|
async updateMetadata(metadata) {
|
|
621
625
|
const wallet = this.wallet();
|
|
626
|
+
const aFirmar = { address: this.addresses.registry, abi: registryAbi, functionName: 'updateMetadata', args: [formatAgentMetadata(metadata)] };
|
|
622
627
|
const hash = await wallet.writeContract({
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
functionName: 'updateMetadata',
|
|
626
|
-
args: [formatAgentMetadata(metadata)],
|
|
628
|
+
...aFirmar,
|
|
629
|
+
gas: await this.gasFijo('El cambio de ficha', aFirmar, TOPE_GAS_FICHA),
|
|
627
630
|
chain: chainFor(this.network),
|
|
628
631
|
account: this.account,
|
|
629
632
|
});
|
|
@@ -633,11 +636,10 @@ export class PanalClient {
|
|
|
633
636
|
/** Cambia el precio por tarea, y opcionalmente la moneda en la que cobras. */
|
|
634
637
|
async updatePrice(pricePerTask, currency = NATIVE_CURRENCY) {
|
|
635
638
|
const wallet = this.wallet();
|
|
639
|
+
const aFirmar = { address: this.addresses.registry, abi: registryAbi, functionName: 'updatePrice', args: [pricePerTask, currency] };
|
|
636
640
|
const hash = await wallet.writeContract({
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
functionName: 'updatePrice',
|
|
640
|
-
args: [pricePerTask, currency],
|
|
641
|
+
...aFirmar,
|
|
642
|
+
gas: await this.gasFijo('El cambio de precio', aFirmar),
|
|
641
643
|
chain: chainFor(this.network),
|
|
642
644
|
account: this.account,
|
|
643
645
|
});
|
|
@@ -651,11 +653,10 @@ export class PanalClient {
|
|
|
651
653
|
*/
|
|
652
654
|
async setActive(active) {
|
|
653
655
|
const wallet = this.wallet();
|
|
656
|
+
const aFirmar = { address: this.addresses.registry, abi: registryAbi, functionName: 'setActive', args: [active] };
|
|
654
657
|
const hash = await wallet.writeContract({
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
functionName: 'setActive',
|
|
658
|
-
args: [active],
|
|
658
|
+
...aFirmar,
|
|
659
|
+
gas: await this.gasFijo('El cambio de estado', aFirmar),
|
|
659
660
|
chain: chainFor(this.network),
|
|
660
661
|
account: this.account,
|
|
661
662
|
});
|
|
@@ -679,11 +680,10 @@ export class PanalClient {
|
|
|
679
680
|
throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se entrega lo que sigue abierto.`);
|
|
680
681
|
}
|
|
681
682
|
const resultHash = keccak256(toBytes(resultText));
|
|
683
|
+
const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'deliverResult', args: [taskId, resultHash] };
|
|
682
684
|
const txHash = await wallet.writeContract({
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
functionName: 'deliverResult',
|
|
686
|
-
args: [taskId, resultHash],
|
|
685
|
+
...aFirmar,
|
|
686
|
+
gas: await this.gasFijo('La entrega', aFirmar),
|
|
687
687
|
chain: chainFor(this.network),
|
|
688
688
|
account: this.account,
|
|
689
689
|
});
|
|
@@ -1016,6 +1016,7 @@ export class PanalClient {
|
|
|
1016
1016
|
? ` Se probó también con ${variantesDeSkill(skill).slice(1).map((v) => `"${v}"`).join(' y ')}.`
|
|
1017
1017
|
: ''));
|
|
1018
1018
|
}
|
|
1019
|
+
const moneda = getAddress(options.asset ?? this.addresses.panalToken);
|
|
1019
1020
|
const quotes = [];
|
|
1020
1021
|
const rechazos = [];
|
|
1021
1022
|
for (const agent of candidates) {
|
|
@@ -1026,7 +1027,10 @@ export class PanalClient {
|
|
|
1026
1027
|
...options,
|
|
1027
1028
|
envelope: heredado,
|
|
1028
1029
|
});
|
|
1029
|
-
if (
|
|
1030
|
+
if (getAddress(accept.asset) !== moneda) {
|
|
1031
|
+
rechazos.push(`${agent.metadata.name || agent.address}: cobra en ${accept.asset}, no en ${moneda}`);
|
|
1032
|
+
}
|
|
1033
|
+
else if (BigInt(accept.amount) <= tope)
|
|
1030
1034
|
quotes.push({ agent, endpoint, accept });
|
|
1031
1035
|
else
|
|
1032
1036
|
rechazos.push(`${agent.metadata.name || agent.address}: pide ${accept.amount}, por encima del tope`);
|
|
@@ -1048,6 +1052,7 @@ export class PanalClient {
|
|
|
1048
1052
|
maxSpend: tope,
|
|
1049
1053
|
chainId: chainFor(this.network).id,
|
|
1050
1054
|
expectedPayee: elegido.agent.address,
|
|
1055
|
+
asset: moneda,
|
|
1051
1056
|
quote: elegido.accept,
|
|
1052
1057
|
envelope: siguiente,
|
|
1053
1058
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Las monedas con las que se paga una consulta x402 en Panal.
|
|
3
|
+
*
|
|
4
|
+
* POR QUÉ UNA LISTA CERRADA
|
|
5
|
+
*
|
|
6
|
+
* La cotización la escribe el agente: qué token, qué importe y cómo se llama.
|
|
7
|
+
* Fiarse de eso para enseñar el precio era el agujero. Un agente podía cobrar
|
|
8
|
+
* en un token de 6 decimales y llamarlo «$PANAL»: 1.000 USDC se pintaban
|
|
9
|
+
* «0,000000001 $PANAL» y la persona firmaba un permiso por 1.000 USDC creyendo
|
|
10
|
+
* que no pagaba nada.
|
|
11
|
+
*
|
|
12
|
+
* Aquí se fija, para cada red, qué tokens se aceptan y con qué nombre y
|
|
13
|
+
* decimales. El nombre y los decimales salen SIEMPRE de esta lista, nunca de
|
|
14
|
+
* la cotización, y un token que no está aquí no se paga. Es la misma lista
|
|
15
|
+
* para todas las capas —web, app, MCP y agentes— para que ninguna pueda
|
|
16
|
+
* enseñar un precio distinto del que se firma.
|
|
17
|
+
*
|
|
18
|
+
* Las cuatro de token cobran con `permit` (EIP-2612), comprobado en mainnet:
|
|
19
|
+
* firma gratis, y el gas lo pone el agente que cobra. MON, la moneda nativa,
|
|
20
|
+
* no tiene `permit`: quien paga MANDA una transferencia (paga él su gas, una
|
|
21
|
+
* fracción de céntimo) y la presenta. Ver `native-transfer` en x402-server.ts.
|
|
22
|
+
*
|
|
23
|
+
* - MON — la moneda de Monad. Nadie puede congelarla.
|
|
24
|
+
* - $PANAL — el token de Panal.
|
|
25
|
+
* - GHO — de Aave. Respaldo cripto, sin lista negra ni pausa: nadie puede
|
|
26
|
+
* congelarlo. La opción estable que no depende de una empresa.
|
|
27
|
+
* - USDC — de Circle. Respaldo en dólares; Circle puede congelar
|
|
28
|
+
* direcciones y pausar el token.
|
|
29
|
+
* - AUSD — de Agora. Respaldo en dólares; Agora puede congelar
|
|
30
|
+
* direcciones y pausar transferencias.
|
|
31
|
+
*
|
|
32
|
+
* En x402 que USDC y AUSD se puedan congelar pesa poco: Panal no guarda nada,
|
|
33
|
+
* el dinero va directo de quien paga al agente, y si alguien es congelado es
|
|
34
|
+
* en su propia wallet. Cada uno elige con qué paga o cobra.
|
|
35
|
+
*/
|
|
36
|
+
import { type Address } from 'viem';
|
|
37
|
+
import type { PanalNetwork } from './chains.js';
|
|
38
|
+
/**
|
|
39
|
+
* Cómo se cobra en una moneda: con una firma de `permit` que ejecuta quien
|
|
40
|
+
* cobra, o con una transferencia que manda quien paga.
|
|
41
|
+
*/
|
|
42
|
+
export type X402Scheme = 'eip2612-permit' | 'native-transfer';
|
|
43
|
+
/** La moneda nativa, como la escribe Panal en todas partes: la dirección cero. */
|
|
44
|
+
export declare const X402_NATIVE: Address;
|
|
45
|
+
export interface X402Currency {
|
|
46
|
+
/** Como se enseña: `$PANAL`, `GHO`, `USDC`, `AUSD`. */
|
|
47
|
+
symbol: string;
|
|
48
|
+
address: Address;
|
|
49
|
+
decimals: number;
|
|
50
|
+
scheme: X402Scheme;
|
|
51
|
+
/** Si su emisor puede congelar direcciones o pausar el token. */
|
|
52
|
+
issuerCanFreeze: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Las monedas aceptadas en una red, en el orden en que se ofrecen. */
|
|
55
|
+
export declare function x402Currencies(network?: PanalNetwork): readonly X402Currency[];
|
|
56
|
+
/** La red de un chainId de Monad, o `null` si no es ninguna de las dos. */
|
|
57
|
+
export declare function networkOfChain(chainId: number): PanalNetwork | null;
|
|
58
|
+
/**
|
|
59
|
+
* La moneda de un token, o `null` si Panal no acepta pagar en él.
|
|
60
|
+
*
|
|
61
|
+
* Por dirección: el nombre lo puede escribir cualquiera, la dirección no.
|
|
62
|
+
*/
|
|
63
|
+
export declare function x402Currency(token: string, network?: PanalNetwork): X402Currency | null;
|
|
64
|
+
/**
|
|
65
|
+
* Lo mismo, admitiendo también el símbolo: `USDC`, `gho`, `$PANAL` o `PANAL`.
|
|
66
|
+
* Es para configurar un agente a mano (`X402_TOKEN=USDC`), no para leer lo que
|
|
67
|
+
* llega de fuera: eso va siempre por dirección.
|
|
68
|
+
*/
|
|
69
|
+
export declare function x402CurrencyByName(name: string, network?: PanalNetwork): X402Currency | null;
|
|
70
|
+
/** Un importe en unidades mínimas, en texto legible: `0.05`. */
|
|
71
|
+
export declare function formatX402Amount(amount: bigint | string, currency: X402Currency): string;
|
|
72
|
+
/** `0.05` → unidades mínimas de esa moneda. Lanza si el texto no es un número. */
|
|
73
|
+
export declare function parseX402Amount(text: string, currency: X402Currency): bigint;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Las monedas con las que se paga una consulta x402 en Panal.
|
|
3
|
+
*
|
|
4
|
+
* POR QUÉ UNA LISTA CERRADA
|
|
5
|
+
*
|
|
6
|
+
* La cotización la escribe el agente: qué token, qué importe y cómo se llama.
|
|
7
|
+
* Fiarse de eso para enseñar el precio era el agujero. Un agente podía cobrar
|
|
8
|
+
* en un token de 6 decimales y llamarlo «$PANAL»: 1.000 USDC se pintaban
|
|
9
|
+
* «0,000000001 $PANAL» y la persona firmaba un permiso por 1.000 USDC creyendo
|
|
10
|
+
* que no pagaba nada.
|
|
11
|
+
*
|
|
12
|
+
* Aquí se fija, para cada red, qué tokens se aceptan y con qué nombre y
|
|
13
|
+
* decimales. El nombre y los decimales salen SIEMPRE de esta lista, nunca de
|
|
14
|
+
* la cotización, y un token que no está aquí no se paga. Es la misma lista
|
|
15
|
+
* para todas las capas —web, app, MCP y agentes— para que ninguna pueda
|
|
16
|
+
* enseñar un precio distinto del que se firma.
|
|
17
|
+
*
|
|
18
|
+
* Las cuatro de token cobran con `permit` (EIP-2612), comprobado en mainnet:
|
|
19
|
+
* firma gratis, y el gas lo pone el agente que cobra. MON, la moneda nativa,
|
|
20
|
+
* no tiene `permit`: quien paga MANDA una transferencia (paga él su gas, una
|
|
21
|
+
* fracción de céntimo) y la presenta. Ver `native-transfer` en x402-server.ts.
|
|
22
|
+
*
|
|
23
|
+
* - MON — la moneda de Monad. Nadie puede congelarla.
|
|
24
|
+
* - $PANAL — el token de Panal.
|
|
25
|
+
* - GHO — de Aave. Respaldo cripto, sin lista negra ni pausa: nadie puede
|
|
26
|
+
* congelarlo. La opción estable que no depende de una empresa.
|
|
27
|
+
* - USDC — de Circle. Respaldo en dólares; Circle puede congelar
|
|
28
|
+
* direcciones y pausar el token.
|
|
29
|
+
* - AUSD — de Agora. Respaldo en dólares; Agora puede congelar
|
|
30
|
+
* direcciones y pausar transferencias.
|
|
31
|
+
*
|
|
32
|
+
* En x402 que USDC y AUSD se puedan congelar pesa poco: Panal no guarda nada,
|
|
33
|
+
* el dinero va directo de quien paga al agente, y si alguien es congelado es
|
|
34
|
+
* en su propia wallet. Cada uno elige con qué paga o cobra.
|
|
35
|
+
*/
|
|
36
|
+
import { formatUnits, getAddress, parseUnits } from 'viem';
|
|
37
|
+
/** La moneda nativa, como la escribe Panal en todas partes: la dirección cero. */
|
|
38
|
+
export const X402_NATIVE = '0x0000000000000000000000000000000000000000';
|
|
39
|
+
const MON = {
|
|
40
|
+
symbol: 'MON',
|
|
41
|
+
address: X402_NATIVE,
|
|
42
|
+
decimals: 18,
|
|
43
|
+
scheme: 'native-transfer',
|
|
44
|
+
issuerCanFreeze: false,
|
|
45
|
+
};
|
|
46
|
+
const MAINNET = [
|
|
47
|
+
{
|
|
48
|
+
symbol: '$PANAL',
|
|
49
|
+
address: getAddress('0x2e2e44e7fa6178822d4397299f719e89d1a67777'),
|
|
50
|
+
decimals: 18,
|
|
51
|
+
scheme: 'eip2612-permit',
|
|
52
|
+
issuerCanFreeze: false,
|
|
53
|
+
},
|
|
54
|
+
MON,
|
|
55
|
+
{
|
|
56
|
+
symbol: 'GHO',
|
|
57
|
+
address: getAddress('0xfc421ad3c883bf9e7c4f42de845c4e4405799e73'),
|
|
58
|
+
decimals: 18,
|
|
59
|
+
scheme: 'eip2612-permit',
|
|
60
|
+
issuerCanFreeze: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
symbol: 'USDC',
|
|
64
|
+
address: getAddress('0x754704bc059f8c67012fed69bc8a327a5aafb603'),
|
|
65
|
+
decimals: 6,
|
|
66
|
+
scheme: 'eip2612-permit',
|
|
67
|
+
issuerCanFreeze: true,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
symbol: 'AUSD',
|
|
71
|
+
address: getAddress('0x00000000efe302beaa2b3e6e1b18d08d69a9012a'),
|
|
72
|
+
decimals: 6,
|
|
73
|
+
scheme: 'eip2612-permit',
|
|
74
|
+
issuerCanFreeze: true,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
/** En testnet no hay ninguno de estos tokens, pero MON sí. */
|
|
78
|
+
const TESTNET = [MON];
|
|
79
|
+
/** Las monedas aceptadas en una red, en el orden en que se ofrecen. */
|
|
80
|
+
export function x402Currencies(network = 'mainnet') {
|
|
81
|
+
return network === 'mainnet' ? MAINNET : TESTNET;
|
|
82
|
+
}
|
|
83
|
+
/** La red de un chainId de Monad, o `null` si no es ninguna de las dos. */
|
|
84
|
+
export function networkOfChain(chainId) {
|
|
85
|
+
return chainId === 143 ? 'mainnet' : chainId === 10143 ? 'testnet' : null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* La moneda de un token, o `null` si Panal no acepta pagar en él.
|
|
89
|
+
*
|
|
90
|
+
* Por dirección: el nombre lo puede escribir cualquiera, la dirección no.
|
|
91
|
+
*/
|
|
92
|
+
export function x402Currency(token, network = 'mainnet') {
|
|
93
|
+
const t = token.trim().toLowerCase();
|
|
94
|
+
return x402Currencies(network).find((c) => c.address.toLowerCase() === t) ?? null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Lo mismo, admitiendo también el símbolo: `USDC`, `gho`, `$PANAL` o `PANAL`.
|
|
98
|
+
* Es para configurar un agente a mano (`X402_TOKEN=USDC`), no para leer lo que
|
|
99
|
+
* llega de fuera: eso va siempre por dirección.
|
|
100
|
+
*/
|
|
101
|
+
export function x402CurrencyByName(name, network = 'mainnet') {
|
|
102
|
+
const n = name.trim().replace(/^\$/, '').toUpperCase();
|
|
103
|
+
return x402Currencies(network).find((c) => c.symbol.replace(/^\$/, '').toUpperCase() === n) ?? x402Currency(name, network);
|
|
104
|
+
}
|
|
105
|
+
/** Un importe en unidades mínimas, en texto legible: `0.05`. */
|
|
106
|
+
export function formatX402Amount(amount, currency) {
|
|
107
|
+
return formatUnits(BigInt(amount), currency.decimals);
|
|
108
|
+
}
|
|
109
|
+
/** `0.05` → unidades mínimas de esa moneda. Lanza si el texto no es un número. */
|
|
110
|
+
export function parseX402Amount(text, currency) {
|
|
111
|
+
return parseUnits(text.trim(), currency.decimals);
|
|
112
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,14 +27,16 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
|
|
|
27
27
|
export type { Agent, AgentLinkKey, AgentLinks, AgentMetadata, Task } from './types.js';
|
|
28
28
|
export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
|
|
29
29
|
export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
30
|
+
export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
|
|
31
|
+
export type { X402Currency, X402Scheme } from './currencies.js';
|
|
30
32
|
export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
|
|
31
33
|
export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
|
|
32
34
|
export { ATTACH_BLOCK, FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appendAttachmentsManifest, appendFilesManifest, attachmentFrom, buildAttachmentsManifest, buildFilesManifest, downloadDeliveredFile, fileUrl, matchAttachment, parseAttachmentsManifest, parseFilesManifest, sanitizeFileName, stripFilesManifest, verifyFileBytes, } from './files.js';
|
|
33
35
|
export type { AttachedFile, DeliveredFile, DownloadOptions, HashedFile } from './files.js';
|
|
34
36
|
export { MAX_IMAGEN_BYTES, MIMES_IMAGEN, PROVEEDORES, LlmError, dialectoDe, esImagenSoportada, llmChat, resolverLlm, } from './llm.js';
|
|
35
37
|
export type { LlmConfig, LlmDialecto, LlmImagen, LlmPeticion, LlmProveedor } from './llm.js';
|
|
36
|
-
export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
|
|
37
|
-
export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402ServerQuote, } from './x402-server.js';
|
|
38
|
+
export { X402_VERSION, X402_SERVER_SCHEME, X402_NATIVE_SCHEME, buildNativeQuote, buildQuote, enqueueByPayer, newQuoteSecret, parsePaymentHeader, parseX402Header, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, verifyNativePayment, } from './x402-server.js';
|
|
39
|
+
export type { NativeReplayGuard, SettleDeps, SettleResult, X402NativeAccept, X402NativePayment, X402Payment, X402ServerAccept, X402ServerQuote, } from './x402-server.js';
|
|
38
40
|
export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
|
|
39
41
|
export type { CallEnvelope } from './envelope.js';
|
|
40
42
|
export type { UrlGuardOptions } from './net.js';
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
|
|
|
25
25
|
export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
|
|
26
26
|
// x402: pagar a otro agente por una consulta, sin escrow y sin humano.
|
|
27
27
|
export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
|
|
28
|
+
export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
|
|
28
29
|
export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
|
|
29
30
|
// Archivos: entregar un PDF o un vídeo anclando SU hash, no el del enlace.
|
|
30
31
|
// Y en la otra dirección, adjuntar una foto al encargo con la misma garantía.
|
|
@@ -34,7 +35,7 @@ export { ATTACH_BLOCK, FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appen
|
|
|
34
35
|
export { MAX_IMAGEN_BYTES, MIMES_IMAGEN, PROVEEDORES, LlmError, dialectoDe, esImagenSoportada, llmChat, resolverLlm, } from './llm.js';
|
|
35
36
|
// x402: la otra mitad, cobrar por llamada. Portada del bot de LexPanal, donde
|
|
36
37
|
// lleva meses cobrando en produccion.
|
|
37
|
-
export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
|
|
38
|
+
export { X402_VERSION, X402_SERVER_SCHEME, X402_NATIVE_SCHEME, buildNativeQuote, buildQuote, enqueueByPayer, newQuoteSecret, parsePaymentHeader, parseX402Header, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, verifyNativePayment, } from './x402-server.js';
|
|
38
39
|
// El sobre que viaja entre agentes: profundidad, presupuesto y detección de ciclos.
|
|
39
40
|
export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
|
|
40
41
|
// La ficha de GET /agent.json: un solo formato, y lectores que perdonan el
|
package/dist/x402-server.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ import type { PermitDomain } from './x402.js';
|
|
|
40
40
|
export type { PermitDomain };
|
|
41
41
|
export declare const X402_VERSION = 1;
|
|
42
42
|
export declare const X402_SERVER_SCHEME = "eip2612-permit";
|
|
43
|
+
/** El esquema de MON: quien paga manda una transferencia y la presenta. */
|
|
44
|
+
export declare const X402_NATIVE_SCHEME = "native-transfer";
|
|
43
45
|
export declare function permitTypedData(domain: PermitDomain, message: {
|
|
44
46
|
owner: Address;
|
|
45
47
|
spender: Address;
|
|
@@ -76,12 +78,18 @@ export declare function permitTypedData(domain: PermitDomain, message: {
|
|
|
76
78
|
};
|
|
77
79
|
};
|
|
78
80
|
/**
|
|
79
|
-
* Lee el dominio EIP-712 del token en cadena
|
|
80
|
-
* `name()` + version "1", que es lo que usan las implementaciones habituales.
|
|
81
|
+
* Lee el dominio EIP-712 del token en cadena.
|
|
81
82
|
*
|
|
82
83
|
* Se lee de la cadena en vez de escribirlo a mano porque un dominio mal
|
|
83
84
|
* construido produce firmas que verifican en local y revierten al llegar al
|
|
84
85
|
* contrato: el fallo aparecería solo en producción y con dinero de por medio.
|
|
86
|
+
*
|
|
87
|
+
* Y no basta con una sola fuente. ERC-5267 (`eip712Domain()`) lo tienen
|
|
88
|
+
* $PANAL y AUSD, pero no GHO ni USDC; y la versión no es siempre "1": USDC usa
|
|
89
|
+
* "2". Antes se suponía "1" si faltaba `eip712Domain()`, y TODAS las firmas de
|
|
90
|
+
* USDC salían inválidas. Ahora se reúnen los candidatos —ERC-5267, `name()`
|
|
91
|
+
* con `version()`, con "1" y con "2"— y se elige el que da exactamente el
|
|
92
|
+
* `DOMAIN_SEPARATOR()` del token. Si el token no lo expone, se usa el primero.
|
|
85
93
|
*/
|
|
86
94
|
export declare function readPermitDomain(publicClient: PublicClient, token: Address, network?: PanalNetwork): Promise<PermitDomain>;
|
|
87
95
|
/** Nonce de permit actual del pagador. Cambia con cada pago consumido. */
|
|
@@ -122,7 +130,7 @@ export declare function buildQuote(params: {
|
|
|
122
130
|
nowS?: number;
|
|
123
131
|
}): X402ServerQuote;
|
|
124
132
|
export interface X402Payment {
|
|
125
|
-
scheme:
|
|
133
|
+
scheme: typeof X402_SERVER_SCHEME;
|
|
126
134
|
payer: Address;
|
|
127
135
|
value: bigint;
|
|
128
136
|
deadline: bigint;
|
|
@@ -169,3 +177,86 @@ export declare function enqueueByPayer<T>(payer: Address, fn: () => Promise<T>):
|
|
|
169
177
|
export declare function verifyAndSettle(deps: SettleDeps, payment: X402Payment, price: bigint): Promise<SettleResult>;
|
|
170
178
|
/** Identificador estable del recurso pagado, para trazas y recibos. */
|
|
171
179
|
export declare function resourceId(method: string, path: string, body: string): Hex;
|
|
180
|
+
/** Un secreto nuevo para firmar cotizaciones. Uno por arranque basta. */
|
|
181
|
+
export declare function newQuoteSecret(): Hex;
|
|
182
|
+
export interface X402NativeAccept {
|
|
183
|
+
scheme: typeof X402_NATIVE_SCHEME;
|
|
184
|
+
network: string;
|
|
185
|
+
chainId: number;
|
|
186
|
+
asset: Address;
|
|
187
|
+
assetSymbol: 'MON';
|
|
188
|
+
/** El precio más unas unidades al azar: la transferencia tiene que ser de ESTO exactamente. */
|
|
189
|
+
amount: string;
|
|
190
|
+
payTo: Address;
|
|
191
|
+
resource: string;
|
|
192
|
+
description: string;
|
|
193
|
+
deadline: number;
|
|
194
|
+
maxTimeoutSeconds: number;
|
|
195
|
+
/** Secreto de esta cotización: se presenta con el pago, nunca va a la cadena. */
|
|
196
|
+
paymentId: Hex;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* La cotización de una pregunta en MON.
|
|
200
|
+
*
|
|
201
|
+
* @param resource `resourceId()` de la petición: ata el pago a ESTA pregunta.
|
|
202
|
+
* @param payer Quien dijo que iba a pagar (cabecera X-Payment-Payer). Sin
|
|
203
|
+
* él, la cotización vale para cualquier pagador, y lo que la
|
|
204
|
+
* protege es el importe único y el secreto.
|
|
205
|
+
*/
|
|
206
|
+
export declare function buildNativeQuote(params: {
|
|
207
|
+
secret: Hex;
|
|
208
|
+
price: bigint;
|
|
209
|
+
payTo: Address;
|
|
210
|
+
resource: Hex;
|
|
211
|
+
description: string;
|
|
212
|
+
payer?: Address | null;
|
|
213
|
+
network?: PanalNetwork;
|
|
214
|
+
nowS?: number;
|
|
215
|
+
}): {
|
|
216
|
+
x402Version: typeof X402_VERSION;
|
|
217
|
+
accepts: X402NativeAccept[];
|
|
218
|
+
hint: string;
|
|
219
|
+
};
|
|
220
|
+
export interface X402NativePayment {
|
|
221
|
+
scheme: typeof X402_NATIVE_SCHEME;
|
|
222
|
+
payer: Address;
|
|
223
|
+
value: bigint;
|
|
224
|
+
deadline: bigint;
|
|
225
|
+
paymentId: Hex;
|
|
226
|
+
txHash: Hex;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Lee X-Payment de los dos esquemas. Nunca lanza: devuelve el motivo.
|
|
230
|
+
*
|
|
231
|
+
* `parsePaymentHeader` sigue aceptando solo el permit, como antes, para que un
|
|
232
|
+
* agente que no sabe cobrar en MON no reciba un pago que no sabe comprobar.
|
|
233
|
+
*/
|
|
234
|
+
export declare function parseX402Header(header: string): {
|
|
235
|
+
ok: true;
|
|
236
|
+
payment: X402Payment | X402NativePayment;
|
|
237
|
+
} | {
|
|
238
|
+
ok: false;
|
|
239
|
+
error: string;
|
|
240
|
+
};
|
|
241
|
+
/** Las transacciones ya cobradas. Basta en memoria: una cotización caduca a los 5 minutos. */
|
|
242
|
+
export interface NativeReplayGuard {
|
|
243
|
+
has(txHash: string): boolean;
|
|
244
|
+
add(txHash: string): void;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Comprueba en la cadena un pago en MON. El recurso NO debe servirse hasta
|
|
248
|
+
* que esto salga bien. No hace falta wallet: el pago ya lo mandó el cliente.
|
|
249
|
+
*/
|
|
250
|
+
export declare function verifyNativePayment(deps: {
|
|
251
|
+
publicClient: PublicClient;
|
|
252
|
+
payee: Address;
|
|
253
|
+
secret: Hex;
|
|
254
|
+
used: NativeReplayGuard;
|
|
255
|
+
/** Para las pruebas. */
|
|
256
|
+
nowS?: number;
|
|
257
|
+
/** Cuánto esperar a que el nodo vea la transacción. */
|
|
258
|
+
waitMs?: number;
|
|
259
|
+
}, payment: X402NativePayment, params: {
|
|
260
|
+
price: bigint;
|
|
261
|
+
resource: Hex;
|
|
262
|
+
}): Promise<SettleResult>;
|
package/dist/x402-server.js
CHANGED
|
@@ -34,11 +34,13 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Portado del bot de LexPanal, donde llevaba meses cobrando en producción.
|
|
36
36
|
*/
|
|
37
|
-
import { getAddress, hexToNumber, isAddress, isHex, keccak256, slice, toHex, verifyTypedData, } from 'viem';
|
|
37
|
+
import { encodeAbiParameters, getAddress, hashDomain, hexToNumber, isAddress, isAddressEqual, isHex, keccak256, slice, toHex, verifyTypedData, } from 'viem';
|
|
38
38
|
import { chainFor } from './chains.js';
|
|
39
39
|
import { erc20Abi } from './abis.js';
|
|
40
40
|
export const X402_VERSION = 1;
|
|
41
41
|
export const X402_SERVER_SCHEME = 'eip2612-permit';
|
|
42
|
+
/** El esquema de MON: quien paga manda una transferencia y la presenta. */
|
|
43
|
+
export const X402_NATIVE_SCHEME = 'native-transfer';
|
|
42
44
|
/** Margen mínimo de vigencia que se exige a la firma al llegar. */
|
|
43
45
|
const MIN_DEADLINE_MARGIN_S = 30;
|
|
44
46
|
/** Vigencia que se sugiere en el presupuesto. */
|
|
@@ -59,47 +61,81 @@ export function permitTypedData(domain, message) {
|
|
|
59
61
|
return { domain, types: PERMIT_TYPES, primaryType: 'Permit', message };
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
|
-
* Lee el dominio EIP-712 del token en cadena
|
|
63
|
-
* `name()` + version "1", que es lo que usan las implementaciones habituales.
|
|
64
|
+
* Lee el dominio EIP-712 del token en cadena.
|
|
64
65
|
*
|
|
65
66
|
* Se lee de la cadena en vez de escribirlo a mano porque un dominio mal
|
|
66
67
|
* construido produce firmas que verifican en local y revierten al llegar al
|
|
67
68
|
* contrato: el fallo aparecería solo en producción y con dinero de por medio.
|
|
69
|
+
*
|
|
70
|
+
* Y no basta con una sola fuente. ERC-5267 (`eip712Domain()`) lo tienen
|
|
71
|
+
* $PANAL y AUSD, pero no GHO ni USDC; y la versión no es siempre "1": USDC usa
|
|
72
|
+
* "2". Antes se suponía "1" si faltaba `eip712Domain()`, y TODAS las firmas de
|
|
73
|
+
* USDC salían inválidas. Ahora se reúnen los candidatos —ERC-5267, `name()`
|
|
74
|
+
* con `version()`, con "1" y con "2"— y se elige el que da exactamente el
|
|
75
|
+
* `DOMAIN_SEPARATOR()` del token. Si el token no lo expone, se usa el primero.
|
|
68
76
|
*/
|
|
69
77
|
export async function readPermitDomain(publicClient, token, network = 'mainnet') {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{ name: 'verifyingContract', type: 'address' },
|
|
85
|
-
{ name: 'salt', type: 'bytes32' },
|
|
86
|
-
{ name: 'extensions', type: 'uint256[]' },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
functionName: 'eip712Domain',
|
|
91
|
-
}));
|
|
92
|
-
return { name: d[1], version: d[2], chainId: Number(d[3]), verifyingContract: getAddress(d[4]) };
|
|
78
|
+
const leer = (functionName) => publicClient
|
|
79
|
+
.readContract({ address: token, abi: dominioAbi, functionName: functionName })
|
|
80
|
+
.then((v) => v, () => null);
|
|
81
|
+
const [erc5267, nombre, version, separador] = await Promise.all([
|
|
82
|
+
leer('eip712Domain'),
|
|
83
|
+
leer('name'),
|
|
84
|
+
leer('version'),
|
|
85
|
+
leer('DOMAIN_SEPARATOR'),
|
|
86
|
+
]);
|
|
87
|
+
const chainId = chainFor(network).id;
|
|
88
|
+
const verifyingContract = getAddress(token);
|
|
89
|
+
const candidatos = [];
|
|
90
|
+
if (erc5267) {
|
|
91
|
+
candidatos.push({ name: erc5267[1], version: erc5267[2], chainId: Number(erc5267[3]), verifyingContract: getAddress(erc5267[4]) });
|
|
93
92
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
if (nombre !== null) {
|
|
94
|
+
for (const v of [version, '1', '2']) {
|
|
95
|
+
if (v !== null)
|
|
96
|
+
candidatos.push({ name: nombre, version: v, chainId, verifyingContract });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!candidatos.length)
|
|
100
|
+
throw new Error(`no se pudo leer el dominio EIP-712 de ${token}`);
|
|
101
|
+
if (!separador)
|
|
102
|
+
return candidatos[0];
|
|
103
|
+
const bueno = candidatos.find((d) => hashDomain({
|
|
104
|
+
domain: { ...d, chainId: BigInt(d.chainId) },
|
|
105
|
+
types: {
|
|
106
|
+
EIP712Domain: [
|
|
107
|
+
{ name: 'name', type: 'string' },
|
|
108
|
+
{ name: 'version', type: 'string' },
|
|
109
|
+
{ name: 'chainId', type: 'uint256' },
|
|
110
|
+
{ name: 'verifyingContract', type: 'address' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
}) === separador);
|
|
114
|
+
if (!bueno) {
|
|
115
|
+
throw new Error(`ningún dominio candidato de ${token} coincide con su DOMAIN_SEPARATOR: no se puede firmar con seguridad`);
|
|
101
116
|
}
|
|
117
|
+
return bueno;
|
|
102
118
|
}
|
|
119
|
+
const dominioAbi = [
|
|
120
|
+
{
|
|
121
|
+
type: 'function',
|
|
122
|
+
name: 'eip712Domain',
|
|
123
|
+
stateMutability: 'view',
|
|
124
|
+
inputs: [],
|
|
125
|
+
outputs: [
|
|
126
|
+
{ name: 'fields', type: 'bytes1' },
|
|
127
|
+
{ name: 'name', type: 'string' },
|
|
128
|
+
{ name: 'version', type: 'string' },
|
|
129
|
+
{ name: 'chainId', type: 'uint256' },
|
|
130
|
+
{ name: 'verifyingContract', type: 'address' },
|
|
131
|
+
{ name: 'salt', type: 'bytes32' },
|
|
132
|
+
{ name: 'extensions', type: 'uint256[]' },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{ type: 'function', name: 'name', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
|
|
136
|
+
{ type: 'function', name: 'version', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
|
|
137
|
+
{ type: 'function', name: 'DOMAIN_SEPARATOR', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'bytes32' }] },
|
|
138
|
+
];
|
|
103
139
|
/** Nonce de permit actual del pagador. Cambia con cada pago consumido. */
|
|
104
140
|
export async function permitNonce(publicClient, token, owner) {
|
|
105
141
|
return publicClient.readContract({
|
|
@@ -273,28 +309,34 @@ export async function verifyAndSettle(deps, payment, price) {
|
|
|
273
309
|
const wallet = deps.walletClient;
|
|
274
310
|
// permit + transferFrom. Se simula antes para no quemar gas en una
|
|
275
311
|
// transacción condenada (firma consumida, deadline pasado, saldo movido).
|
|
276
|
-
|
|
312
|
+
const permitCall = {
|
|
277
313
|
address: deps.token,
|
|
278
314
|
abi: permitAbi,
|
|
279
315
|
functionName: 'permit',
|
|
280
316
|
args: [payment.payer, deps.payee, payment.value, payment.deadline, v, r, s],
|
|
281
317
|
account: wallet.account,
|
|
282
|
-
}
|
|
318
|
+
};
|
|
319
|
+
await deps.publicClient.simulateContract(permitCall);
|
|
320
|
+
// Con el gas FIJADO: Monad cobra el límite entero, y si se deja a viem lo
|
|
321
|
+
// rellena con `eth_fillTransaction`, que el nodo a veces infla. Así se
|
|
322
|
+
// perdieron retiradas enteras; aquí sería el agente pagando de más en
|
|
323
|
+
// cada cobro.
|
|
283
324
|
const permitTx = await wallet.writeContract({
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
functionName: 'permit',
|
|
287
|
-
args: [payment.payer, deps.payee, payment.value, payment.deadline, v, r, s],
|
|
288
|
-
account: wallet.account,
|
|
325
|
+
...permitCall,
|
|
326
|
+
gas: await gasDeCobro(deps.publicClient, permitCall),
|
|
289
327
|
chain,
|
|
290
328
|
});
|
|
291
329
|
await deps.publicClient.waitForTransactionReceipt({ hash: permitTx });
|
|
292
|
-
const
|
|
330
|
+
const transferCall = {
|
|
293
331
|
address: deps.token,
|
|
294
332
|
abi: tokenExtraAbi,
|
|
295
333
|
functionName: 'transferFrom',
|
|
296
334
|
args: [payment.payer, deps.payee, payment.value],
|
|
297
335
|
account: wallet.account,
|
|
336
|
+
};
|
|
337
|
+
const transferTx = await wallet.writeContract({
|
|
338
|
+
...transferCall,
|
|
339
|
+
gas: await gasDeCobro(deps.publicClient, transferCall),
|
|
298
340
|
chain,
|
|
299
341
|
});
|
|
300
342
|
const receipt = await deps.publicClient.waitForTransactionReceipt({ hash: transferTx });
|
|
@@ -304,6 +346,18 @@ export async function verifyAndSettle(deps, payment, price) {
|
|
|
304
346
|
return { ok: true, txHash: transferTx, amount: payment.value };
|
|
305
347
|
});
|
|
306
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* El gas de un paso del cobro: `eth_estimateGas` + 10 %. Un permit o un
|
|
351
|
+
* transferFrom gastan unos 50.000–90.000; por encima de 300.000 algo va mal y
|
|
352
|
+
* no se manda, en vez de pagarlo entero.
|
|
353
|
+
*/
|
|
354
|
+
async function gasDeCobro(publicClient, llamada) {
|
|
355
|
+
const estimado = await publicClient.estimateContractGas(llamada);
|
|
356
|
+
const gas = (estimado * 11n + 9n) / 10n;
|
|
357
|
+
if (gas > 300000n)
|
|
358
|
+
throw new Error(`el gas estimado para cobrar (${estimado}) es anormal: no se manda`);
|
|
359
|
+
return gas;
|
|
360
|
+
}
|
|
307
361
|
/** Trozos del ERC-20 que no están en el `erc20Abi` del SDK. */
|
|
308
362
|
const tokenExtraAbi = [
|
|
309
363
|
{
|
|
@@ -346,3 +400,198 @@ const permitAbi = [
|
|
|
346
400
|
export function resourceId(method, path, body) {
|
|
347
401
|
return keccak256(toHex(`${method} ${path}\n${body}`));
|
|
348
402
|
}
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// MON: el esquema `native-transfer`.
|
|
405
|
+
//
|
|
406
|
+
// La moneda nativa no tiene `permit`, así que no se puede cobrar con una firma.
|
|
407
|
+
// Quien paga MANDA el MON al agente (paga él su gas: una fracción de céntimo
|
|
408
|
+
// en Monad) y repite la petición con el hash de esa transacción.
|
|
409
|
+
//
|
|
410
|
+
// Lo difícil es que una transferencia es pública: cualquiera la ve en la
|
|
411
|
+
// cadena en cuanto entra. Sin cuidado, otro podría presentarla como suya y
|
|
412
|
+
// llevarse la respuesta que pagó un tercero. Lo impiden dos cosas:
|
|
413
|
+
//
|
|
414
|
+
// 1. EL IMPORTE ES ÚNICO. La cotización pide el precio más unas pocas
|
|
415
|
+
// unidades al azar (menos de una millonésima de céntimo). La transferencia
|
|
416
|
+
// tiene que ser EXACTAMENTE de ese importe, así que solo vale para la
|
|
417
|
+
// cotización que lo pidió: quien pida otra recibe otro importe.
|
|
418
|
+
// 2. EL CÓDIGO DE PAGO ES SECRETO. La cotización lleva un `paymentId` que
|
|
419
|
+
// solo conoce quien la pidió (va por HTTPS, no por la cadena) y que ata
|
|
420
|
+
// pagador, agente, importe, caducidad y la pregunta exacta. Sin él, una
|
|
421
|
+
// transferencia vista en la cadena no se puede presentar.
|
|
422
|
+
//
|
|
423
|
+
// El `paymentId` es un keccak256 de un secreto del agente con esos datos: el
|
|
424
|
+
// agente no tiene que guardar las cotizaciones que da, solo rehacer la cuenta.
|
|
425
|
+
// Y cada transacción se acepta UNA vez.
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
/** Un secreto nuevo para firmar cotizaciones. Uno por arranque basta. */
|
|
428
|
+
export function newQuoteSecret() {
|
|
429
|
+
return toHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
430
|
+
}
|
|
431
|
+
/** Margen tras la caducidad para que la transferencia llegue a minarse. */
|
|
432
|
+
const NATIVE_GRACE_S = 120;
|
|
433
|
+
function nativePaymentId(secret, q) {
|
|
434
|
+
return keccak256(encodeAbiParameters([
|
|
435
|
+
{ type: 'bytes32' },
|
|
436
|
+
{ type: 'address' },
|
|
437
|
+
{ type: 'address' },
|
|
438
|
+
{ type: 'uint256' },
|
|
439
|
+
{ type: 'uint256' },
|
|
440
|
+
{ type: 'bytes32' },
|
|
441
|
+
], [secret, getAddress(q.payer), getAddress(q.payTo), q.amount, BigInt(q.deadline), q.resource]));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* La cotización de una pregunta en MON.
|
|
445
|
+
*
|
|
446
|
+
* @param resource `resourceId()` de la petición: ata el pago a ESTA pregunta.
|
|
447
|
+
* @param payer Quien dijo que iba a pagar (cabecera X-Payment-Payer). Sin
|
|
448
|
+
* él, la cotización vale para cualquier pagador, y lo que la
|
|
449
|
+
* protege es el importe único y el secreto.
|
|
450
|
+
*/
|
|
451
|
+
export function buildNativeQuote(params) {
|
|
452
|
+
const now = params.nowS ?? Math.floor(Date.now() / 1000);
|
|
453
|
+
const deadline = now + QUOTE_TTL_S;
|
|
454
|
+
// De 1 a 1.048.576 unidades (2²⁰): menos de una millonésima de céntimo, y
|
|
455
|
+
// basta para que dos cotizaciones no pidan el mismo importe.
|
|
456
|
+
//
|
|
457
|
+
// Con una máscara y no con `%`: el rango es una potencia de dos, así que
|
|
458
|
+
// cada valor sale con la misma probabilidad. Con `% 999999` unos salían un
|
|
459
|
+
// poco más que otros (lo marcó CodeQL); aquí no importaba, porque solo hace
|
|
460
|
+
// falta que no se repita ni se adivine, pero sin sesgo no hay que pensarlo.
|
|
461
|
+
const azar = BigInt(1 + (crypto.getRandomValues(new Uint32Array(1))[0] & 0xfffff));
|
|
462
|
+
const amount = params.price + azar;
|
|
463
|
+
const payer = params.payer ? getAddress(params.payer) : ZERO;
|
|
464
|
+
return {
|
|
465
|
+
x402Version: X402_VERSION,
|
|
466
|
+
accepts: [
|
|
467
|
+
{
|
|
468
|
+
scheme: X402_NATIVE_SCHEME,
|
|
469
|
+
network: 'monad',
|
|
470
|
+
chainId: chainFor(params.network ?? 'mainnet').id,
|
|
471
|
+
asset: ZERO,
|
|
472
|
+
assetSymbol: 'MON',
|
|
473
|
+
amount: amount.toString(),
|
|
474
|
+
payTo: getAddress(params.payTo),
|
|
475
|
+
resource: params.resource,
|
|
476
|
+
description: params.description,
|
|
477
|
+
deadline,
|
|
478
|
+
maxTimeoutSeconds: 120,
|
|
479
|
+
paymentId: nativePaymentId(params.secret, { payer, payTo: params.payTo, amount, deadline, resource: params.resource }),
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
hint: 'Send EXACTLY `amount` MON to `payTo` before `deadline`, then repeat the request with the header ' +
|
|
483
|
+
'X-Payment: base64({scheme:"native-transfer",payer,value,deadline,paymentId,txHash}).',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const ZERO = '0x0000000000000000000000000000000000000000';
|
|
487
|
+
/**
|
|
488
|
+
* Lee X-Payment de los dos esquemas. Nunca lanza: devuelve el motivo.
|
|
489
|
+
*
|
|
490
|
+
* `parsePaymentHeader` sigue aceptando solo el permit, como antes, para que un
|
|
491
|
+
* agente que no sabe cobrar en MON no reciba un pago que no sabe comprobar.
|
|
492
|
+
*/
|
|
493
|
+
export function parseX402Header(header) {
|
|
494
|
+
const encoded = header.trim();
|
|
495
|
+
if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded))
|
|
496
|
+
return { ok: false, error: 'the X-Payment header is not base64' };
|
|
497
|
+
let raw;
|
|
498
|
+
try {
|
|
499
|
+
raw = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return { ok: false, error: 'the contents of X-Payment are not JSON' };
|
|
503
|
+
}
|
|
504
|
+
if (raw.scheme !== X402_NATIVE_SCHEME)
|
|
505
|
+
return parsePaymentHeader(header);
|
|
506
|
+
const { payer, value, deadline, paymentId, txHash } = raw;
|
|
507
|
+
if (typeof payer !== 'string' || !isAddress(payer))
|
|
508
|
+
return { ok: false, error: 'payer is not an address' };
|
|
509
|
+
const es32 = (x) => typeof x === 'string' && isHex(x) && x.length === 66;
|
|
510
|
+
if (!es32(paymentId))
|
|
511
|
+
return { ok: false, error: 'paymentId must be the 32-byte hex from the quote' };
|
|
512
|
+
if (!es32(txHash))
|
|
513
|
+
return { ok: false, error: 'txHash must be a 32-byte transaction hash' };
|
|
514
|
+
let valueBig;
|
|
515
|
+
let deadlineBig;
|
|
516
|
+
try {
|
|
517
|
+
valueBig = BigInt(String(value));
|
|
518
|
+
deadlineBig = BigInt(String(deadline));
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return { ok: false, error: 'value and deadline must be integers' };
|
|
522
|
+
}
|
|
523
|
+
if (valueBig <= 0n)
|
|
524
|
+
return { ok: false, error: 'value must be greater than zero' };
|
|
525
|
+
return {
|
|
526
|
+
ok: true,
|
|
527
|
+
payment: {
|
|
528
|
+
scheme: X402_NATIVE_SCHEME,
|
|
529
|
+
payer: getAddress(payer),
|
|
530
|
+
value: valueBig,
|
|
531
|
+
deadline: deadlineBig,
|
|
532
|
+
paymentId: paymentId.toLowerCase(),
|
|
533
|
+
txHash: txHash.toLowerCase(),
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Comprueba en la cadena un pago en MON. El recurso NO debe servirse hasta
|
|
539
|
+
* que esto salga bien. No hace falta wallet: el pago ya lo mandó el cliente.
|
|
540
|
+
*/
|
|
541
|
+
export async function verifyNativePayment(deps, payment, params) {
|
|
542
|
+
if (payment.value < params.price) {
|
|
543
|
+
return { ok: false, status: 402, error: `the payment (${payment.value}) is less than the price (${params.price})` };
|
|
544
|
+
}
|
|
545
|
+
// El código de pago se rehace con lo que dice el pago. Si alguien cambia el
|
|
546
|
+
// importe, la caducidad o el pagador, o presenta una cotización de otra
|
|
547
|
+
// pregunta, no cuadra. Se prueba también sin pagador: la cotización se pudo
|
|
548
|
+
// pedir sin decir quién pagaría.
|
|
549
|
+
const datos = { payTo: deps.payee, amount: payment.value, deadline: Number(payment.deadline), resource: params.resource };
|
|
550
|
+
const esperado = [payment.payer, ZERO].map((payer) => nativePaymentId(deps.secret, { ...datos, payer }));
|
|
551
|
+
if (!esperado.includes(payment.paymentId.toLowerCase())) {
|
|
552
|
+
return { ok: false, status: 402, error: 'unknown or altered quote: ask for a new one and pay exactly what it says' };
|
|
553
|
+
}
|
|
554
|
+
const nowS = deps.nowS ?? Math.floor(Date.now() / 1000);
|
|
555
|
+
if (BigInt(nowS) > payment.deadline + BigInt(NATIVE_GRACE_S)) {
|
|
556
|
+
return { ok: false, status: 402, error: 'the quote has expired' };
|
|
557
|
+
}
|
|
558
|
+
if (deps.used.has(payment.txHash)) {
|
|
559
|
+
return { ok: false, status: 409, error: 'this payment was already used' };
|
|
560
|
+
}
|
|
561
|
+
// El nodo puede tardar un instante en ver una transacción recién minada.
|
|
562
|
+
const hasta = Date.now() + (deps.waitMs ?? 6_000);
|
|
563
|
+
let recibo = null;
|
|
564
|
+
for (;;) {
|
|
565
|
+
recibo = await deps.publicClient.getTransactionReceipt({ hash: payment.txHash }).catch(() => null);
|
|
566
|
+
if (recibo || Date.now() >= hasta)
|
|
567
|
+
break;
|
|
568
|
+
await new Promise((r) => setTimeout(r, 750));
|
|
569
|
+
}
|
|
570
|
+
if (!recibo)
|
|
571
|
+
return { ok: false, status: 402, error: 'the payment transaction is not on chain yet' };
|
|
572
|
+
if (recibo.status !== 'success')
|
|
573
|
+
return { ok: false, status: 402, error: 'the payment transaction reverted' };
|
|
574
|
+
const tx = await deps.publicClient.getTransaction({ hash: payment.txHash });
|
|
575
|
+
if (!isAddressEqual(tx.from, payment.payer)) {
|
|
576
|
+
return { ok: false, status: 402, error: 'the payment was sent by someone else' };
|
|
577
|
+
}
|
|
578
|
+
if (!tx.to || !isAddressEqual(tx.to, deps.payee)) {
|
|
579
|
+
return { ok: false, status: 402, error: 'the payment was not sent to this agent' };
|
|
580
|
+
}
|
|
581
|
+
if (tx.value !== payment.value) {
|
|
582
|
+
return { ok: false, status: 402, error: `the payment sent ${tx.value}, and the quote asked for exactly ${payment.value}` };
|
|
583
|
+
}
|
|
584
|
+
const bloque = await deps.publicClient.getBlock({ blockNumber: recibo.blockNumber });
|
|
585
|
+
if (bloque.timestamp > payment.deadline) {
|
|
586
|
+
return { ok: false, status: 402, error: 'the payment arrived after the quote expired' };
|
|
587
|
+
}
|
|
588
|
+
if (bloque.timestamp + BigInt(QUOTE_TTL_S + 60) < payment.deadline) {
|
|
589
|
+
return { ok: false, status: 402, error: 'the payment is older than the quote' };
|
|
590
|
+
}
|
|
591
|
+
// Se marca ANTES de servir: dos peticiones con la misma transacción a la
|
|
592
|
+
// vez no pueden pasar las dos.
|
|
593
|
+
if (deps.used.has(payment.txHash))
|
|
594
|
+
return { ok: false, status: 409, error: 'this payment was already used' };
|
|
595
|
+
deps.used.add(payment.txHash);
|
|
596
|
+
return { ok: true, txHash: payment.txHash, amount: tx.value };
|
|
597
|
+
}
|
package/dist/x402.d.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
import type { Account, Address, Hex, WalletClient } from 'viem';
|
|
19
19
|
import { type UrlGuardOptions } from './net.js';
|
|
20
20
|
import { type CallEnvelope } from './envelope.js';
|
|
21
|
-
|
|
21
|
+
import { type X402Currency } from './currencies.js';
|
|
22
|
+
/** El esquema de las monedas con `permit`. Debe coincidir con el servidor. */
|
|
22
23
|
export declare const X402_SCHEME = "eip2612-permit";
|
|
23
24
|
export interface PermitDomain {
|
|
24
25
|
name: string;
|
|
@@ -40,7 +41,10 @@ export interface X402Accept {
|
|
|
40
41
|
deadline: number;
|
|
41
42
|
maxTimeoutSeconds?: number;
|
|
42
43
|
payerNonce?: string;
|
|
43
|
-
|
|
44
|
+
/** Solo en las de `permit`: el dominio con el que firmar. */
|
|
45
|
+
domain?: PermitDomain;
|
|
46
|
+
/** Solo en MON: el código de esta cotización, que se presenta con el pago. */
|
|
47
|
+
paymentId?: Hex;
|
|
44
48
|
}
|
|
45
49
|
export interface X402Quote {
|
|
46
50
|
x402Version?: number;
|
|
@@ -88,6 +92,8 @@ export interface AskResult {
|
|
|
88
92
|
answer: string;
|
|
89
93
|
/** Lo que se ha pagado de verdad, en unidades mínimas. */
|
|
90
94
|
paid: bigint;
|
|
95
|
+
/** En qué, con su nombre y decimales de la lista de Panal (no de la cotización). */
|
|
96
|
+
currency: X402Currency;
|
|
91
97
|
/** Quién ha cobrado. */
|
|
92
98
|
payee: Address;
|
|
93
99
|
/** Transacción del cobro, si el servidor la reporta. */
|
package/dist/x402.js
CHANGED
|
@@ -16,10 +16,16 @@
|
|
|
16
16
|
* El cliente NO paga gas: solo firma. La transacción la manda quien cobra.
|
|
17
17
|
*/
|
|
18
18
|
import { isAddress, getAddress } from 'viem';
|
|
19
|
+
import { estimateGas, waitForTransactionReceipt } from 'viem/actions';
|
|
19
20
|
import { assertPublicUrl, fetchLimited } from './net.js';
|
|
20
21
|
import { envelopeHeaders } from './envelope.js';
|
|
21
|
-
|
|
22
|
+
import { networkOfChain, x402Currency } from './currencies.js';
|
|
23
|
+
/** El esquema de las monedas con `permit`. Debe coincidir con el servidor. */
|
|
22
24
|
export const X402_SCHEME = 'eip2612-permit';
|
|
25
|
+
/** El de MON, que no tiene `permit`: se manda una transferencia y se presenta. */
|
|
26
|
+
const ESQUEMA_MON = 'native-transfer';
|
|
27
|
+
/** Lo más que puede valer una cotización: una hora. Ver `payAndAsk`. */
|
|
28
|
+
const PLAZO_MAX_S = 60 * 60;
|
|
23
29
|
const PERMIT_TYPES = {
|
|
24
30
|
Permit: [
|
|
25
31
|
{ name: 'owner', type: 'address' },
|
|
@@ -70,10 +76,10 @@ export async function quoteAsk(endpoint, prompt, options = {}) {
|
|
|
70
76
|
catch {
|
|
71
77
|
throw new X402Error('La cotización no es JSON válido.');
|
|
72
78
|
}
|
|
73
|
-
const accept = quote.accepts?.find((a) => a.scheme === X402_SCHEME);
|
|
79
|
+
const accept = quote.accepts?.find((a) => a.scheme === X402_SCHEME || a.scheme === ESQUEMA_MON);
|
|
74
80
|
if (!accept) {
|
|
75
81
|
const vistos = quote.accepts?.map((a) => a.scheme).join(', ') || 'ninguno';
|
|
76
|
-
throw new X402Error(`Ese agente no
|
|
82
|
+
throw new X402Error(`Ese agente no cobra con un esquema que sepamos pagar. Esquemas que ofrece: ${vistos}.`);
|
|
77
83
|
}
|
|
78
84
|
return accept;
|
|
79
85
|
}
|
|
@@ -109,62 +115,56 @@ export async function payAndAsk(wallet, account, endpoint, prompt, options) {
|
|
|
109
115
|
if (options.asset && getAddress(accept.asset) !== getAddress(options.asset)) {
|
|
110
116
|
throw new X402Error(`La cotización pide pagar en ${accept.asset} y esperabas ${options.asset}.`);
|
|
111
117
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
// Solo en las monedas de la lista de Panal. El nombre y los decimales que
|
|
119
|
+
// trae la cotización los escribe el agente, y fiarse de ellos permitía
|
|
120
|
+
// cobrar 1.000 USDC enseñando «0,000000001 $PANAL». Ver currencies.ts.
|
|
121
|
+
const moneda = x402Currency(accept.asset, networkOfChain(accept.chainId) ?? 'mainnet');
|
|
122
|
+
if (!moneda) {
|
|
123
|
+
throw new X402Error(`La cotización pide pagar en ${accept.asset}, que no es una moneda que Panal acepte: no se firma.`);
|
|
124
|
+
}
|
|
125
|
+
if (accept.scheme !== moneda.scheme) {
|
|
126
|
+
throw new X402Error(`${moneda.symbol} se paga con "${moneda.scheme}" y la cotización pide "${accept.scheme}".`);
|
|
116
127
|
}
|
|
117
128
|
const ahora = Math.floor(Date.now() / 1000);
|
|
118
129
|
if (accept.deadline <= ahora + 30) {
|
|
119
130
|
throw new X402Error('La cotización caduca de inmediato: pide otra.');
|
|
120
131
|
}
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
// Y tampoco una que valga demasiado. El plazo lo pone el agente, y un permiso
|
|
133
|
+
// firmado con un plazo de años se puede cobrar cuando él quiera mientras no
|
|
134
|
+
// se use otro antes. Las cotizaciones de Panal duran 5 minutos.
|
|
135
|
+
if (accept.deadline > ahora + PLAZO_MAX_S) {
|
|
136
|
+
throw new X402Error(`La cotización vale hasta dentro de ${Math.round((accept.deadline - ahora) / 60)} minutos, y el máximo es una hora: no se firma.`);
|
|
123
137
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
account,
|
|
127
|
-
domain: accept.domain,
|
|
128
|
-
types: PERMIT_TYPES,
|
|
129
|
-
primaryType: 'Permit',
|
|
130
|
-
message: {
|
|
131
|
-
owner: account.address,
|
|
132
|
-
spender: getAddress(accept.payTo),
|
|
133
|
-
value: amount,
|
|
134
|
-
nonce: BigInt(accept.payerNonce),
|
|
135
|
-
deadline: BigInt(accept.deadline),
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
const header = toBase64(JSON.stringify({
|
|
139
|
-
scheme: X402_SCHEME,
|
|
140
|
-
payer: account.address,
|
|
141
|
-
value: amount.toString(),
|
|
142
|
-
deadline: accept.deadline.toString(),
|
|
143
|
-
signature,
|
|
144
|
-
}));
|
|
138
|
+
const header = moneda.scheme === ESQUEMA_MON
|
|
139
|
+
? await pagarEnMon(wallet, account, accept, amount)
|
|
140
|
+
: await firmarPermit(wallet, account, accept, amount);
|
|
145
141
|
// ---- Segunda llamada: se cobra y se responde en la misma ----------------
|
|
146
142
|
const res = await fetchLimited(url, {
|
|
147
143
|
method: 'POST',
|
|
148
144
|
headers: {
|
|
149
145
|
'content-type': 'application/json',
|
|
150
|
-
'x-payment': header,
|
|
146
|
+
'x-payment': header.value,
|
|
151
147
|
...(options.envelope ? envelopeHeaders(options.envelope) : {}),
|
|
152
148
|
},
|
|
153
149
|
body: JSON.stringify({ prompt }),
|
|
154
150
|
timeoutMs: options.timeoutMs ?? (accept.maxTimeoutSeconds ?? 120) * 1000,
|
|
155
151
|
});
|
|
152
|
+
// `paid` es como lo devuelve la plantilla de agentes; `payment`, como lo
|
|
153
|
+
// devolvía el bot de LexPanal. Se leen los dos para no perder el hash.
|
|
156
154
|
let body;
|
|
157
155
|
try {
|
|
158
156
|
body = JSON.parse(res.text);
|
|
159
157
|
}
|
|
160
158
|
catch {
|
|
161
|
-
throw new X402Error(`Respuesta ilegible del agente (HTTP ${res.status})
|
|
159
|
+
throw new X402Error(`Respuesta ilegible del agente (HTTP ${res.status}).${header.txHash ? ` Tu pago: tx ${header.txHash}.` : ''}`, res.status);
|
|
162
160
|
}
|
|
163
161
|
if (res.status !== 200) {
|
|
164
162
|
// El 502 con `paymentTx` es el caso feo y hay que distinguirlo: te han
|
|
165
163
|
// cobrado y no han respondido, así que el hash es tu prueba para reclamar.
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// En MON el pago ya salió de tu wallet al mandarlo: el hash es el tuyo.
|
|
165
|
+
const pagado = body.paymentTx ?? header.txHash;
|
|
166
|
+
if (pagado) {
|
|
167
|
+
throw new X402Error(`Pagaste (tx ${pagado}) y el agente no entregó respuesta: ${body.error ?? `HTTP ${res.status}`}`, res.status);
|
|
168
168
|
}
|
|
169
169
|
if (res.status === 508) {
|
|
170
170
|
throw new X402Error(`El agente rechazó la llamada por ciclo: ya había atendido esta cadena. ${body.error ?? ''}`.trim(), 508);
|
|
@@ -177,11 +177,85 @@ export async function payAndAsk(wallet, account, endpoint, prompt, options) {
|
|
|
177
177
|
return {
|
|
178
178
|
answer: body.answer,
|
|
179
179
|
paid: amount,
|
|
180
|
+
currency: moneda,
|
|
180
181
|
payee: getAddress(accept.payTo),
|
|
181
|
-
txHash: body.payment?.txHash,
|
|
182
|
+
txHash: body.payment?.txHash ?? body.paid?.txHash ?? header.txHash,
|
|
182
183
|
endpoint: url.toString(),
|
|
183
184
|
};
|
|
184
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Las de `permit`: una firma, sin gas ni transacción. Quien cobra la ejecuta.
|
|
188
|
+
*/
|
|
189
|
+
async function firmarPermit(wallet, account, accept, amount) {
|
|
190
|
+
if (!accept.domain || getAddress(accept.domain.verifyingContract) !== getAddress(accept.asset)) {
|
|
191
|
+
// El dominio EIP-712 tiene que ser el del propio token: si apunta a otro
|
|
192
|
+
// contrato, la firma valdría para algo distinto de lo que crees.
|
|
193
|
+
throw new X402Error('El dominio de firma no corresponde al token que se va a pagar.');
|
|
194
|
+
}
|
|
195
|
+
if (accept.payerNonce === undefined) {
|
|
196
|
+
throw new X402Error('La cotización no trae el nonce del pagador. Vuelve a pedirla indicando `payer`, o léelo del token.');
|
|
197
|
+
}
|
|
198
|
+
// ---- Firma (sin gas, sin transacción) -----------------------------------
|
|
199
|
+
const signature = await wallet.signTypedData({
|
|
200
|
+
account,
|
|
201
|
+
domain: accept.domain,
|
|
202
|
+
types: PERMIT_TYPES,
|
|
203
|
+
primaryType: 'Permit',
|
|
204
|
+
message: {
|
|
205
|
+
owner: account.address,
|
|
206
|
+
spender: getAddress(accept.payTo),
|
|
207
|
+
value: amount,
|
|
208
|
+
nonce: BigInt(accept.payerNonce),
|
|
209
|
+
deadline: BigInt(accept.deadline),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
value: toBase64(JSON.stringify({
|
|
214
|
+
scheme: X402_SCHEME,
|
|
215
|
+
payer: account.address,
|
|
216
|
+
value: amount.toString(),
|
|
217
|
+
deadline: accept.deadline.toString(),
|
|
218
|
+
signature,
|
|
219
|
+
})),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* MON: se MANDA la transferencia y se presenta su hash.
|
|
224
|
+
*
|
|
225
|
+
* El importe es exactamente el de la cotización —lleva unas unidades al azar
|
|
226
|
+
* que la hacen única— y el código de pago no va a la cadena: es lo que impide
|
|
227
|
+
* que otro presente esta transferencia como suya. Ver x402-server.ts.
|
|
228
|
+
*
|
|
229
|
+
* Aquí sí pagas tú el gas, una fracción de céntimo en Monad, y se espera a que
|
|
230
|
+
* entre en un bloque: el agente lo va a comprobar en la cadena.
|
|
231
|
+
*/
|
|
232
|
+
async function pagarEnMon(wallet, account, accept, amount) {
|
|
233
|
+
if (!accept.paymentId || !/^0x[0-9a-fA-F]{64}$/.test(accept.paymentId)) {
|
|
234
|
+
throw new X402Error('La cotización en MON no trae su código de pago: pide otra.');
|
|
235
|
+
}
|
|
236
|
+
const to = getAddress(accept.payTo);
|
|
237
|
+
// Monad cobra el límite de gas entero, no el gastado: se pide lo justo. Una
|
|
238
|
+
// transferencia a una wallet normal son 21.000 exactos; solo se deja margen
|
|
239
|
+
// si la estimación dice que el destino ejecuta código.
|
|
240
|
+
const estimado = await estimateGas(wallet, { account, to, value: amount });
|
|
241
|
+
const gas = estimado > 21000n ? (estimado * 11n) / 10n : estimado;
|
|
242
|
+
const txHash = await wallet.sendTransaction({ account, chain: wallet.chain ?? null, to, value: amount, gas });
|
|
243
|
+
const recibo = await waitForTransactionReceipt(wallet, { hash: txHash, timeout: 90_000 });
|
|
244
|
+
if (recibo.status !== 'success') {
|
|
245
|
+
throw new X402Error(`La transferencia de MON se revirtió (tx ${txHash}): no se ha preguntado nada.`);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
txHash,
|
|
249
|
+
value: toBase64(JSON.stringify({
|
|
250
|
+
scheme: ESQUEMA_MON,
|
|
251
|
+
payer: account.address,
|
|
252
|
+
value: amount.toString(),
|
|
253
|
+
deadline: accept.deadline.toString(),
|
|
254
|
+
paymentId: accept.paymentId,
|
|
255
|
+
txHash,
|
|
256
|
+
})),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
185
259
|
/** base64 sin depender de Buffer, para que valga también en el navegador. */
|
|
186
260
|
function toBase64(text) {
|
|
187
261
|
const bytes = new TextEncoder().encode(text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panal/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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 && tsx test/tablon.test.ts && tsx test/reserva.test.ts",
|
|
48
|
+
"test": "tsx test/sdk.test.ts && tsx test/currencies.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
|
}
|