@panal/sdk 0.18.0 → 0.18.2

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
@@ -129,6 +129,30 @@ export declare class PanalClient {
129
129
  constructor(options?: PanalClientOptions);
130
130
  /** El wallet client, o un error que dice exactamente qué falta. */
131
131
  private wallet;
132
+ /**
133
+ * Comprueba ANTES de firmar que la wallet cubre lo que Monad va a reservar.
134
+ *
135
+ * Monad bloquea `gas_limit × maxFeePerGas` antes de ejecutar, aunque luego
136
+ * cobre bastante menos: darse de alta estima ~264.000 de gas, que a 122 gwei
137
+ * son 0,032 MON de reserva (medido el 2026-09-14). Con menos, el nodo
138
+ * rechaza la transacción con «Signer had insufficient balance» y viem lo
139
+ * presenta como un revert del contrato, que no llegó a ejecutarse. Y hay una
140
+ * segunda trampa: reintentar tras recargar con el mismo nonce y las mismas
141
+ * comisiones firma una transacción idéntica, y el nodo repite el rechazo sin
142
+ * volver a mirar el saldo.
143
+ *
144
+ * Si no se puede estimar —RPC caído, o una llamada que el contrato rechazaría
145
+ * por otro motivo— no se bloquea nada: que hable el error de verdad.
146
+ */
147
+ private comprobarReserva;
148
+ /**
149
+ * Espera el recibo y comprueba que la transacción SALIÓ BIEN.
150
+ *
151
+ * Esperar el recibo no basta: un revert también llega con recibo. Sin mirar
152
+ * `status`, una transacción que gastó gas y no cambió nada volvía como si
153
+ * todo hubiera ido bien.
154
+ */
155
+ private esperarExito;
132
156
  /** Todos los agentes del registry, activos e inactivos. */
133
157
  listAgents(): Promise<Agent[]>;
134
158
  /**
@@ -412,7 +436,18 @@ export declare class PanalClient {
412
436
  * ya están desplegados sin obligarles a actualizarse.
413
437
  */
414
438
  private x402Endpoint;
415
- /** Retira lo acreditado en una moneda (patrón pull payment). */
439
+ /**
440
+ * Retira lo acreditado en una moneda (patrón pull payment).
441
+ *
442
+ * EL GAS VA FIJADO A MANO, y no es un detalle. viem no estima: le pide al nodo
443
+ * que rellene la transacción (`eth_fillTransaction`), y el de Monad devuelve
444
+ * un gas disparatado para retirar MON —1,05 M para una wallet, 10,7 M para
445
+ * otra— cuando lo necesario son 55.157. Monad cobra el LÍMITE entero, no lo
446
+ * usado: una retirada de 1,092 MON pagó 1,096 MON de gas (2026-09-14, tx
447
+ * 0xa960cb7e…). El gas sale de `eth_estimateGas`, que da el número bueno, con
448
+ * un 10 % de margen; y si la estimación vuelve por encima de `TOPE_GAS_RETIRADA`
449
+ * no se firma nada.
450
+ */
416
451
  withdraw(currency?: Address): Promise<Hex>;
417
452
  /** Comprueba el saldo antes de firmar, para fallar con un mensaje legible. */
418
453
  private assertFunds;
package/dist/client.js CHANGED
@@ -24,6 +24,11 @@ import { descend, newEnvelope, remainingBudget } from './envelope.js';
24
24
  import { NATIVE_CURRENCY, addressesFor, chainFor } from './chains.js';
25
25
  import { BUZON_URL, TABLON, VENTANA_FIRMA_S, encargoSignMessage, entregaSignMessage, ofertaSignMessage, } from './tablon.js';
26
26
  import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js';
27
+ /**
28
+ * Por encima de esto una retirada no se firma: retirar MON son 55.157 de gas y
29
+ * $PANAL 103.511. Ver `withdraw()` para lo que pasó sin este tope.
30
+ */
31
+ const TOPE_GAS_RETIRADA = 300000n;
27
32
  /** Cuántos agentes se leen por llamada al registry. */
28
33
  const REGISTRY_PAGE = 50n;
29
34
  /** Tope duro de agentes recorridos, por si el registro crece mucho. */
@@ -126,6 +131,55 @@ export class PanalClient {
126
131
  }
127
132
  return this.walletClient;
128
133
  }
