panal-mcp 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/server.js +97 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
|
37
37
|
| `panal_get_agent` | Ficha completa: precio, skills, estado y nombre único leído de la cadena |
|
|
38
38
|
| `panal_get_task` | Estado de un encargo en el escrow |
|
|
39
39
|
| `panal_marketplace_stats` | Cifras del marketplace |
|
|
40
|
-
| `panal_wallet` | Saldo y presupuesto restante *(escritura)* |
|
|
40
|
+
| `panal_wallet` | Saldo en cada moneda (MON y $PANAL), presupuesto restante y lo que el escrow te debe *(escritura)* |
|
|
41
41
|
| `panal_quote_hire` | Presupuesta un encargo sin pagar, y comprueba que el agente responde *(escritura)* |
|
|
42
42
|
| `panal_hire` | Contrata y bloquea el pago *(escritura)* |
|
|
43
43
|
| `panal_send_brief` | Reenvía el encargo si no llegó al contratar *(escritura)* |
|
|
@@ -46,7 +46,7 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
|
46
46
|
| `panal_quote_ask` / `panal_ask` | Pregunta suelta a un agente, sin abrir un encargo *(escritura)* |
|
|
47
47
|
| `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
|
|
48
48
|
| `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
|
|
49
|
-
| `panal_withdraw` | Retira lo que el escrow te debe *(escritura)* |
|
|
49
|
+
| `panal_withdraw` | Retira lo que el escrow te debe, en todas las monedas *(escritura)* |
|
|
50
50
|
|
|
51
51
|
Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
|
|
52
52
|
hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
|
package/dist/server.js
CHANGED
|
@@ -34,7 +34,7 @@ import { createInterface } from 'node:readline';
|
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
35
35
|
import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
|
|
36
36
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
37
|
-
import { NATIVE_CURRENCY, TaskStatus, createPanalClient, } from '@panal/sdk';
|
|
37
|
+
import { NATIVE_CURRENCY, TaskStatus, createPanalClient, erc20Abi, } from '@panal/sdk';
|
|
38
38
|
import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
|
|
39
39
|
import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
|
|
40
40
|
const SERVER_NAME = 'panal';
|
|
@@ -94,8 +94,39 @@ function writesBlockedReason() {
|
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
95
95
|
// Presentación
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
97
|
+
/** ¿Es la moneda nativa de la cadena, o un token? */
|
|
98
|
+
function esNativa(currency) {
|
|
99
|
+
return currency.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
|
|
100
|
+
}
|
|
97
101
|
function symbolOf(currency) {
|
|
98
|
-
return currency
|
|
102
|
+
return esNativa(currency) ? 'MON' : '$PANAL';
|
|
103
|
+
}
|
|
104
|
+
/** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
|
|
105
|
+
function monedasConocidas() {
|
|
106
|
+
return [...limits.porMoneda.keys()];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Saldo de una wallet en una moneda: a la cadena si es la nativa, al contrato
|
|
110
|
+
* del token si es un ERC-20.
|
|
111
|
+
*
|
|
112
|
+
* `null` es «no se pudo leer», y se distingue de cero a propósito. Enseñar 0
|
|
113
|
+
* cuando la consulta falla es afirmar que no hay fondos, que es una respuesta
|
|
114
|
+
* distinta: llevaría a no intentar un pago que sí se podía hacer.
|
|
115
|
+
*/
|
|
116
|
+
async function saldoDe(owner, currency) {
|
|
117
|
+
try {
|
|
118
|
+
if (esNativa(currency))
|
|
119
|
+
return await panal.publicClient.getBalance({ address: owner });
|
|
120
|
+
return (await panal.publicClient.readContract({
|
|
121
|
+
address: currency,
|
|
122
|
+
abi: erc20Abi,
|
|
123
|
+
functionName: 'balanceOf',
|
|
124
|
+
args: [owner],
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
99
130
|
}
|
|
100
131
|
/**
|
|
101
132
|
* ¿Cabe este gasto en el presupuesto de SU moneda? Devuelve el motivo, o null.
|
|
@@ -370,36 +401,54 @@ const READ_TOOLS = [
|
|
|
370
401
|
const WRITE_TOOLS = [
|
|
371
402
|
{
|
|
372
403
|
name: 'panal_wallet',
|
|
373
|
-
description: 'State of the wallet this server would pay with: address, balance
|
|
374
|
-
'is left. Call it before hiring to know
|
|
404
|
+
description: 'State of the wallet this server would pay with: address, the balance of EVERY currency Panal ' +
|
|
405
|
+
'takes (MON and $PANAL), and how much of today\'s budget is left. Call it before hiring to know ' +
|
|
406
|
+
'whether there are funds.',
|
|
375
407
|
inputSchema: { type: 'object', properties: {} },
|
|
376
408
|
handler: async () => {
|
|
377
409
|
const blocked = writesBlockedReason();
|
|
378
410
|
if (blocked)
|
|
379
411
|
return blocked;
|
|
380
412
|
const address = account.address;
|
|
381
|
-
const
|
|
413
|
+
const monedas = monedasConocidas();
|
|
414
|
+
// Un saldo por moneda, no solo el nativo. Antes se leía únicamente MON y
|
|
415
|
+
// justo debajo se listaban los topes de $PANAL: quien lo leía daba por
|
|
416
|
+
// comprobado un saldo que nadie había mirado, y podía contratar en $PANAL
|
|
417
|
+
// sin tener con qué pagar. Las lecturas son independientes, así que van
|
|
418
|
+
// en paralelo.
|
|
419
|
+
const [saldos, pendientes] = await Promise.all([
|
|
420
|
+
Promise.all(monedas.map((m) => saldoDe(address, m))),
|
|
421
|
+
// Lo acreditado en el escrow no aparece en el balance de la wallet: es
|
|
422
|
+
// pull payment, así que un reembolso se queda ahí quieto hasta que se
|
|
423
|
+
// reclama. Sin decirlo, el dinero devuelto es indistinguible del
|
|
424
|
+
// perdido — y también hay que preguntarlo moneda a moneda.
|
|
425
|
+
Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
|
|
426
|
+
]);
|
|
427
|
+
const importe = (v) => (v === null ? 'could not be read' : formatEther(v));
|
|
428
|
+
const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null)}`);
|
|
382
429
|
// Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
|
|
383
430
|
// tipo de cambio, así que se enseñan por separado o no se entienden.
|
|
384
|
-
const presupuestos =
|
|
431
|
+
const presupuestos = monedas.map((moneda) => {
|
|
432
|
+
const lim = limitFor(limits, moneda);
|
|
385
433
|
const spent = ledger.spentToday(moneda);
|
|
386
434
|
const left = lim.dailyBudgetWei > spent ? lim.dailyBudgetWei - spent : 0n;
|
|
387
|
-
|
|
388
|
-
return (` ${sym}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
|
|
435
|
+
return (` ${symbolOf(moneda)}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
|
|
389
436
|
`${formatEther(lim.dailyBudgetWei)} spent · ${formatEther(left)} left`);
|
|
390
437
|
});
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
438
|
+
// Solo se nombra lo que hay que cobrar, o lo que no se pudo comprobar. Un
|
|
439
|
+
// «0» por cada moneda sería ruido en la respuesta más leída del servidor.
|
|
440
|
+
const enEscrow = monedas
|
|
441
|
+
.map((m, i) => ({ sym: symbolOf(m), v: pendientes[i] ?? null }))
|
|
442
|
+
.filter(({ v }) => v === null || v > 0n)
|
|
443
|
+
.map(({ sym, v }) => ` ${sym}: ${importe(v)}`);
|
|
395
444
|
return [
|
|
396
445
|
`Wallet: ${address}`,
|
|
397
|
-
|
|
446
|
+
'Balance:',
|
|
447
|
+
...balances,
|
|
398
448
|
'Caps and budgets (each currency is counted on its own):',
|
|
399
449
|
...presupuestos,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
: null,
|
|
450
|
+
enEscrow.length ? 'Waiting in the escrow — collect it with panal_withdraw:' : null,
|
|
451
|
+
...enEscrow,
|
|
403
452
|
]
|
|
404
453
|
.filter(Boolean)
|
|
405
454
|
.join('\n');
|
|
@@ -838,9 +887,9 @@ const WRITE_TOOLS = [
|
|
|
838
887
|
},
|
|
839
888
|
{
|
|
840
889
|
name: 'panal_withdraw',
|
|
841
|
-
description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs
|
|
842
|
-
'payment: it credits balances and never pushes
|
|
843
|
-
'Call panal_wallet first to see
|
|
890
|
+
description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs — in EVERY currency it ' +
|
|
891
|
+
'holds, one transaction each. The escrow is pull payment: it credits balances and never pushes ' +
|
|
892
|
+
'them, so money sits there until this is called. Call panal_wallet first to see what is waiting.',
|
|
844
893
|
inputSchema: {
|
|
845
894
|
type: 'object',
|
|
846
895
|
properties: {
|
|
@@ -856,11 +905,36 @@ const WRITE_TOOLS = [
|
|
|
856
905
|
return 'I will not move funds without confirmed_by_user: true.';
|
|
857
906
|
}
|
|
858
907
|
try {
|
|
859
|
-
|
|
860
|
-
|
|
908
|
+
// Se retira MONEDA A MONEDA. El escrow lleva un saldo por cada una y
|
|
909
|
+
// `withdraw` solo vacía la que se le pide; preguntando únicamente por
|
|
910
|
+
// la nativa, un reembolso en $PANAL quedaba invisible y esta misma
|
|
911
|
+
// herramienta contestaba que no había nada que cobrar. El dinero seguía
|
|
912
|
+
// en el contrato, pero desde aquí no había forma de sacarlo.
|
|
913
|
+
const monedas = monedasConocidas();
|
|
914
|
+
const pendientes = [];
|
|
915
|
+
for (const moneda of monedas) {
|
|
916
|
+
const importe = await panal.getPendingWithdrawal(account.address, moneda);
|
|
917
|
+
if (importe > 0n)
|
|
918
|
+
pendientes.push({ moneda, importe });
|
|
919
|
+
}
|
|
920
|
+
if (!pendientes.length)
|
|
861
921
|
return 'The escrow owes this wallet nothing: there is nothing to collect.';
|
|
862
|
-
|
|
863
|
-
|
|
922
|
+
// Cada moneda es una transacción aparte, y si una falla se sigue con
|
|
923
|
+
// las demás: quedarse sin cobrar el $PANAL porque el MON se quedó sin
|
|
924
|
+
// gas es perder dos veces.
|
|
925
|
+
const lineas = [];
|
|
926
|
+
for (const { moneda, importe } of pendientes) {
|
|
927
|
+
const sym = symbolOf(moneda);
|
|
928
|
+
try {
|
|
929
|
+
const hash = await panal.withdraw(moneda);
|
|
930
|
+
lineas.push(`Collected ${formatEther(importe)} ${sym} from the escrow.\n tx: ${EXPLORER}/tx/${hash}`);
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
lineas.push(`Could not collect the ${formatEther(importe)} ${sym}: ${err instanceof Error ? err.message : err}` +
|
|
934
|
+
'\n It is still credited in the escrow; calling this again will retry it.');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return lineas.join('\n');
|
|
864
938
|
}
|
|
865
939
|
catch (err) {
|
|
866
940
|
return `Could not withdraw: ${err instanceof Error ? err.message : err}`;
|