@panal/sdk 0.4.0 → 0.5.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/dist/client.d.ts CHANGED
@@ -15,7 +15,7 @@
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 type { Account, Address, Hex, PublicClient } from 'viem';
18
+ 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';
@@ -56,7 +56,12 @@ export declare class PanalClient {
56
56
  readonly addresses: PanalAddresses;
57
57
  readonly publicClient: PublicClient;
58
58
  readonly account?: Account;
59
- private readonly walletClient?;
59
+ /**
60
+ * Público a propósito: quien monte la mitad servidor de x402 lo necesita para
61
+ * ejecutar el `permit` y el `transferFrom` del cobro. Es undefined cuando el
62
+ * cliente se creó sin cuenta, o sea en modo solo lectura.
63
+ */
64
+ readonly walletClient?: WalletClient;
60
65
  constructor(options?: PanalClientOptions);
61
66
  /** El wallet client, o un error que dice exactamente qué falta. */
62
67
  private wallet;
package/dist/client.js CHANGED
@@ -31,6 +31,11 @@ export class PanalClient {
31
31
  addresses;
32
32
  publicClient;
33
33
  account;
34
+ /**
35
+ * Público a propósito: quien monte la mitad servidor de x402 lo necesita para
36
+ * ejecutar el `permit` y el `transferFrom` del cobro. Es undefined cuando el
37
+ * cliente se creó sin cuenta, o sea en modo solo lectura.
38
+ */
34
39
  walletClient;
35
40
  constructor(options = {}) {
36
41
  this.network = options.network ?? 'mainnet';
package/dist/index.d.ts CHANGED
@@ -29,6 +29,8 @@ export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
29
29
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
30
30
  export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
31
31
  export { assertPublicUrl, fetchLimited, isPrivateIp } from './net.js';
32
+ export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
33
+ export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402ServerQuote, } from './x402-server.js';
32
34
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
33
35
  export type { CallEnvelope } from './envelope.js';
34
36
  export type { UrlGuardOptions } from './net.js';
package/dist/index.js CHANGED
@@ -26,5 +26,8 @@ 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
28
  export { assertPublicUrl, fetchLimited, isPrivateIp } from './net.js';
29
+ // x402: la otra mitad, cobrar por llamada. Portada del bot de LexPanal, donde
30
+ // lleva meses cobrando en produccion.
31
+ export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
29
32
  // El sobre que viaja entre agentes: profundidad, presupuesto y detección de ciclos.
30
33
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
@@ -0,0 +1,171 @@
1
+ /**
2
+ * x402 — la mitad SERVIDOR: cobrar por llamada HTTP.
3
+ *
4
+ * El otro archivo, `x402.ts`, es la mitad cliente: pide presupuesto y paga.
5
+ * Este es el lado del que cobra.
6
+ *
7
+ * 1. Llega una petición sin pagar. POST /x402/ask
8
+ * 2. Se responde 402 con un presupuesto legible por máquina. (buildQuote)
9
+ * 3. El cliente FIRMA una autorización de pago, sin gas ni transacción.
10
+ * 4. Repite la petición con la firma en la cabecera X-Payment.
11
+ * 5. Se cobra on-chain y se sirve el recurso en esa misma llamada. (verifyAndSettle)
12
+ *
13
+ * Sin alta, sin API key, sin tarjeta: **el pago es la autenticación**. Un
14
+ * desconocido puede usar tu servicio y pagarlo sin que ninguno de los dos sepa
15
+ * quién es el otro.
16
+ *
17
+ * POR QUÉ EXIGE UNA CADENA
18
+ * Cobrar dos milésimas por llamada es imposible con tarjeta: la comisión fija
19
+ * (~0,30 $) multiplica por cien el importe. En Monad la comisión es una
20
+ * fracción de céntimo, así que el micropago sale. Es la única pieza de Panal
21
+ * donde la cadena es requisito y no decoración.
22
+ *
23
+ * ESQUEMA: eip2612-permit
24
+ * El cliente autoriza el cobro con una firma off-chain —gratis e instantánea— y
25
+ * el agente paga el gas de ejecutarla. Sin `permit` harían falta dos
26
+ * transacciones del cliente y el modelo entero se cae. Por eso este raíl solo
27
+ * funciona con un ERC-20 que implemente EIP-2612, no con la moneda nativa.
28
+ *
29
+ * SE COBRA ANTES DE SERVIR. El orden es deliberado: si se sirviera primero y el
30
+ * cobro fallara, el recurso se habría regalado.
31
+ *
32
+ * Los textos que salen por la red van en inglés a propósito: los lee un
33
+ * desconocido de cualquier parte, no el operador del agente.
34
+ *
35
+ * Portado del bot de LexPanal, donde llevaba meses cobrando en producción.
36
+ */
37
+ import { type Address, type Hex, type PublicClient, type WalletClient } from 'viem';
38
+ import { type PanalNetwork } from './chains.js';
39
+ import type { PermitDomain } from './x402.js';
40
+ export type { PermitDomain };
41
+ export declare const X402_VERSION = 1;
42
+ export declare const X402_SERVER_SCHEME = "eip2612-permit";
43
+ export declare function permitTypedData(domain: PermitDomain, message: {
44
+ owner: Address;
45
+ spender: Address;
46
+ value: bigint;
47
+ nonce: bigint;
48
+ deadline: bigint;
49
+ }): {
50
+ domain: PermitDomain;
51
+ types: {
52
+ readonly Permit: readonly [{
53
+ readonly name: "owner";
54
+ readonly type: "address";
55
+ }, {
56
+ readonly name: "spender";
57
+ readonly type: "address";
58
+ }, {
59
+ readonly name: "value";
60
+ readonly type: "uint256";
61
+ }, {
62
+ readonly name: "nonce";
63
+ readonly type: "uint256";
64
+ }, {
65
+ readonly name: "deadline";
66
+ readonly type: "uint256";
67
+ }];
68
+ };
69
+ primaryType: "Permit";
70
+ message: {
71
+ owner: Address;
72
+ spender: Address;
73
+ value: bigint;
74
+ nonce: bigint;
75
+ deadline: bigint;
76
+ };
77
+ };
78
+ /**
79
+ * Lee el dominio EIP-712 del token en cadena (ERC-5267), con respaldo a
80
+ * `name()` + version "1", que es lo que usan las implementaciones habituales.
81
+ *
82
+ * Se lee de la cadena en vez de escribirlo a mano porque un dominio mal
83
+ * construido produce firmas que verifican en local y revierten al llegar al
84
+ * contrato: el fallo aparecería solo en producción y con dinero de por medio.
85
+ */
86
+ export declare function readPermitDomain(publicClient: PublicClient, token: Address, network?: PanalNetwork): Promise<PermitDomain>;
87
+ /** Nonce de permit actual del pagador. Cambia con cada pago consumido. */
88
+ export declare function permitNonce(publicClient: PublicClient, token: Address, owner: Address): Promise<bigint>;
89
+ export interface X402ServerAccept {
90
+ scheme: typeof X402_SERVER_SCHEME;
91
+ network: string;
92
+ chainId: number;
93
+ asset: Address;
94
+ assetSymbol: string;
95
+ amount: string;
96
+ payTo: Address;
97
+ resource: string;
98
+ description: string;
99
+ deadline: number;
100
+ maxTimeoutSeconds: number;
101
+ /** Nonce del pagador, si dijo quién era con la cabecera X-Payment-Payer. */
102
+ payerNonce?: string;
103
+ /** Dominio EIP-712 con el que firmar, para no obligar al cliente a leerlo. */
104
+ domain: PermitDomain;
105
+ }
106
+ export interface X402ServerQuote {
107
+ x402Version: typeof X402_VERSION;
108
+ accepts: X402ServerAccept[];
109
+ /** Ayuda para quien lo lea a mano; los clientes usan `accepts`. */
110
+ hint: string;
111
+ }
112
+ export declare function buildQuote(params: {
113
+ asset: Address;
114
+ assetSymbol: string;
115
+ amount: bigint;
116
+ payTo: Address;
117
+ resource: string;
118
+ description: string;
119
+ domain: PermitDomain;
120
+ payerNonce?: bigint;
121
+ network?: PanalNetwork;
122
+ nowS?: number;
123
+ }): X402ServerQuote;
124
+ export interface X402Payment {
125
+ scheme: string;
126
+ payer: Address;
127
+ value: bigint;
128
+ deadline: bigint;
129
+ signature: Hex;
130
+ }
131
+ /** Descodifica y valida la forma de X-Payment. Nunca lanza: devuelve el motivo. */
132
+ export declare function parsePaymentHeader(header: string): {
133
+ ok: true;
134
+ payment: X402Payment;
135
+ } | {
136
+ ok: false;
137
+ error: string;
138
+ };
139
+ /** Parte la firma de 65 bytes en (v, r, s) para pasársela a `permit`. */
140
+ export declare function splitSignature(signature: Hex): {
141
+ v: number;
142
+ r: Hex;
143
+ s: Hex;
144
+ };
145
+ export interface SettleDeps {
146
+ publicClient: PublicClient;
147
+ walletClient: WalletClient | null;
148
+ token: Address;
149
+ domain: PermitDomain;
150
+ /** Quien cobra: la wallet del agente, que es también el spender del permit. */
151
+ payee: Address;
152
+ network?: PanalNetwork;
153
+ }
154
+ export type SettleResult = {
155
+ ok: true;
156
+ txHash: Hex;
157
+ amount: bigint;
158
+ } | {
159
+ ok: false;
160
+ status: number;
161
+ error: string;
162
+ };
163
+ /** Exportada para poder probar la serialización directamente. */
164
+ export declare function enqueueByPayer<T>(payer: Address, fn: () => Promise<T>): Promise<T>;
165
+ /**
166
+ * Verifica la firma y cobra on-chain. Devuelve el hash de la transacción.
167
+ * El recurso NO debe servirse hasta que esto salga bien.
168
+ */
169
+ export declare function verifyAndSettle(deps: SettleDeps, payment: X402Payment, price: bigint): Promise<SettleResult>;
170
+ /** Identificador estable del recurso pagado, para trazas y recibos. */
171
+ export declare function resourceId(method: string, path: string, body: string): Hex;
@@ -0,0 +1,348 @@
1
+ /**
2
+ * x402 — la mitad SERVIDOR: cobrar por llamada HTTP.
3
+ *
4
+ * El otro archivo, `x402.ts`, es la mitad cliente: pide presupuesto y paga.
5
+ * Este es el lado del que cobra.
6
+ *
7
+ * 1. Llega una petición sin pagar. POST /x402/ask
8
+ * 2. Se responde 402 con un presupuesto legible por máquina. (buildQuote)
9
+ * 3. El cliente FIRMA una autorización de pago, sin gas ni transacción.
10
+ * 4. Repite la petición con la firma en la cabecera X-Payment.
11
+ * 5. Se cobra on-chain y se sirve el recurso en esa misma llamada. (verifyAndSettle)
12
+ *
13
+ * Sin alta, sin API key, sin tarjeta: **el pago es la autenticación**. Un
14
+ * desconocido puede usar tu servicio y pagarlo sin que ninguno de los dos sepa
15
+ * quién es el otro.
16
+ *
17
+ * POR QUÉ EXIGE UNA CADENA
18
+ * Cobrar dos milésimas por llamada es imposible con tarjeta: la comisión fija
19
+ * (~0,30 $) multiplica por cien el importe. En Monad la comisión es una
20
+ * fracción de céntimo, así que el micropago sale. Es la única pieza de Panal
21
+ * donde la cadena es requisito y no decoración.
22
+ *
23
+ * ESQUEMA: eip2612-permit
24
+ * El cliente autoriza el cobro con una firma off-chain —gratis e instantánea— y
25
+ * el agente paga el gas de ejecutarla. Sin `permit` harían falta dos
26
+ * transacciones del cliente y el modelo entero se cae. Por eso este raíl solo
27
+ * funciona con un ERC-20 que implemente EIP-2612, no con la moneda nativa.
28
+ *
29
+ * SE COBRA ANTES DE SERVIR. El orden es deliberado: si se sirviera primero y el
30
+ * cobro fallara, el recurso se habría regalado.
31
+ *
32
+ * Los textos que salen por la red van en inglés a propósito: los lee un
33
+ * desconocido de cualquier parte, no el operador del agente.
34
+ *
35
+ * Portado del bot de LexPanal, donde llevaba meses cobrando en producción.
36
+ */
37
+ import { getAddress, hexToNumber, isAddress, isHex, keccak256, slice, toHex, verifyTypedData, } from 'viem';
38
+ import { chainFor } from './chains.js';
39
+ import { erc20Abi } from './abis.js';
40
+ export const X402_VERSION = 1;
41
+ export const X402_SERVER_SCHEME = 'eip2612-permit';
42
+ /** Margen mínimo de vigencia que se exige a la firma al llegar. */
43
+ const MIN_DEADLINE_MARGIN_S = 30;
44
+ /** Vigencia que se sugiere en el presupuesto. */
45
+ const QUOTE_TTL_S = 300;
46
+ // ---------------------------------------------------------------------------
47
+ // EIP-2612: datos tipados de `permit`.
48
+ // ---------------------------------------------------------------------------
49
+ const PERMIT_TYPES = {
50
+ Permit: [
51
+ { name: 'owner', type: 'address' },
52
+ { name: 'spender', type: 'address' },
53
+ { name: 'value', type: 'uint256' },
54
+ { name: 'nonce', type: 'uint256' },
55
+ { name: 'deadline', type: 'uint256' },
56
+ ],
57
+ };
58
+ export function permitTypedData(domain, message) {
59
+ return { domain, types: PERMIT_TYPES, primaryType: 'Permit', message };
60
+ }
61
+ /**
62
+ * Lee el dominio EIP-712 del token en cadena (ERC-5267), con respaldo a
63
+ * `name()` + version "1", que es lo que usan las implementaciones habituales.
64
+ *
65
+ * Se lee de la cadena en vez de escribirlo a mano porque un dominio mal
66
+ * construido produce firmas que verifican en local y revierten al llegar al
67
+ * contrato: el fallo aparecería solo en producción y con dinero de por medio.
68
+ */
69
+ export async function readPermitDomain(publicClient, token, network = 'mainnet') {
70
+ try {
71
+ const d = (await publicClient.readContract({
72
+ address: token,
73
+ abi: [
74
+ {
75
+ type: 'function',
76
+ name: 'eip712Domain',
77
+ stateMutability: 'view',
78
+ inputs: [],
79
+ outputs: [
80
+ { name: 'fields', type: 'bytes1' },
81
+ { name: 'name', type: 'string' },
82
+ { name: 'version', type: 'string' },
83
+ { name: 'chainId', type: 'uint256' },
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]) };
93
+ }
94
+ catch {
95
+ const name = (await publicClient.readContract({
96
+ address: token,
97
+ abi: tokenExtraAbi,
98
+ functionName: 'name',
99
+ }));
100
+ return { name, version: '1', chainId: chainFor(network).id, verifyingContract: getAddress(token) };
101
+ }
102
+ }
103
+ /** Nonce de permit actual del pagador. Cambia con cada pago consumido. */
104
+ export async function permitNonce(publicClient, token, owner) {
105
+ return publicClient.readContract({
106
+ address: token,
107
+ abi: [
108
+ {
109
+ type: 'function',
110
+ name: 'nonces',
111
+ stateMutability: 'view',
112
+ inputs: [{ name: 'owner', type: 'address' }],
113
+ outputs: [{ name: '', type: 'uint256' }],
114
+ },
115
+ ],
116
+ functionName: 'nonces',
117
+ args: [owner],
118
+ });
119
+ }
120
+ export function buildQuote(params) {
121
+ const now = params.nowS ?? Math.floor(Date.now() / 1000);
122
+ return {
123
+ x402Version: X402_VERSION,
124
+ accepts: [
125
+ {
126
+ scheme: X402_SERVER_SCHEME,
127
+ network: 'monad',
128
+ chainId: chainFor(params.network ?? 'mainnet').id,
129
+ asset: params.asset,
130
+ assetSymbol: params.assetSymbol,
131
+ amount: params.amount.toString(),
132
+ payTo: params.payTo,
133
+ resource: params.resource,
134
+ description: params.description,
135
+ deadline: now + QUOTE_TTL_S,
136
+ maxTimeoutSeconds: 120,
137
+ payerNonce: params.payerNonce?.toString(),
138
+ domain: params.domain,
139
+ },
140
+ ],
141
+ hint: 'Sign an EIP-2612 permit with the domain and fields of accepts[0] (spender = payTo, ' +
142
+ 'value = amount, nonce = your current token nonce, deadline <= the one given) and repeat the ' +
143
+ 'request with the header X-Payment: base64({scheme,payer,value,deadline,signature}).',
144
+ };
145
+ }
146
+ /** Descodifica y valida la forma de X-Payment. Nunca lanza: devuelve el motivo. */
147
+ export function parsePaymentHeader(header) {
148
+ // Validación explícita: Buffer.from(…, 'base64') NO lanza con entrada
149
+ // inválida, se limita a ignorar los caracteres que no reconoce. Sin esta
150
+ // comprobación, una cabecera basura llegaba al JSON.parse y el error que se
151
+ // devolvía culpaba al JSON en vez de al base64.
152
+ const encoded = header.trim();
153
+ if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded)) {
154
+ return { ok: false, error: 'the X-Payment header is not base64' };
155
+ }
156
+ const json = Buffer.from(encoded, 'base64').toString('utf8');
157
+ let raw;
158
+ try {
159
+ raw = JSON.parse(json);
160
+ }
161
+ catch {
162
+ return { ok: false, error: 'the contents of X-Payment are not JSON' };
163
+ }
164
+ const { scheme, payer, value, deadline, signature } = raw;
165
+ if (typeof scheme !== 'string' || scheme !== X402_SERVER_SCHEME) {
166
+ return { ok: false, error: `unsupported scheme (expected "${X402_SERVER_SCHEME}")` };
167
+ }
168
+ if (typeof payer !== 'string' || !isAddress(payer))
169
+ return { ok: false, error: 'payer is not an address' };
170
+ if (typeof signature !== 'string' || !isHex(signature) || signature.length !== 132) {
171
+ return { ok: false, error: 'signature must be a 65-byte hex signature' };
172
+ }
173
+ let valueBig;
174
+ let deadlineBig;
175
+ try {
176
+ valueBig = BigInt(String(value));
177
+ deadlineBig = BigInt(String(deadline));
178
+ }
179
+ catch {
180
+ return { ok: false, error: 'value and deadline must be integers' };
181
+ }
182
+ if (valueBig <= 0n)
183
+ return { ok: false, error: 'value must be greater than zero' };
184
+ return {
185
+ ok: true,
186
+ payment: { scheme, payer: getAddress(payer), value: valueBig, deadline: deadlineBig, signature },
187
+ };
188
+ }
189
+ /** Parte la firma de 65 bytes en (v, r, s) para pasársela a `permit`. */
190
+ export function splitSignature(signature) {
191
+ const r = slice(signature, 0, 32);
192
+ const s = slice(signature, 32, 64);
193
+ let v = hexToNumber(slice(signature, 64, 65));
194
+ if (v < 27)
195
+ v += 27; // algunas wallets firman con 0/1
196
+ return { v, r, s };
197
+ }
198
+ /**
199
+ * Cola por pagador.
200
+ *
201
+ * ESTA ES LA TRAMPA DEL ESQUEMA. El nonce de EIP-2612 es SECUENCIAL por
202
+ * dirección: si el mismo cliente lanza dos llamadas en paralelo, ambas firman
203
+ * con el nonce N y solo una puede consumirse; la otra revierte en cadena —y
204
+ * para entonces ya le habríamos servido el recurso—. Serializando por pagador,
205
+ * cada cobro lee el nonce actualizado y firma sobre él.
206
+ *
207
+ * Los pagadores distintos siguen yendo en paralelo: la cola es por dirección.
208
+ */
209
+ const payerQueues = new Map();
210
+ /** Exportada para poder probar la serialización directamente. */
211
+ export function enqueueByPayer(payer, fn) {
212
+ const key = payer.toLowerCase();
213
+ const prev = payerQueues.get(key) ?? Promise.resolve();
214
+ const next = prev.then(fn, fn);
215
+ // La cola guarda la promesa "apagada" para que un fallo no la rompa.
216
+ payerQueues.set(key, next.then(() => undefined, () => undefined));
217
+ void next.catch(() => undefined);
218
+ return next;
219
+ }
220
+ /**
221
+ * Verifica la firma y cobra on-chain. Devuelve el hash de la transacción.
222
+ * El recurso NO debe servirse hasta que esto salga bien.
223
+ */
224
+ export async function verifyAndSettle(deps, payment, price) {
225
+ if (payment.value < price) {
226
+ return { ok: false, status: 402, error: `the payment (${payment.value}) is less than the price (${price})` };
227
+ }
228
+ const nowS = BigInt(Math.floor(Date.now() / 1000));
229
+ if (payment.deadline < nowS + BigInt(MIN_DEADLINE_MARGIN_S)) {
230
+ return { ok: false, status: 402, error: 'the payment authorization has expired or has too little margin left' };
231
+ }
232
+ if (!deps.walletClient) {
233
+ return { ok: false, status: 503, error: 'the agent has no wallet to execute the charge' };
234
+ }
235
+ const chain = chainFor(deps.network ?? 'mainnet');
236
+ return enqueueByPayer(payment.payer, async () => {
237
+ // El nonce se lee DENTRO de la cola: si otro pago del mismo pagador acaba
238
+ // de consumirse, aquí ya se ve el valor nuevo.
239
+ const nonce = await permitNonce(deps.publicClient, deps.token, payment.payer);
240
+ const valid = await verifyTypedData({
241
+ address: payment.payer,
242
+ ...permitTypedData(deps.domain, {
243
+ owner: payment.payer,
244
+ spender: deps.payee,
245
+ value: payment.value,
246
+ nonce,
247
+ deadline: payment.deadline,
248
+ }),
249
+ signature: payment.signature,
250
+ }).catch(() => false);
251
+ if (!valid) {
252
+ return {
253
+ ok: false,
254
+ status: 402,
255
+ error: `the signature is not a valid permit from ${payment.payer} ` +
256
+ `(current nonce ${nonce}; if you signed with another, ask for a new quote)`,
257
+ };
258
+ }
259
+ const balance = (await deps.publicClient.readContract({
260
+ address: deps.token,
261
+ abi: erc20Abi,
262
+ functionName: 'balanceOf',
263
+ args: [payment.payer],
264
+ }));
265
+ if (balance < payment.value) {
266
+ return {
267
+ ok: false,
268
+ status: 402,
269
+ error: `payer balance is not enough (${balance} < ${payment.value})`,
270
+ };
271
+ }
272
+ const { v, r, s } = splitSignature(payment.signature);
273
+ const wallet = deps.walletClient;
274
+ // permit + transferFrom. Se simula antes para no quemar gas en una
275
+ // transacción condenada (firma consumida, deadline pasado, saldo movido).
276
+ await deps.publicClient.simulateContract({
277
+ address: deps.token,
278
+ abi: permitAbi,
279
+ functionName: 'permit',
280
+ args: [payment.payer, deps.payee, payment.value, payment.deadline, v, r, s],
281
+ account: wallet.account,
282
+ });
283
+ const permitTx = await wallet.writeContract({
284
+ address: deps.token,
285
+ abi: permitAbi,
286
+ functionName: 'permit',
287
+ args: [payment.payer, deps.payee, payment.value, payment.deadline, v, r, s],
288
+ account: wallet.account,
289
+ chain,
290
+ });
291
+ await deps.publicClient.waitForTransactionReceipt({ hash: permitTx });
292
+ const transferTx = await wallet.writeContract({
293
+ address: deps.token,
294
+ abi: tokenExtraAbi,
295
+ functionName: 'transferFrom',
296
+ args: [payment.payer, deps.payee, payment.value],
297
+ account: wallet.account,
298
+ chain,
299
+ });
300
+ const receipt = await deps.publicClient.waitForTransactionReceipt({ hash: transferTx });
301
+ if (receipt.status !== 'success') {
302
+ return { ok: false, status: 502, error: 'the transfer reverted on chain' };
303
+ }
304
+ return { ok: true, txHash: transferTx, amount: payment.value };
305
+ });
306
+ }
307
+ /** Trozos del ERC-20 que no están en el `erc20Abi` del SDK. */
308
+ const tokenExtraAbi = [
309
+ {
310
+ type: 'function',
311
+ name: 'name',
312
+ stateMutability: 'view',
313
+ inputs: [],
314
+ outputs: [{ name: '', type: 'string' }],
315
+ },
316
+ {
317
+ type: 'function',
318
+ name: 'transferFrom',
319
+ stateMutability: 'nonpayable',
320
+ inputs: [
321
+ { name: 'from', type: 'address' },
322
+ { name: 'to', type: 'address' },
323
+ { name: 'amount', type: 'uint256' },
324
+ ],
325
+ outputs: [{ name: '', type: 'bool' }],
326
+ },
327
+ ];
328
+ const permitAbi = [
329
+ {
330
+ type: 'function',
331
+ name: 'permit',
332
+ stateMutability: 'nonpayable',
333
+ inputs: [
334
+ { name: 'owner', type: 'address' },
335
+ { name: 'spender', type: 'address' },
336
+ { name: 'value', type: 'uint256' },
337
+ { name: 'deadline', type: 'uint256' },
338
+ { name: 'v', type: 'uint8' },
339
+ { name: 'r', type: 'bytes32' },
340
+ { name: 's', type: 'bytes32' },
341
+ ],
342
+ outputs: [],
343
+ },
344
+ ];
345
+ /** Identificador estable del recurso pagado, para trazas y recibos. */
346
+ export function resourceId(method, path, body) {
347
+ return keccak256(toHex(`${method} ${path}\n${body}`));
348
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.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,6 +45,6 @@
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.json",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "test": "tsx test/envelope.test.ts && tsx test/sdk.test.ts && tsx test/x402.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"
49
49
  }
50
50
  }