134
+ /**
135
+ * Comprueba ANTES de firmar que la wallet cubre lo que Monad va a reservar.
136
+ *
137
+ * Monad bloquea `gas_limit × maxFeePerGas` antes de ejecutar, aunque luego
138
+ * cobre bastante menos: darse de alta estima ~264.000 de gas, que a 122 gwei
139
+ * son 0,032 MON de reserva (medido el 2026-09-14). Con menos, el nodo
140
+ * rechaza la transacción con «Signer had insufficient balance» y viem lo
141
+ * presenta como un revert del contrato, que no llegó a ejecutarse. Y hay una
142
+ * segunda trampa: reintentar tras recargar con el mismo nonce y las mismas
143
+ * comisiones firma una transacción idéntica, y el nodo repite el rechazo sin
144
+ * volver a mirar el saldo.
145
+ *
146
+ * Si no se puede estimar —RPC caído, o una llamada que el contrato rechazaría
147
+ * por otro motivo— no se bloquea nada: que hable el error de verdad.
148
+ */
149
+ async comprobarReserva(que, llamada) {
150
+ let gas;
151
+ let maxFeePerGas;
152
+ let saldo;
153
+ try {
154
+ [gas, { maxFeePerGas }, saldo] = await Promise.all([
155
+ this.publicClient.estimateContractGas({ ...llamada, account: this.account }),
156
+ this.publicClient.estimateFeesPerGas(),
157
+ this.publicClient.getBalance({ address: this.account.address }),
158
+ ]);
159
+ }
160
+ catch {
161
+ return;
162
+ }
163
+ const reserva = gas * maxFeePerGas + (llamada.value ?? 0n);
164
+ if (saldo < reserva) {
165
+ throw new Error(`${que}: Monad reserva ${formatEther(reserva)} MON por adelantado (límite de gas × precio máximo), ` +
166
+ `aunque luego cobre menos. ${this.account.address} tiene ${formatEther(saldo)} MON: ` +
167
+ `faltan ${formatEther(reserva - saldo)} MON. No se ha enviado nada.`);
168
+ }
169
+ }
170
+ /**
171
+ * Espera el recibo y comprueba que la transacción SALIÓ BIEN.
172
+ *
173
+ * Esperar el recibo no basta: un revert también llega con recibo. Sin mirar
174
+ * `status`, una transacción que gastó gas y no cambió nada volvía como si
175
+ * todo hubiera ido bien.
176
+ */
177
+ async esperarExito(hash, que) {
178
+ const recibo = await this.publicClient.waitForTransactionReceipt({ hash });
179
+ if (recibo.status !== 'success') {
180
+ throw new Error(`${que} revirtió (tx ${hash}): se cobró el gas y no cambió nada.`);
181
+ }
182
+ }
129
183
  // -------------------------------------------------------------------------
130
184
  // Lectura
131
185
  // -------------------------------------------------------------------------
