agentwallet-mcp 1.12.5 → 1.13.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/README.md +381 -376
- package/build/index.js +342 -148
- package/build/local-solana.js +70 -4
- package/build/local-wallet.d.ts +1 -0
- package/build/local-wallet.js +81 -13
- package/build/ssrf-guard.d.ts +9 -0
- package/build/ssrf-guard.js +39 -2
- package/build/wrapped-native.d.ts +11 -0
- package/build/wrapped-native.js +20 -0
- package/build/x402-eip3009.js +6 -1
- package/build/x402-payment.d.ts +59 -0
- package/build/x402-payment.js +111 -0
- package/build/x402-permit2.js +3 -1
- package/package.json +21 -7
package/build/index.js
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
14
|
import { z } from 'zod';
|
|
15
|
-
import { assertPublicUrl, safeFetch } from './ssrf-guard.js';
|
|
16
|
-
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, } from './x402-payment.js';
|
|
15
|
+
import { assertPublicUrl, safeFetch, FINAL_URL_HEADER } from './ssrf-guard.js';
|
|
16
|
+
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, assetLabel, autopayAssetAllowed, autopayEnvCap, effectiveAutopayCap, maxAuthWindowSeconds, } from './x402-payment.js';
|
|
17
17
|
import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
|
|
18
18
|
import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upto } from './local-wallet.js';
|
|
19
|
+
import { WRAPPED_NATIVE } from './wrapped-native.js';
|
|
19
20
|
import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, PERMIT2_ADDRESS, } from './x402-permit2.js';
|
|
20
21
|
import { assessTokenRisk } from './token-risk.js';
|
|
21
22
|
import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, } from './x402-eip3009.js';
|
|
@@ -30,6 +31,11 @@ const API_USER = process.env.AGENTWALLET_USER || '';
|
|
|
30
31
|
const API_PASS = process.env.AGENTWALLET_PASS || ''; // WordPress application password
|
|
31
32
|
const X402_WALLET_ID = process.env.AGENTWALLET_WALLET_ID || ''; // Wallet ID for x402 auto-pay
|
|
32
33
|
const DASHBOARD_URL = process.env.AGENTWALLET_DASHBOARD_URL || 'https://hifriendbot.com/wallet/';
|
|
34
|
+
// Basic credentials and 402 auto-pay decisions ride on this URL, so it must be
|
|
35
|
+
// https, or loopback for a local test server. Anything else is refused at start.
|
|
36
|
+
if (/^http:/i.test(API_BASE) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i.test(API_BASE)) {
|
|
37
|
+
throw new Error(`AGENTWALLET_API_URL must be https (got ${API_BASE}); plain http is allowed only for localhost.`);
|
|
38
|
+
}
|
|
33
39
|
/* ── Local-mode routing ──────────────────────────────────────────
|
|
34
40
|
Translates the REST paths the tools already speak into in-process signing
|
|
35
41
|
calls. Anything not listed here falls through to the hosted API, so
|
|
@@ -222,7 +228,7 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
222
228
|
const res = await fetch(url, options);
|
|
223
229
|
const data = await res.json();
|
|
224
230
|
// Handle 402 Payment Required — auto-pay if a wallet or a local key is configured
|
|
225
|
-
if (res.status === 402 && (X402_WALLET_ID || anyLocalMode()) && !extraHeaders?.['X-PAYMENT']) {
|
|
231
|
+
if (res.status === 402 && (X402_WALLET_ID || anyLocalMode()) && !extraHeaders?.['X-PAYMENT'] && !extraHeaders?.['X-AGW-SKIP-X402']) {
|
|
226
232
|
return handleX402Payment(data, path, method, body);
|
|
227
233
|
}
|
|
228
234
|
if (!res.ok) {
|
|
@@ -287,10 +293,15 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
|
|
|
287
293
|
throw new Error('402 Payment Required but no payment options available.');
|
|
288
294
|
}
|
|
289
295
|
const accept = accepts[0]; // Use first option
|
|
290
|
-
const payTo = accept.payTo;
|
|
296
|
+
const payTo = String(accept.payTo || '');
|
|
291
297
|
// Determine chain_id from network string (CAIP-2, plain name, or raw ID)
|
|
292
298
|
const network = accept.network || '';
|
|
293
|
-
const chainId = resolveChainId(network)
|
|
299
|
+
const chainId = resolveChainId(network);
|
|
300
|
+
if (chainId === null)
|
|
301
|
+
throw new Error(`x402 auto-pay: unknown network "${network}" in the payment requirements; refusing to guess a chain.`);
|
|
302
|
+
if (isSolanaChain(chainId) ? !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(payTo) : !/^0x[0-9a-fA-F]{40}$/.test(payTo)) {
|
|
303
|
+
throw new Error(`x402 auto-pay: payTo "${payTo}" is not a valid address for chain ${chainId}.`);
|
|
304
|
+
}
|
|
294
305
|
// x402 maxAmountRequired is ALREADY in base/atomic units (e.g. "10000" =
|
|
295
306
|
// 0.01 USDC at 6 decimals) and must be used directly. The previous code ran
|
|
296
307
|
// it through parseUnits(), which re-scaled it by 10**decimals and overpaid by
|
|
@@ -298,8 +309,11 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
|
|
|
298
309
|
// the pay_x402 tool) and enforces a hard ceiling so a malformed or malicious
|
|
299
310
|
// 402 response cannot drain the wallet. AGENTWALLET_MAX_AUTOPAY is in
|
|
300
311
|
// human-readable units of the asset; default 1.
|
|
301
|
-
const maxAutopay =
|
|
312
|
+
const maxAutopay = autopayEnvCap();
|
|
302
313
|
const assetForDecimals = accept.asset || accept.extra?.token || '';
|
|
314
|
+
const policy = autopayAssetAllowed(chainId, assetForDecimals);
|
|
315
|
+
if (!policy.allowed)
|
|
316
|
+
throw new Error(policy.reason);
|
|
303
317
|
const trustedDecimals = assetForDecimals
|
|
304
318
|
? await resolveTrustedDecimals(chainId, assetForDecimals)
|
|
305
319
|
: nativeDecimals(chainId); // native asset: 18 on EVM (wei), 9 on Solana (lamports)
|
|
@@ -369,6 +383,66 @@ function isValidAddress(address) {
|
|
|
369
383
|
return true;
|
|
370
384
|
return false;
|
|
371
385
|
}
|
|
386
|
+
// ─── Guard helpers ──────────────────────────────────────────────
|
|
387
|
+
/** Request headers the caller may not set on pay_x402: Host would rename the TLS peer, the rest belong to the client. */
|
|
388
|
+
const FORBIDDEN_REQUEST_HEADERS = new Set(['host', 'content-length', 'transfer-encoding', 'connection', 'upgrade', 'expect', 'te', 'trailer', 'keep-alive']);
|
|
389
|
+
/** Authorizations signed in this process, keyed by endpoint and requirement, kept until they expire. */
|
|
390
|
+
const signedPayments = new Map();
|
|
391
|
+
function looksLikeTxHash(v) {
|
|
392
|
+
return typeof v === 'string' && (/^0x[0-9a-fA-F]{64}$/.test(v) || /^[1-9A-HJ-NP-Za-km-z]{86,88}$/.test(v));
|
|
393
|
+
}
|
|
394
|
+
function assertPayTo(chainId, payTo) {
|
|
395
|
+
const p = String(payTo ?? '');
|
|
396
|
+
const ok = isSolanaChain(chainId) ? /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(p) : /^0x[0-9a-fA-F]{40}$/.test(p);
|
|
397
|
+
if (!ok)
|
|
398
|
+
throw new Error(`x402: payTo "${p}" is not a valid address for chain ${chainId}.`);
|
|
399
|
+
}
|
|
400
|
+
/** A URL with credentials in its path or query (Alchemy/Infura style keys) reduced to its origin for display. */
|
|
401
|
+
function redactUrl(u) {
|
|
402
|
+
try {
|
|
403
|
+
const p = new URL(u);
|
|
404
|
+
return p.pathname !== '/' || p.search ? `${p.origin}/…` : p.origin;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return u;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Decimals to scale a human amount by. Always the token's own value (registry,
|
|
412
|
+
* then decimals() on chain); a caller-supplied value must agree or the call
|
|
413
|
+
* is refused, because "18" for USDC scales the amount by a trillion.
|
|
414
|
+
*/
|
|
415
|
+
async function verifiedDecimals(chainId, token, given) {
|
|
416
|
+
let trusted = null;
|
|
417
|
+
if (isSolanaChain(chainId)) {
|
|
418
|
+
trusted = lookupTrustedDecimals(chainId, token);
|
|
419
|
+
if (trusted === null) {
|
|
420
|
+
if (given === undefined)
|
|
421
|
+
throw new Error(`Decimals for mint ${token} are not in the registry; pass decimals explicitly.`);
|
|
422
|
+
return given; // the token program verifies it on-chain (TransferChecked)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
trusted = await resolveTrustedDecimals(chainId, token);
|
|
427
|
+
}
|
|
428
|
+
if (given !== undefined && given !== trusted) {
|
|
429
|
+
throw new Error(`decimals ${given} was passed but ${token} has ${trusted} decimals; refusing to scale the amount by the wrong factor.`);
|
|
430
|
+
}
|
|
431
|
+
return trusted;
|
|
432
|
+
}
|
|
433
|
+
/** Cap variables are read at send time; a typo must fail at startup, not after funding. */
|
|
434
|
+
function validateGuardEnv() {
|
|
435
|
+
for (const name of ['AGENTWALLET_MAX_TX_NATIVE', 'AGENTWALLET_MAX_TX_TOKEN', 'AGENTWALLET_MAX_TX_SOL', 'AGENTWALLET_MAX_AUTOPAY']) {
|
|
436
|
+
const v = (process.env[name] || '').trim();
|
|
437
|
+
if (v && !/^\d+(\.\d+)?$/.test(v))
|
|
438
|
+
throw new Error(`${name} must be a decimal number, got "${v}".`);
|
|
439
|
+
}
|
|
440
|
+
for (const e of (process.env.AGENTWALLET_AUTOPAY_ASSETS || '').split(',').map(s => s.trim()).filter(Boolean)) {
|
|
441
|
+
if (e !== 'native' && !/^0x[0-9a-fA-F]{40}$/.test(e) && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(e)) {
|
|
442
|
+
throw new Error(`AGENTWALLET_AUTOPAY_ASSETS entry "${e}" is not "native", an EVM address or an SPL mint.`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
372
446
|
// ─── EVM Helpers ──────────────────────────────────────────────────
|
|
373
447
|
/**
|
|
374
448
|
* Convert a human-readable amount (e.g. "0.1") to raw units string given decimals.
|
|
@@ -379,8 +453,11 @@ function parseUnits(amount, decimals) {
|
|
|
379
453
|
throw new Error(`Invalid amount "${amount}". Must be a positive number (e.g. "0.1" or "100").`);
|
|
380
454
|
}
|
|
381
455
|
const [whole, frac = ''] = amount.split('.');
|
|
382
|
-
|
|
383
|
-
|
|
456
|
+
// More fractional digits than the token has cannot be sent; truncating would
|
|
457
|
+
// move a different amount than the one echoed back to the caller.
|
|
458
|
+
if (frac.length > decimals)
|
|
459
|
+
throw new Error(`Amount "${amount}" has ${frac.length} decimal places but the token has ${decimals}.`);
|
|
460
|
+
const raw = BigInt(whole + frac.padEnd(decimals, '0'));
|
|
384
461
|
return raw.toString();
|
|
385
462
|
}
|
|
386
463
|
/**
|
|
@@ -398,14 +475,20 @@ function formatUnits(raw, decimals) {
|
|
|
398
475
|
* Pad an address to 32 bytes (64 hex chars) for ABI encoding.
|
|
399
476
|
*/
|
|
400
477
|
function padAddress(address) {
|
|
478
|
+
// An over-long "address" would shift every later calldata word (the amount
|
|
479
|
+
// among them) and the EVM would decode whatever landed in the first 32 bytes.
|
|
480
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address))
|
|
481
|
+
throw new Error(`Not an EVM address: "${address}".`);
|
|
401
482
|
return address.slice(2).toLowerCase().padStart(64, '0');
|
|
402
483
|
}
|
|
403
484
|
/**
|
|
404
485
|
* Encode a uint256 as 32 bytes hex (64 chars).
|
|
405
486
|
*/
|
|
406
487
|
function encodeUint256(value) {
|
|
407
|
-
const
|
|
408
|
-
|
|
488
|
+
const v = BigInt(value);
|
|
489
|
+
if (v < 0n || v > (1n << 256n) - 1n)
|
|
490
|
+
throw new Error(`Amount ${value} does not fit in uint256.`);
|
|
491
|
+
return v.toString(16).padStart(64, '0');
|
|
409
492
|
}
|
|
410
493
|
// ─── Server ──────────────────────────────────────────────────────
|
|
411
494
|
// ─── x402 Network Mapping ──────────────────────────────────────
|
|
@@ -467,87 +550,89 @@ function resolveChainId(network) {
|
|
|
467
550
|
const AddressSchema = z.string().regex(/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/, 'Must be a 0x-prefixed 40-hex EVM address or a base58 Solana address');
|
|
468
551
|
const server = new McpServer({
|
|
469
552
|
name: 'agentwallet',
|
|
470
|
-
version: '1.
|
|
553
|
+
version: '1.13.0',
|
|
471
554
|
}, {
|
|
472
|
-
instructions: `AgentWallet gives AI agents their own blockchain wallets.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
- **
|
|
488
|
-
- **
|
|
489
|
-
- **
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
- Use
|
|
529
|
-
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
|
535
|
-
|
|
536
|
-
|
|
|
537
|
-
|
|
|
538
|
-
|
|
|
539
|
-
|
|
|
540
|
-
|
|
|
541
|
-
|
|
|
542
|
-
|
|
|
543
|
-
|
|
|
544
|
-
|
|
|
545
|
-
|
|
|
555
|
+
instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
|
|
556
|
+
? 'This server runs in LOCAL (self-custody) mode: the operator supplied the key, signing happens in this process, and the only spend guards are the AGENTWALLET_MAX_TX_* environment caps. There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
|
|
557
|
+
: 'Private keys are encrypted server-side and never exposed: agents sign and broadcast transactions without ever touching raw keys.'}
|
|
558
|
+
|
|
559
|
+
## Getting Started
|
|
560
|
+
1. Call list_wallets to see existing wallets, or create_wallet to make a new one.
|
|
561
|
+
2. Fund the wallet by sending tokens to its address from an external source.
|
|
562
|
+
3. Use transfer (native tokens) or transfer_token (ERC-20/SPL tokens) to send funds.
|
|
563
|
+
|
|
564
|
+
## Supported Chains
|
|
565
|
+
- **EVM chains:** Ethereum (1), Base (8453), Arbitrum (42161), Optimism (10), Polygon (137), Avalanche (43114), BSC (56), Zora (7777777), PulseChain (369)
|
|
566
|
+
- **Solana:** Mainnet (900), Devnet (901), Testnet (902)
|
|
567
|
+
- Call get_chains for the full list with native tokens and stablecoin contract addresses.
|
|
568
|
+
|
|
569
|
+
## EVM vs Solana
|
|
570
|
+
- **Addresses:** EVM uses 0x-prefixed hex (42 chars). Solana uses Base58 (32-44 chars).
|
|
571
|
+
- **Native tokens:** EVM = ETH/POL/BNB/etc (18 decimals, wei). Solana = SOL (9 decimals, lamports).
|
|
572
|
+
- **Token standards:** EVM uses ERC-20. Solana uses SPL tokens.
|
|
573
|
+
- **EVM-only tools:** approve_token, get_allowance, wrap_eth, unwrap_eth, call_contract, get_token_info. These do not work on Solana.
|
|
574
|
+
- **Works on both:** create_wallet, transfer, transfer_token, send_transaction, sign_transaction, get_balance, get_token_balance.
|
|
575
|
+
|
|
576
|
+
## Common Workflows
|
|
577
|
+
|
|
578
|
+
### Send native tokens (ETH, SOL, etc.)
|
|
579
|
+
Use the transfer tool with a human-readable amount (e.g. "0.1" ETH). It auto-converts to wei/lamports.
|
|
580
|
+
|
|
581
|
+
### Send ERC-20 or SPL tokens
|
|
582
|
+
Use transfer_token. You need: wallet_id, token contract address, recipient, amount, chain_id, and decimals (6 for USDC, 18 for most ERC-20s). Call get_chains to find stablecoin addresses.
|
|
583
|
+
|
|
584
|
+
### Interact with DeFi (EVM only)
|
|
585
|
+
1. Check allowance with get_allowance.
|
|
586
|
+
2. If needed, call approve_token to let the DeFi contract spend your tokens.
|
|
587
|
+
3. Use send_transaction with the contract's calldata to execute the interaction.
|
|
588
|
+
|
|
589
|
+
### Wrap/unwrap native tokens (EVM only)
|
|
590
|
+
Use wrap_eth to convert ETH → WETH (required by many DeFi protocols). Use unwrap_eth to convert back.
|
|
591
|
+
|
|
592
|
+
### Read on-chain data (EVM only)
|
|
593
|
+
Use call_contract for gas-free read-only calls (eth_call). Provide ABI-encoded calldata.
|
|
594
|
+
|
|
595
|
+
## x402 Payments — Paying for Resources
|
|
596
|
+
The pay_x402 tool handles the full x402 payment flow automatically:
|
|
597
|
+
1. Fetches the URL.
|
|
598
|
+
2. If the server returns HTTP 402, parses the payment requirements.
|
|
599
|
+
3. Pays on-chain using your wallet.
|
|
600
|
+
4. Retries the request with proof of payment.
|
|
601
|
+
Always set max_payment to prevent overspending (e.g. "1.00" for max 1 USDC).
|
|
602
|
+
|
|
603
|
+
## x402 Paywalls — Accepting Payments
|
|
604
|
+
Create paywalls to charge other agents for accessing your resources:
|
|
605
|
+
1. create_paywall — set price, token, chain, and the resource URL to protect.
|
|
606
|
+
2. Share the access URL. Clients that pay on-chain get the resource; others get HTTP 402.
|
|
607
|
+
3. list_paywalls and get_paywall_payments to track revenue.
|
|
608
|
+
4. get_x402_revenue for aggregate stats across all paywalls.
|
|
609
|
+
|
|
610
|
+
## Wallet Security
|
|
611
|
+
- ${anyLocalMode() ? 'pause_wallet does not exist in local mode: revoke the key (rotate it and move the funds) if the process is compromised.' : 'Use pause_wallet to immediately freeze a wallet if compromised. No transactions can be signed while paused.'}
|
|
612
|
+
- Use unpause_wallet to resume operations.
|
|
613
|
+
- Use delete_wallet to permanently disable a wallet.
|
|
614
|
+
- ${anyLocalMode() ? 'Spend limits are the AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL / _AUTOPAY caps the operator set; run wallet_mode to see them. Unset means no limit.' : 'Wallets have server-enforced spending limits.'}
|
|
615
|
+
|
|
616
|
+
## Tool Selection Guide
|
|
617
|
+
| Goal | Tool |
|
|
618
|
+
|------|------|
|
|
619
|
+
| Send ETH/SOL/native tokens | transfer |
|
|
620
|
+
| Send USDC/ERC-20/SPL tokens | transfer_token |
|
|
621
|
+
| Check native balance | get_balance |
|
|
622
|
+
| Check token balance | get_token_balance |
|
|
623
|
+
| Look up token details | get_token_info (EVM only) |
|
|
624
|
+
| Approve DeFi spending | approve_token (EVM only) |
|
|
625
|
+
| Check approval amount | get_allowance (EVM only) |
|
|
626
|
+
| Read smart contract | call_contract (EVM only) |
|
|
627
|
+
| Custom transaction | send_transaction |
|
|
628
|
+
| Sign without broadcasting | sign_transaction |
|
|
629
|
+
| Pay for x402 resource | pay_x402 |
|
|
630
|
+
| Charge for your resource | create_paywall |
|
|
546
631
|
| Check usage/billing | get_usage |`,
|
|
547
632
|
});
|
|
548
633
|
// ─── Tool: create_wallet ─────────────────────────────────────────
|
|
549
634
|
server.tool('create_wallet', 'Create a new EVM or Solana wallet. Returns the wallet ID and address. ' +
|
|
550
|
-
'Private key is encrypted server-side and never exposed.', {
|
|
635
|
+
(anyLocalMode() ? 'In local mode this reports the key already configured; no wallet is created remotely.' : 'Private key is encrypted server-side and never exposed.'), {
|
|
551
636
|
label: z.string().default('').describe('Friendly name for the wallet'),
|
|
552
637
|
chain_id: z.number().int().default(8453).describe('Default chain ID (1=Ethereum, 8453=Base, 42161=Arbitrum, 10=Optimism, 137=Polygon, 43114=Avalanche, 56=BSC, 7777777=Zora, 369=PulseChain, 900=Solana, 901=Solana Devnet)'),
|
|
553
638
|
}, async ({ label, chain_id }) => {
|
|
@@ -585,13 +670,13 @@ server.tool('sign_transaction', 'Sign a transaction with a wallet\'s private key
|
|
|
585
670
|
wallet_id: z.number().int().describe('Wallet ID'),
|
|
586
671
|
to: AddressSchema.describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
|
|
587
672
|
chain_id: z.number().int().optional().describe('Chain ID (defaults to wallet\'s default)'),
|
|
588
|
-
value: z.string().default('0').describe('Value in wei/lamports (decimal string)'),
|
|
589
|
-
data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
|
|
673
|
+
value: z.string().regex(/^\d+$/, 'decimal integer string').default('0').describe('Value in wei/lamports (decimal integer string; no hex, no decimals)'),
|
|
674
|
+
data: z.string().regex(/^(0x([0-9a-fA-F]{2})*)?$/, '0x-prefixed even-length hex').default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
|
|
590
675
|
gas_limit: z.string().optional().describe('Gas limit — EVM only (auto-estimated if omitted)'),
|
|
591
676
|
max_fee: z.string().optional().describe('Max fee per gas in wei — EVM only (auto if omitted)'),
|
|
592
677
|
priority_fee: z.string().optional().describe('Max priority fee per gas in wei — EVM only (auto if omitted)'),
|
|
593
678
|
token_mint: z.string().optional().describe('SPL token mint address — Solana only (for SPL token transfers)'),
|
|
594
|
-
token_decimals: z.number().int().optional().describe('SPL token decimals
|
|
679
|
+
token_decimals: z.number().int().min(0).max(36).optional().describe('SPL token decimals, Solana only (6 for USDC)'),
|
|
595
680
|
}, async ({ wallet_id, to, chain_id, value, data, gas_limit, max_fee, priority_fee, token_mint, token_decimals }) => {
|
|
596
681
|
// Validate address format
|
|
597
682
|
if (!isValidAddress(to)) {
|
|
@@ -600,6 +685,10 @@ server.tool('sign_transaction', 'Sign a transaction with a wallet\'s private key
|
|
|
600
685
|
const body = { to, value };
|
|
601
686
|
if (chain_id)
|
|
602
687
|
body.chain_id = chain_id;
|
|
688
|
+
// Without a chain id the SPL fields were silently dropped and the server
|
|
689
|
+
// fell back to a native transfer of the same number, to the same address.
|
|
690
|
+
if (token_mint && !chain_id)
|
|
691
|
+
throw new Error('token_mint needs an explicit Solana chain_id (900 mainnet, 901 devnet).');
|
|
603
692
|
if (isSolanaChain(chain_id ?? 0)) {
|
|
604
693
|
// Solana-specific params
|
|
605
694
|
if (token_mint)
|
|
@@ -623,17 +712,17 @@ server.tool('sign_transaction', 'Sign a transaction with a wallet\'s private key
|
|
|
623
712
|
// ─── Tool: send_transaction ──────────────────────────────────────
|
|
624
713
|
server.tool('send_transaction', 'Sign and broadcast a transaction. ' +
|
|
625
714
|
'Returns the transaction hash (EVM) or signature (Solana) on success. ' +
|
|
626
|
-
'The transaction is signed server-side and broadcast via RPC.', {
|
|
715
|
+
(anyLocalMode() ? 'Signed in this process with the local key and broadcast via the configured RPC.' : 'The transaction is signed server-side and broadcast via RPC.'), {
|
|
627
716
|
wallet_id: z.number().int().describe('Wallet ID'),
|
|
628
717
|
to: AddressSchema.describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
|
|
629
718
|
chain_id: z.number().int().optional().describe('Chain ID (defaults to wallet\'s default)'),
|
|
630
|
-
value: z.string().default('0').describe('Value in wei/lamports (decimal string)'),
|
|
631
|
-
data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
|
|
719
|
+
value: z.string().regex(/^\d+$/, 'decimal integer string').default('0').describe('Value in wei/lamports (decimal integer string; no hex, no decimals)'),
|
|
720
|
+
data: z.string().regex(/^(0x([0-9a-fA-F]{2})*)?$/, '0x-prefixed even-length hex').default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
|
|
632
721
|
gas_limit: z.string().optional().describe('Gas limit — EVM only (auto-estimated if omitted)'),
|
|
633
722
|
max_fee: z.string().optional().describe('Max fee per gas in wei — EVM only (auto if omitted)'),
|
|
634
723
|
priority_fee: z.string().optional().describe('Max priority fee per gas in wei — EVM only (auto if omitted)'),
|
|
635
724
|
token_mint: z.string().optional().describe('SPL token mint address — Solana only (for SPL token transfers)'),
|
|
636
|
-
token_decimals: z.number().int().optional().describe('SPL token decimals
|
|
725
|
+
token_decimals: z.number().int().min(0).max(36).optional().describe('SPL token decimals, Solana only (6 for USDC)'),
|
|
637
726
|
}, async ({ wallet_id, to, chain_id, value, data, gas_limit, max_fee, priority_fee, token_mint, token_decimals }) => {
|
|
638
727
|
// Validate address format
|
|
639
728
|
if (!isValidAddress(to)) {
|
|
@@ -642,6 +731,10 @@ server.tool('send_transaction', 'Sign and broadcast a transaction. ' +
|
|
|
642
731
|
const body = { to, value };
|
|
643
732
|
if (chain_id)
|
|
644
733
|
body.chain_id = chain_id;
|
|
734
|
+
// Without a chain id the SPL fields were silently dropped and the server
|
|
735
|
+
// fell back to a native transfer of the same number, to the same address.
|
|
736
|
+
if (token_mint && !chain_id)
|
|
737
|
+
throw new Error('token_mint needs an explicit Solana chain_id (900 mainnet, 901 devnet).');
|
|
645
738
|
if (isSolanaChain(chain_id ?? 0)) {
|
|
646
739
|
// Solana-specific params
|
|
647
740
|
if (token_mint)
|
|
@@ -700,7 +793,7 @@ server.tool('get_token_balance', 'Get the ERC-20 or SPL token balance for a wall
|
|
|
700
793
|
wallet_id: z.number().int().describe('Wallet ID to check'),
|
|
701
794
|
token: z.string().describe('Token address (0x-prefixed ERC-20 contract for EVM, Base58 mint for Solana)'),
|
|
702
795
|
chain_id: z.number().int().describe('Chain ID to check on'),
|
|
703
|
-
decimals: z.number().int().
|
|
796
|
+
decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Taken from the response or the token contract when omitted'),
|
|
704
797
|
}, async ({ wallet_id, token, chain_id, decimals }) => {
|
|
705
798
|
// Validate token address format
|
|
706
799
|
if (!isValidAddress(token)) {
|
|
@@ -713,15 +806,28 @@ server.tool('get_token_balance', 'Get the ERC-20 or SPL token balance for a wall
|
|
|
713
806
|
return jsonResponse({
|
|
714
807
|
...data,
|
|
715
808
|
balance: data.balance_formatted,
|
|
716
|
-
decimals: data.decimals ?? decimals,
|
|
809
|
+
decimals: data.decimals ?? decimals ?? null,
|
|
717
810
|
});
|
|
718
811
|
}
|
|
719
|
-
//
|
|
720
|
-
|
|
812
|
+
// The response's own decimals (local mode reads them from the contract) win
|
|
813
|
+
// over the caller's guess; a guess of 18 used to reformat a 1 USDC balance
|
|
814
|
+
// as 0.000000000001. When nothing supplies them, resolve them ourselves.
|
|
815
|
+
let trusted = typeof data.decimals === 'number' ? data.decimals : null;
|
|
816
|
+
if (trusted === null) {
|
|
817
|
+
if (typeof decimals === 'number')
|
|
818
|
+
trusted = decimals;
|
|
819
|
+
else if (!isSolanaChain(chain_id))
|
|
820
|
+
trusted = await resolveTrustedDecimals(chain_id, token);
|
|
821
|
+
}
|
|
822
|
+
if (trusted === null)
|
|
823
|
+
throw new Error('Could not determine the token decimals; pass decimals explicitly.');
|
|
824
|
+
const warning = typeof decimals === 'number' && typeof data.decimals === 'number' && decimals !== data.decimals
|
|
825
|
+
? `decimals ${decimals} was passed but the token reports ${data.decimals}; the token's value was used.` : undefined;
|
|
721
826
|
return jsonResponse({
|
|
722
827
|
...data,
|
|
723
|
-
balance:
|
|
724
|
-
decimals,
|
|
828
|
+
balance: data.balance ?? formatUnits(data.balance_raw || '0', trusted),
|
|
829
|
+
decimals: trusted,
|
|
830
|
+
...(warning ? { warning } : {}),
|
|
725
831
|
});
|
|
726
832
|
});
|
|
727
833
|
// ─── Tool: transfer_token ───────────────────────────────────────
|
|
@@ -733,8 +839,8 @@ server.tool('transfer_token', 'Send ERC-20 tokens (EVM) or SPL tokens (Solana) t
|
|
|
733
839
|
to: AddressSchema.describe('Recipient address (0x-prefixed for EVM, Base58 for Solana)'),
|
|
734
840
|
amount: z.string().describe('Amount in human-readable format (e.g. "100" for 100 USDC)'),
|
|
735
841
|
chain_id: z.number().int().describe('Chain ID'),
|
|
736
|
-
decimals: z.number().int().
|
|
737
|
-
}, async ({ wallet_id, token, to, amount, chain_id, decimals }) => {
|
|
842
|
+
decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Resolved from the registry or the token contract when omitted; if given, it must match'),
|
|
843
|
+
}, async ({ wallet_id, token, to, amount, chain_id, decimals: givenDecimals }) => {
|
|
738
844
|
// Validate addresses
|
|
739
845
|
if (!isValidAddress(token)) {
|
|
740
846
|
throw new Error(`Invalid token address "${token}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
|
|
@@ -742,6 +848,7 @@ server.tool('transfer_token', 'Send ERC-20 tokens (EVM) or SPL tokens (Solana) t
|
|
|
742
848
|
if (!isValidAddress(to)) {
|
|
743
849
|
throw new Error(`Invalid recipient address "${to}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
|
|
744
850
|
}
|
|
851
|
+
const decimals = await verifiedDecimals(chain_id, token, givenDecimals);
|
|
745
852
|
const rawAmount = parseUnits(amount, decimals);
|
|
746
853
|
let result;
|
|
747
854
|
if (isSolanaChain(chain_id)) {
|
|
@@ -795,8 +902,8 @@ server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 toke
|
|
|
795
902
|
spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address to approve as spender'),
|
|
796
903
|
amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"), or "max" for unlimited'),
|
|
797
904
|
chain_id: z.number().int().describe('Chain ID'),
|
|
798
|
-
decimals: z.number().int().
|
|
799
|
-
}, async ({ wallet_id, token, spender, amount, chain_id, decimals }) => {
|
|
905
|
+
decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Resolved from the registry or the token contract when omitted; if given, it must match'),
|
|
906
|
+
}, async ({ wallet_id, token, spender, amount, chain_id, decimals: givenDecimals }) => {
|
|
800
907
|
if (isSolanaChain(chain_id)) {
|
|
801
908
|
throw new Error('approve_token is not supported on Solana. Solana SPL tokens do not use ERC-20 style approvals.');
|
|
802
909
|
}
|
|
@@ -806,13 +913,13 @@ server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 toke
|
|
|
806
913
|
rawAmount = (BigInt(2) ** BigInt(256) - BigInt(1)).toString();
|
|
807
914
|
}
|
|
808
915
|
else {
|
|
809
|
-
rawAmount = parseUnits(amount,
|
|
916
|
+
rawAmount = parseUnits(amount, await verifiedDecimals(chain_id, token, givenDecimals));
|
|
810
917
|
}
|
|
811
918
|
const calldata = '0x095ea7b3' + padAddress(spender) + encodeUint256(rawAmount);
|
|
812
919
|
// Asset risk rides along with the approval (issue #13). It never blocks; it is the
|
|
813
920
|
// caller's call. Skipped for Permit2, which only moves what a signature allows.
|
|
814
921
|
let risk = null;
|
|
815
|
-
if (spender.toLowerCase() !== PERMIT2_ADDRESS.toLowerCase()) {
|
|
922
|
+
if (spender.toLowerCase() !== PERMIT2_ADDRESS.toLowerCase() && process.env.AGENTWALLET_TOKEN_RISK !== '0') {
|
|
816
923
|
try {
|
|
817
924
|
risk = await assessTokenRisk(chain_id, token, (to, data) => ethCallHex(chain_id, to, data));
|
|
818
925
|
}
|
|
@@ -842,8 +949,8 @@ server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved
|
|
|
842
949
|
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
843
950
|
spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Spender contract address to check'),
|
|
844
951
|
chain_id: z.number().int().describe('Chain ID'),
|
|
845
|
-
decimals: z.number().int().
|
|
846
|
-
}, async ({ wallet_id, token, spender, chain_id, decimals }) => {
|
|
952
|
+
decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Resolved from the registry or the token contract when omitted; if given, it must match'),
|
|
953
|
+
}, async ({ wallet_id, token, spender, chain_id, decimals: givenDecimals }) => {
|
|
847
954
|
if (isSolanaChain(chain_id)) {
|
|
848
955
|
throw new Error('get_allowance is not supported on Solana. Solana SPL tokens do not use ERC-20 style allowances.');
|
|
849
956
|
}
|
|
@@ -852,9 +959,13 @@ server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved
|
|
|
852
959
|
// allowance(address owner, address spender) — selector: 0xdd62ed3e
|
|
853
960
|
const calldata = '0xdd62ed3e' + padAddress(wallet.address) + padAddress(spender);
|
|
854
961
|
const result = await api('/eth-call', 'POST', { chain_id, to: token, data: calldata });
|
|
855
|
-
// Parse uint256
|
|
856
|
-
|
|
962
|
+
// Parse exactly one uint256 word: a hostile token returning more bytes must
|
|
963
|
+
// not turn into a 512-bit "allowance".
|
|
964
|
+
const rawHex = String(result?.result || '').replace(/^0x/, '').slice(0, 64);
|
|
965
|
+
if (rawHex && !/^[0-9a-fA-F]+$/.test(rawHex))
|
|
966
|
+
throw new Error('allowance() returned non-hex data.');
|
|
857
967
|
const raw = BigInt('0x' + (rawHex || '0')).toString();
|
|
968
|
+
const decimals = await verifiedDecimals(chain_id, token, givenDecimals);
|
|
858
969
|
const maxUint256 = (BigInt(2) ** BigInt(256) - BigInt(1)).toString();
|
|
859
970
|
return jsonResponse({
|
|
860
971
|
token,
|
|
@@ -866,17 +977,6 @@ server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved
|
|
|
866
977
|
});
|
|
867
978
|
});
|
|
868
979
|
// ─── WETH / Wrapped Native Token addresses ──────────────────────
|
|
869
|
-
const WRAPPED_NATIVE = {
|
|
870
|
-
1: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH' },
|
|
871
|
-
8453: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
872
|
-
42161: { address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', symbol: 'WETH' },
|
|
873
|
-
10: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
874
|
-
137: { address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', symbol: 'WPOL' },
|
|
875
|
-
56: { address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', symbol: 'WBNB' },
|
|
876
|
-
43114: { address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', symbol: 'WAVAX' },
|
|
877
|
-
7777777: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
878
|
-
369: { address: '0xA1077a294dDE1B09bB078844df40758a5D0f9a27', symbol: 'WPLS' },
|
|
879
|
-
};
|
|
880
980
|
// ─── Tool: wrap_eth ─────────────────────────────────────────────
|
|
881
981
|
server.tool('wrap_eth', 'Wrap native tokens (ETH, AVAX, BNB, POL, PLS) into their wrapped ERC-20 version (WETH, WAVAX, etc.). ' +
|
|
882
982
|
'Required for most DeFi protocols that use ERC-20 tokens instead of raw native tokens. ' +
|
|
@@ -959,7 +1059,9 @@ function decodeAbiString(hex) {
|
|
|
959
1059
|
return '';
|
|
960
1060
|
// Convert hex to UTF-8
|
|
961
1061
|
const bytes = new Uint8Array(pairs.map(b => parseInt(b, 16)));
|
|
962
|
-
|
|
1062
|
+
// Attacker-controlled text: keep it short and printable so a token cannot
|
|
1063
|
+
// smuggle kilobytes of instructions or terminal escapes into the agent's context.
|
|
1064
|
+
return new TextDecoder().decode(bytes).replace(/[\u0000-\u001f\u007f-\u009f]/g, '').slice(0, 64);
|
|
963
1065
|
}
|
|
964
1066
|
catch {
|
|
965
1067
|
return '';
|
|
@@ -981,8 +1083,9 @@ server.tool('get_token_info', 'Get the name, symbol, and decimals of any ERC-20
|
|
|
981
1083
|
]);
|
|
982
1084
|
const name = decodeAbiString(nameResult.result);
|
|
983
1085
|
const symbol = decodeAbiString(symbolResult.result);
|
|
984
|
-
const decimalsHex = decimalsResult
|
|
985
|
-
const
|
|
1086
|
+
const decimalsHex = String(decimalsResult?.result || '').replace(/^0x/, '').slice(0, 64);
|
|
1087
|
+
const parsedDecimals = /^[0-9a-fA-F]+$/.test(decimalsHex) ? Number(BigInt('0x' + decimalsHex)) : NaN;
|
|
1088
|
+
const decimals = Number.isInteger(parsedDecimals) && parsedDecimals >= 0 && parsedDecimals <= 255 ? parsedDecimals : null;
|
|
986
1089
|
return jsonResponse({
|
|
987
1090
|
token,
|
|
988
1091
|
chain_id,
|
|
@@ -1111,16 +1214,32 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1111
1214
|
request_approval: z.boolean().optional().describe('Hosted wallets only. When the payment exceeds the cap, email the wallet owner an approve/deny link ' +
|
|
1112
1215
|
'instead of refusing, and return an approval_id to retry with. Default true unless AGENTWALLET_APPROVALS=0.'),
|
|
1113
1216
|
approval_id: z.string().optional().describe('An approval id from an earlier over-cap attempt. Once the owner has approved it, pass it here to make that one payment.'),
|
|
1114
|
-
|
|
1217
|
+
fresh_authorization: z.boolean().optional().describe('Sign a new authorization even if one for the same endpoint, amount and recipient is still valid. By default a repeat call ' +
|
|
1218
|
+
'within the validity window re-sends the earlier signature (same nonce, so it cannot settle twice) instead of paying again.'),
|
|
1219
|
+
}, async ({ url, wallet_id, method, headers: headersJson, body: reqBody, max_payment, prefer_chain, request_approval, approval_id, fresh_authorization }) => {
|
|
1115
1220
|
// Build request headers
|
|
1116
1221
|
const reqHeaders = { Accept: 'application/json' };
|
|
1117
1222
|
if (headersJson) {
|
|
1223
|
+
let parsed;
|
|
1118
1224
|
try {
|
|
1119
|
-
|
|
1225
|
+
parsed = JSON.parse(headersJson);
|
|
1120
1226
|
}
|
|
1121
1227
|
catch {
|
|
1228
|
+
parsed = null;
|
|
1229
|
+
}
|
|
1230
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1122
1231
|
throw new Error('Invalid headers JSON. Must be a JSON object (e.g. {"Authorization": "Bearer ..."}).');
|
|
1123
1232
|
}
|
|
1233
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1234
|
+
const name = k.toLowerCase();
|
|
1235
|
+
// Host would replace the TLS server name; the rest are hop-by-hop or framing headers the client owns.
|
|
1236
|
+
if (FORBIDDEN_REQUEST_HEADERS.has(name) || name.startsWith('proxy-') || name.startsWith(':')) {
|
|
1237
|
+
throw new Error(`Header "${k}" cannot be set by the caller.`);
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof v !== 'string')
|
|
1240
|
+
throw new Error(`Header "${k}" must be a string.`);
|
|
1241
|
+
reqHeaders[k] = v;
|
|
1242
|
+
}
|
|
1124
1243
|
}
|
|
1125
1244
|
// Validate URL: canonicalizes IP literals (including IPv4-mapped IPv6) and
|
|
1126
1245
|
// resolves DNS names, rejecting any private/internal destination. An early
|
|
@@ -1157,6 +1276,15 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1157
1276
|
});
|
|
1158
1277
|
}
|
|
1159
1278
|
// Step 2: Requirements come from the PAYMENT-REQUIRED header (v2) or the JSON body (v1).
|
|
1279
|
+
// A 402 that arrived through a cross-origin redirect cannot be paid here: the
|
|
1280
|
+
// payment header would be stripped on the same hop and the money would go
|
|
1281
|
+
// to whoever the redirecting server named. The agent should call the final URL.
|
|
1282
|
+
const servedFrom = initialRes.headers.get(FINAL_URL_HEADER) || url;
|
|
1283
|
+
if (new URL(servedFrom).origin !== new URL(url).origin) {
|
|
1284
|
+
return jsonResponse({ status: 402, payment_required: true, payment_made: false,
|
|
1285
|
+
error: `The endpoint redirected to another origin (${new URL(servedFrom).origin}) before asking for payment. Call pay_x402 with that URL directly if you trust it.`,
|
|
1286
|
+
final_url: servedFrom });
|
|
1287
|
+
}
|
|
1160
1288
|
const initialText = await initialRes.text();
|
|
1161
1289
|
let initialBody = null;
|
|
1162
1290
|
try {
|
|
@@ -1200,20 +1328,33 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1200
1328
|
if (typeof option.requiredDecimals === 'number')
|
|
1201
1329
|
assertDeclaredDecimals(option.requiredDecimals, trustedDecimals);
|
|
1202
1330
|
const amount = formatUnits(rawAmount, trustedDecimals);
|
|
1203
|
-
const tokenLabel =
|
|
1331
|
+
const tokenLabel = assetLabel(chainId, tokenAddress); // never the 402's own label
|
|
1204
1332
|
const skip = { 'X-AGW-SKIP-X402': 'true' };
|
|
1333
|
+
assertPayTo(chainId, option.payTo);
|
|
1334
|
+
// Step 3b: Can this asset be priced at all? The cap is in dollars; native
|
|
1335
|
+
// ETH or an arbitrary token is refused unless the operator allowlisted it.
|
|
1336
|
+
const assetPolicy = autopayAssetAllowed(chainId, tokenAddress);
|
|
1337
|
+
if (!assetPolicy.allowed) {
|
|
1338
|
+
return jsonResponse({
|
|
1339
|
+
status: 402, payment_required: true, payment_made: false,
|
|
1340
|
+
required_amount: amount, token: tokenLabel, token_address: tokenAddress || null, network: option.network, chain_id: chainId, pay_to: option.payTo,
|
|
1341
|
+
error: assetPolicy.reason,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1205
1344
|
// Step 4: Hard per-payment cap, ALWAYS applied. If the caller omits max_payment
|
|
1206
1345
|
// we fall back to AGENTWALLET_MAX_AUTOPAY (default "1"). Above the cap a hosted
|
|
1207
1346
|
// wallet can ask its owner instead of refusing: an approval is created, the
|
|
1208
1347
|
// owner gets approve/deny links by email, and the agent retries with the id.
|
|
1209
1348
|
// For upto the cap applies to the MAXIMUM the seller may settle.
|
|
1210
|
-
|
|
1211
|
-
|
|
1349
|
+
// max_payment can only lower the operator's ceiling; an agent-chosen number
|
|
1350
|
+
// above it is ignored and reported, because the agent is what the cap bounds.
|
|
1351
|
+
const { cap: effectiveMax, source: capSource, clamped } = effectiveAutopayCap(max_payment);
|
|
1212
1352
|
let approvalUsed = null;
|
|
1213
1353
|
if (!isWithinCap(rawAmount, trustedDecimals, effectiveMax)) {
|
|
1214
1354
|
const overCap = {
|
|
1215
1355
|
status: 402, payment_required: true, payment_made: false,
|
|
1216
1356
|
required_amount: amount, max_allowed: effectiveMax, cap_source: capSource, token: tokenLabel,
|
|
1357
|
+
...(clamped ? { max_payment_ignored: `max_payment ${max_payment} exceeds the operator ceiling AGENTWALLET_MAX_AUTOPAY=${effectiveMax}; only the operator can raise it.` } : {}),
|
|
1217
1358
|
network: option.network, chain_id: chainId, pay_to: option.payTo, description: option.description,
|
|
1218
1359
|
};
|
|
1219
1360
|
const wantApproval = request_approval ?? (process.env.AGENTWALLET_APPROVALS !== '0');
|
|
@@ -1222,8 +1363,9 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1222
1363
|
if (a?.status !== 'approved' || Number(a.used) === 1) {
|
|
1223
1364
|
return jsonResponse({ ...overCap, approval_id, approval_status: a?.status ?? null, error: `Approval ${approval_id} is ${a?.status ?? 'unknown'}, not approved.${a?.error ? ' ' + a.error : ''}` });
|
|
1224
1365
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1366
|
+
const sameAsset = String(a.asset ?? '').toLowerCase() === tokenAddress.toLowerCase();
|
|
1367
|
+
if (!sameAsset || String(a.pay_to || '').toLowerCase() !== option.payTo.toLowerCase() || Number(a.chain_id) !== chainId || BigInt(String(a.value || '0')) < BigInt(rawAmount)) {
|
|
1368
|
+
return jsonResponse({ ...overCap, approval_id, error: 'Approval does not cover this payment (asset, recipient, chain or amount differ).' });
|
|
1227
1369
|
}
|
|
1228
1370
|
approvalUsed = String(approval_id);
|
|
1229
1371
|
}
|
|
@@ -1265,12 +1407,25 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1265
1407
|
let payer = null;
|
|
1266
1408
|
let permit2ApprovalTx = null;
|
|
1267
1409
|
let payerDelegationInfo = null;
|
|
1268
|
-
|
|
1410
|
+
let authorizationReused = false;
|
|
1411
|
+
const cacheKey = [wallet_id, chainId, tokenAddress.toLowerCase(), option.payTo.toLowerCase(), rawAmount, option.scheme, new URL(url).origin + new URL(url).pathname].join('|');
|
|
1412
|
+
const cachedAuth = fresh_authorization ? undefined : signedPayments.get(cacheKey);
|
|
1413
|
+
for (const [k, v] of signedPayments)
|
|
1414
|
+
if (v.until <= Date.now())
|
|
1415
|
+
signedPayments.delete(k);
|
|
1416
|
+
if (cachedAuth && cachedAuth.until > Date.now()) {
|
|
1417
|
+
// Same endpoint, same requirement, still inside the window: re-send the
|
|
1418
|
+
// earlier signature. Its nonce is single-use, so a server that already
|
|
1419
|
+
// settled it cannot settle it again, and one that never did now can.
|
|
1420
|
+
({ headerName, paymentHeader, txHash, authorization, uptoAuth, payer } = cachedAuth);
|
|
1421
|
+
authorizationReused = true;
|
|
1422
|
+
}
|
|
1423
|
+
else if (isUpto) {
|
|
1269
1424
|
payer = await payerAddress(wallet_id);
|
|
1270
1425
|
payerDelegationInfo = await payerDelegation(chainId, payer);
|
|
1271
1426
|
const allowance = decodeUint(await ethCallHex(chainId, tokenAddress, permit2AllowanceCalldata(payer)));
|
|
1272
1427
|
if (allowance < BigInt(rawAmount)) {
|
|
1273
|
-
if (process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1') {
|
|
1428
|
+
if (process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1' && assetPolicy.via === 'stablecoin') {
|
|
1274
1429
|
const r = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: tokenAddress, value: '0', data: permit2ApproveCalldata(), chain_id: chainId }, skip));
|
|
1275
1430
|
permit2ApprovalTx = String(r?.tx_hash || '');
|
|
1276
1431
|
await new Promise(res => setTimeout(res, 4000));
|
|
@@ -1322,6 +1477,12 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1322
1477
|
throw new Error('x402: the payment transaction returned no hash.');
|
|
1323
1478
|
paymentHeader = Buffer.from(JSON.stringify({ x402Version, scheme: option.scheme, network: option.network, payload: { txHash } })).toString('base64');
|
|
1324
1479
|
}
|
|
1480
|
+
if (!authorizationReused) {
|
|
1481
|
+
const until = authorization ? Number(authorization.validBefore) * 1000
|
|
1482
|
+
: uptoAuth ? Number(uptoAuth.deadline) * 1000
|
|
1483
|
+
: Date.now() + maxAuthWindowSeconds() * 1000; // a broadcast transfer: the hash stays valid, re-sending it never pays twice
|
|
1484
|
+
signedPayments.set(cacheKey, { headerName, paymentHeader, txHash, authorization, uptoAuth, payer, until });
|
|
1485
|
+
}
|
|
1325
1486
|
// Step 6: Retry with the payment header and read the settlement receipt.
|
|
1326
1487
|
const retryHeaders = { ...reqHeaders, [headerName]: paymentHeader };
|
|
1327
1488
|
const retryOptions = {
|
|
@@ -1345,6 +1506,11 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1345
1506
|
// A second 402 means the facilitator declined (unfunded payer, expired window, bad domain...). v2 servers
|
|
1346
1507
|
// put the reason in a fresh PAYMENT-REQUIRED header, v1 servers in the body; surface it instead of {}.
|
|
1347
1508
|
let retryError = null;
|
|
1509
|
+
const retryServedFrom = retryRes.headers.get(FINAL_URL_HEADER) || url;
|
|
1510
|
+
const retryCrossOrigin = new URL(retryServedFrom).origin !== new URL(url).origin;
|
|
1511
|
+
if (retryCrossOrigin) {
|
|
1512
|
+
retryError = `The paid request was redirected to another origin (${new URL(retryServedFrom).origin}); the payment header is never forwarded across origins, so this response did not see the payment.`;
|
|
1513
|
+
}
|
|
1348
1514
|
if (retryRes.status === 402) {
|
|
1349
1515
|
const again = parsePaymentRequired(n => retryRes.headers.get(n), retryParsed);
|
|
1350
1516
|
retryError = String(again?.error || retryParsed?.error || 'Payment was not accepted (no reason given).');
|
|
@@ -1357,7 +1523,8 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1357
1523
|
// Paid means the endpoint accepted the payment: a settlement receipt, or a success status after the payment header.
|
|
1358
1524
|
// A 4xx other than 402 (bad body, auth) means the request failed for another reason; the authorization was
|
|
1359
1525
|
// sent but the facilitator normally does not settle a failed request, so it is not reported as paid.
|
|
1360
|
-
payment_made: Boolean(settlement?.success) || (retryRes.status >= 200 && retryRes.status < 300),
|
|
1526
|
+
payment_made: !retryCrossOrigin && (Boolean(settlement?.success) || (retryRes.status >= 200 && retryRes.status < 300)),
|
|
1527
|
+
authorization_reused: authorizationReused,
|
|
1361
1528
|
retry_error: retryError,
|
|
1362
1529
|
payment_method: isUpto ? 'permit2-upto-authorization' : (standardExact ? 'eip3009-authorization' : 'onchain-transfer'),
|
|
1363
1530
|
approval_id: approvalUsed,
|
|
@@ -1371,8 +1538,10 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
1371
1538
|
pay_to: option.payTo,
|
|
1372
1539
|
payer,
|
|
1373
1540
|
payer_delegation: payerDelegationInfo,
|
|
1374
|
-
|
|
1375
|
-
|
|
1541
|
+
// txHash is ours (we broadcast it); settlement.transaction is the server's
|
|
1542
|
+
// claim and is surfaced only when it is shaped like a real hash.
|
|
1543
|
+
tx_hash: txHash ?? (looksLikeTxHash(settlement?.transaction) ? settlement?.transaction : null) ?? null,
|
|
1544
|
+
settlement_reported_by_server: settlement,
|
|
1376
1545
|
authorization: authorization ? { nonce: authorization.nonce, valid_before: authorization.validBefore }
|
|
1377
1546
|
: (uptoAuth ? { scheme: 'upto', max_amount: amount, nonce: uptoAuth.nonce, deadline: uptoAuth.deadline, facilitator: uptoAuth.witness.facilitator } : null),
|
|
1378
1547
|
description: option.description,
|
|
@@ -1384,16 +1553,23 @@ server.tool('check_approval', 'Status of a payment approval created when pay_x40
|
|
|
1384
1553
|
'Returns pending, approved, denied or expired. Once approved, call pay_x402 again with approval_id.', { approval_id: z.string().describe('Approval id returned by pay_x402') }, async ({ approval_id }) => jsonResponse(await api(`/approvals/${encodeURIComponent(approval_id)}`, 'GET', undefined, { 'X-AGW-SKIP-X402': 'true' })));
|
|
1385
1554
|
// ─── Tool: approve_permit2 ──────────────────────────────────────
|
|
1386
1555
|
server.tool('approve_permit2', 'One-time ERC-20 approval of a token to the Permit2 contract, needed before paying x402 "upto" endpoints with that token. ' +
|
|
1387
|
-
'A normal on-chain transaction (needs gas).
|
|
1388
|
-
'
|
|
1556
|
+
'A normal on-chain transaction (needs gas). By default approves the maximum, the ecosystem norm, so it never has to be repeated; ' +
|
|
1557
|
+
'pass amount to grant a bounded allowance instead (required under AGENTWALLET_MAX_TX_TOKEN, which refuses a max approval). ' +
|
|
1558
|
+
'Permit2 itself only moves what each signed authorization allows.', {
|
|
1389
1559
|
wallet_id: z.number().int().describe('Wallet ID'),
|
|
1390
1560
|
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token address (e.g. USDC on Base)'),
|
|
1391
1561
|
chain_id: z.number().int().describe('Chain ID'),
|
|
1392
|
-
|
|
1562
|
+
amount: z.string().optional().describe('Allowance to grant in human units (e.g. "100"). Omit for unlimited.'),
|
|
1563
|
+
}, async ({ wallet_id, token, chain_id, amount }) => {
|
|
1393
1564
|
if (isSolanaChain(chain_id))
|
|
1394
1565
|
throw new Error('Permit2 is an EVM contract; there is nothing to approve on Solana.');
|
|
1395
|
-
|
|
1396
|
-
|
|
1566
|
+
let data = permit2ApproveCalldata();
|
|
1567
|
+
if (amount !== undefined) {
|
|
1568
|
+
const raw = parseUnits(amount, await resolveTrustedDecimals(chain_id, token));
|
|
1569
|
+
data = '0x095ea7b3' + padAddress(PERMIT2_ADDRESS) + encodeUint256(raw);
|
|
1570
|
+
}
|
|
1571
|
+
const result = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: token, value: '0', data, chain_id }));
|
|
1572
|
+
return jsonResponse({ ...result, token, spender: PERMIT2_ADDRESS, amount: amount ?? 'unlimited', note: 'Permit2 only transfers what a signed authorization allows; the allowance itself moves nothing.' });
|
|
1397
1573
|
});
|
|
1398
1574
|
// ─── Tool: check_token_risk ─────────────────────────────────────
|
|
1399
1575
|
server.tool('check_token_risk', 'Assess an ERC-20 token before approving or swapping it: honeypot, taxes, owner powers, verified source, ' +
|
|
@@ -1457,13 +1633,16 @@ server.tool('create_paywall', 'Create an x402 paywall that charges agents/client
|
|
|
1457
1633
|
amount: z.string().describe('Price in human-readable format (e.g. "0.01" for 0.01 USDC)'),
|
|
1458
1634
|
token_type: z.enum(['erc20', 'spl', 'native']).default('erc20').describe('"erc20" for EVM stablecoins, "spl" for Solana SPL tokens, "native" for ETH/SOL/POL/etc.'),
|
|
1459
1635
|
token_address: z.string().default('').describe('Token contract address (ERC-20 for EVM, SPL mint Base58 for Solana). Required if token_type is "erc20" or "spl". Use get_chains to find stablecoin addresses.'),
|
|
1460
|
-
token_decimals: z.number().int().
|
|
1636
|
+
token_decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Defaults to the chain native decimals for "native", else 6 (USDC); set it for any other token'),
|
|
1461
1637
|
token_name: z.string().default('USDC').describe('Token display name (e.g. "USDC", "ETH")'),
|
|
1462
1638
|
chain_id: z.number().int().default(8453).describe('Chain ID for payments (8453=Base, 1=Ethereum, etc.)'),
|
|
1463
1639
|
resource_url: z.string().url().describe('URL of the protected resource to serve after payment verification'),
|
|
1464
1640
|
resource_mime: z.string().default('application/json').describe('MIME type of the resource (e.g. "application/json", "text/plain")'),
|
|
1465
1641
|
}, async ({ wallet_id, name, description, amount, token_type, token_address, token_decimals, token_name, chain_id, resource_url, resource_mime }) => {
|
|
1466
|
-
// Convert human-readable amount to raw token units
|
|
1642
|
+
// Convert human-readable amount to raw token units. A native paywall used
|
|
1643
|
+
// to default to 6 decimals and price 0.01 ETH as 10000 wei.
|
|
1644
|
+
if (token_decimals === undefined)
|
|
1645
|
+
token_decimals = token_type === 'native' ? nativeDecimals(chain_id) : (lookupTrustedDecimals(chain_id, token_address) ?? 6);
|
|
1467
1646
|
const rawAmount = parseUnits(amount, token_decimals);
|
|
1468
1647
|
const data = await api('/x402/paywalls', 'POST', {
|
|
1469
1648
|
wallet_id,
|
|
@@ -1488,7 +1667,7 @@ server.tool('create_paywall', 'Create an x402 paywall that charges agents/client
|
|
|
1488
1667
|
server.tool('list_paywalls', 'List all your x402 paywalls. Returns paywall IDs, names, pricing, ' +
|
|
1489
1668
|
'access URLs, payment counts, and revenue totals.', {
|
|
1490
1669
|
page: z.number().int().default(1).describe('Page number'),
|
|
1491
|
-
per_page: z.number().int().default(50).describe('Results per page (max 100)'),
|
|
1670
|
+
per_page: z.number().int().min(1).max(100).default(50).describe('Results per page (max 100)'),
|
|
1492
1671
|
}, async ({ page, per_page }) => {
|
|
1493
1672
|
const data = await api(`/x402/paywalls?page=${page}&per_page=${per_page}`);
|
|
1494
1673
|
return jsonResponse(data);
|
|
@@ -1508,7 +1687,7 @@ server.tool('update_paywall', 'Update an x402 paywall configuration. ' +
|
|
|
1508
1687
|
name: z.string().optional().describe('New paywall name'),
|
|
1509
1688
|
description: z.string().optional().describe('New description'),
|
|
1510
1689
|
amount: z.string().optional().describe('New price in human-readable format (e.g. "0.05")'),
|
|
1511
|
-
token_decimals: z.number().int().optional().describe('Token decimals
|
|
1690
|
+
token_decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Read from the paywall itself when omitted'),
|
|
1512
1691
|
resource_url: z.string().url().optional().describe('New resource URL'),
|
|
1513
1692
|
resource_mime: z.string().optional().describe('New MIME type'),
|
|
1514
1693
|
is_active: z.boolean().optional().describe('Enable (true) or disable (false) the paywall'),
|
|
@@ -1526,7 +1705,14 @@ server.tool('update_paywall', 'Update an x402 paywall configuration. ' +
|
|
|
1526
1705
|
body.is_active = is_active;
|
|
1527
1706
|
// Convert human-readable amount to raw if provided
|
|
1528
1707
|
if (amount !== undefined) {
|
|
1529
|
-
|
|
1708
|
+
let decimals = token_decimals;
|
|
1709
|
+
if (decimals === undefined) {
|
|
1710
|
+
const existing = (await api(`/x402/paywalls/${paywall_id}`));
|
|
1711
|
+
const d = Number(existing?.token_decimals);
|
|
1712
|
+
if (!Number.isInteger(d) || d < 0 || d > 36)
|
|
1713
|
+
throw new Error('Could not read the paywall decimals; pass token_decimals with the new amount.');
|
|
1714
|
+
decimals = d;
|
|
1715
|
+
}
|
|
1530
1716
|
body.amount = parseUnits(amount, decimals);
|
|
1531
1717
|
}
|
|
1532
1718
|
const data = await api(`/x402/paywalls/${paywall_id}`, 'PUT', body);
|
|
@@ -1544,7 +1730,7 @@ server.tool('get_paywall_payments', 'Get payment history for a specific x402 pay
|
|
|
1544
1730
|
'Returns verified payments with TX hashes, payer addresses, amounts, and timestamps.', {
|
|
1545
1731
|
paywall_id: z.number().int().describe('Paywall ID'),
|
|
1546
1732
|
page: z.number().int().default(1).describe('Page number'),
|
|
1547
|
-
per_page: z.number().int().default(20).describe('Results per page (max 100)'),
|
|
1733
|
+
per_page: z.number().int().min(1).max(100).default(20).describe('Results per page (max 100)'),
|
|
1548
1734
|
}, async ({ paywall_id, page, per_page }) => {
|
|
1549
1735
|
const data = await api(`/x402/paywalls/${paywall_id}/payments?page=${page}&per_page=${per_page}`);
|
|
1550
1736
|
return jsonResponse(data);
|
|
@@ -1573,6 +1759,10 @@ server.tool('wallet_mode', 'Report whether this server is signing locally (self-
|
|
|
1573
1759
|
custody: 'self',
|
|
1574
1760
|
signing: 'Signed in this process. Keys are never sent to AgentWallet or anyone else.',
|
|
1575
1761
|
max_autopay: process.env.AGENTWALLET_MAX_AUTOPAY || '1',
|
|
1762
|
+
autopay_assets: process.env.AGENTWALLET_AUTOPAY_ASSETS || 'registry stablecoins only',
|
|
1763
|
+
x402_max_timeout_seconds: maxAuthWindowSeconds(),
|
|
1764
|
+
per_tx_cap_token: process.env.AGENTWALLET_MAX_TX_TOKEN || 'not set (ERC-20 and SPL transfers are uncapped)',
|
|
1765
|
+
allow_unknown_token_calls: process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1',
|
|
1576
1766
|
};
|
|
1577
1767
|
if (isLocalMode()) {
|
|
1578
1768
|
let rpc = 'default public endpoint';
|
|
@@ -1582,7 +1772,7 @@ server.tool('wallet_mode', 'Report whether this server is signing locally (self-
|
|
|
1582
1772
|
catch { /* chain has no default; not worth failing the report over */ }
|
|
1583
1773
|
report.evm = {
|
|
1584
1774
|
address: getLocalAddress(),
|
|
1585
|
-
rpc_endpoint: rpc,
|
|
1775
|
+
rpc_endpoint: redactUrl(rpc),
|
|
1586
1776
|
per_tx_cap_native: process.env.AGENTWALLET_MAX_TX_NATIVE || 'not set',
|
|
1587
1777
|
};
|
|
1588
1778
|
}
|
|
@@ -1597,8 +1787,9 @@ server.tool('wallet_mode', 'Report whether this server is signing locally (self-
|
|
|
1597
1787
|
catch { /* fall through to the default label */ }
|
|
1598
1788
|
report.solana = {
|
|
1599
1789
|
address: getSolanaAddress(),
|
|
1600
|
-
rpc_endpoint: rpc,
|
|
1790
|
+
rpc_endpoint: redactUrl(rpc),
|
|
1601
1791
|
per_tx_cap_sol: process.env.AGENTWALLET_MAX_TX_SOL || 'not set',
|
|
1792
|
+
per_tx_cap_token: process.env.AGENTWALLET_MAX_TX_TOKEN || 'not set (SPL transfers are uncapped; AGENTWALLET_MAX_TX_SOL does not cover them)',
|
|
1602
1793
|
};
|
|
1603
1794
|
}
|
|
1604
1795
|
else {
|
|
@@ -1635,6 +1826,7 @@ server.tool('export_wallet_key', 'Explain how to export the private key of a hos
|
|
|
1635
1826
|
});
|
|
1636
1827
|
// ─── Start ──────────────────────────────────────────────────────
|
|
1637
1828
|
async function main() {
|
|
1829
|
+
validateGuardEnv();
|
|
1638
1830
|
const transport = new StdioServerTransport();
|
|
1639
1831
|
await server.connect(transport);
|
|
1640
1832
|
/* stderr, so it never corrupts the stdio JSON-RPC stream. Announcing custody
|
|
@@ -1647,6 +1839,8 @@ async function main() {
|
|
|
1647
1839
|
if (isSolanaLocalMode())
|
|
1648
1840
|
parts.push(`Solana ${getSolanaAddress()}`);
|
|
1649
1841
|
console.error(`AgentWallet MCP: LOCAL signing mode. ${parts.join(', ')}. Keys never leave this machine.`);
|
|
1842
|
+
if (!(process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim())
|
|
1843
|
+
console.error('AgentWallet MCP: warning, AGENTWALLET_MAX_TX_TOKEN is not set; ERC-20 and SPL transfers have no per-transaction ceiling.');
|
|
1650
1844
|
}
|
|
1651
1845
|
catch (e) {
|
|
1652
1846
|
console.error(`AgentWallet MCP: local signing configured but a key could not be loaded: ${e.message}`);
|