@panal/sdk 0.18.2 → 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 +38 -8
- package/dist/client.js +94 -56
- 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/tablon.d.ts +5 -1
- package/dist/tablon.js +5 -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/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
|
}
|