@@ -526,15 +580,21 @@ export class PanalClient {
526
580
  */
527
581
  async registerAgent(params) {
528
582
  const wallet = this.wallet();
529
- const hash = await wallet.writeContract({
583
+ const llamada = {
530
584
  address: this.addresses.registry,
531
585
  abi: registryAbi,
532
586
  functionName: 'registerAgent',
533
587
  args: [formatAgentMetadata(params.metadata), params.pricePerTask, params.currency ?? NATIVE_CURRENCY],
588
+ };
589
+ await this.comprobarReserva('No se puede dar de alta', llamada);
590
+ const hash = await wallet.writeContract({
591
+ ...llamada,
592
+ abi: registryAbi,
593
+ functionName: 'registerAgent',
534
594
  chain: chainFor(this.network),
535
595
  account: this.account,
536
596
  });
537
- await this.publicClient.waitForTransactionReceipt({ hash });
597
+ await this.esperarExito(hash, 'El alta');
538
598
  return hash;
539
599
  }
540
600
  /** Cambia el nombre, la descripción, las skills o el endpoint publicados. */
@@ -608,7 +668,7 @@ export class PanalClient {
608
668
  chain: chainFor(this.network),
609
669
  account: this.account,
610
670
  });
611
- await this.publicClient.waitForTransactionReceipt({ hash: txHash });
671
+ await this.esperarExito(txHash, `La entrega de #${taskId}`);
612
672
  return { txHash, resultHash };
613
673
  }
614
674
  /**
@@ -747,7 +807,7 @@ export class PanalClient {
747
807
  chain: chainFor(this.network),
748
808
  account: this.account,
749
809
  });
750
- await this.publicClient.waitForTransactionReceipt({ hash: txHash });
810
+ await this.esperarExito(txHash, `Coger la tarea #${taskId}`);
751
811
  return { txHash };
752
812
  }
753
813
  /**
@@ -990,18 +1050,39 @@ export class PanalClient {
990
1050
  }
991
1051
  return rutaDeAgente(base, 'x402/ask');
992
1052
  }
993
- /** Retira lo acreditado en una moneda (patrón pull payment). */
1053
+ /**
1054
+ * Retira lo acreditado en una moneda (patrón pull payment).
1055
+ *
1056
+ * EL GAS VA FIJADO A MANO, y no es un detalle. viem no estima: le pide al nodo
1057
+ * que rellene la transacción (`eth_fillTransaction`), y el de Monad devuelve
1058
+ * un gas disparatado para retirar MON —1,05 M para una wallet, 10,7 M para
1059
+ * otra— cuando lo necesario son 55.157. Monad cobra el LÍMITE entero, no lo
1060
+ * usado: una retirada de 1,092 MON pagó 1,096 MON de gas (2026-09-14, tx
1061
+ * 0xa960cb7e…). El gas sale de `eth_estimateGas`, que da el número bueno, con
1062
+ * un 10 % de margen; y si la estimación vuelve por encima de `TOPE_GAS_RETIRADA`
1063
+ * no se firma nada.
1064
+ */
994
1065
  async withdraw(currency = NATIVE_CURRENCY) {
995
1066
  const wallet = this.wallet();
996
- const hash = await wallet.writeContract({
1067
+ const llamada = {
997
1068
  address: this.addresses.escrow,
998
1069
  abi: escrowAbi,
999
1070
  functionName: 'withdraw',
1000
1071
  args: [currency],
1072
+ };
1073
+ const estimado = await this.publicClient.estimateContractGas({ ...llamada, account: this.account });
1074
+ const gas = (estimado * 11n + 9n) / 10n;
1075
+ if (gas > TOPE_GAS_RETIRADA) {
1076
+ throw new Error(`La estimación de gas para retirar salió en ${estimado}, muy por encima de lo normal (~55.000 en MON, ` +
1077
+ `~104.000 en $PANAL). Monad cobra el límite entero: no se ha enviado nada.`);
1078
+ }
1079
+ const hash = await wallet.writeContract({
1080
+ ...llamada,
1081
+ gas,
1001
1082
  chain: chainFor(this.network),
1002
1083
  account: this.account,
1003
1084
  });
1004
- await this.publicClient.waitForTransactionReceipt({ hash });
1085
+ await this.esperarExito(hash, 'La retirada');
1005
1086
  return hash;
1006
1087
  }
1007
1088
  /** Comprueba el saldo antes de firmar, para fallar con un mensaje legible. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
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",
48
+ "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts && tsx test/tablon.test.ts && tsx test/reserva.test.ts",
49
49
  "test:nombres": "tsx test/nombres.test.ts"
50
50
  }
51
51
  }