agentwallet-mcp 1.10.2 → 1.10.3

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/build/index.js CHANGED
@@ -410,6 +410,18 @@ function resolveChainId(network) {
410
410
  return null;
411
411
  }
412
412
  // ─── Server ──────────────────────────────────────────────────────
413
+ /**
414
+ * Destination address shape, EVM or Solana.
415
+ *
416
+ * The contract-address params were regex-guarded but the DESTINATION on every
417
+ * transfer tool was a bare z.string(), so a truncated or mistyped address went
418
+ * straight into a signed transaction. On-chain sends are irreversible, and the
419
+ * chain will happily accept any well-formed address, so the cheapest place to
420
+ * catch a malformed one is before it is signed. This does not validate the
421
+ * EIP-55 checksum, only the shape; a wrong-but-well-formed address is still the
422
+ * caller's responsibility.
423
+ */
424
+ 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');
413
425
  const server = new McpServer({
414
426
  name: 'agentwallet',
415
427
  version: '1.10.1',
@@ -528,7 +540,7 @@ server.tool('sign_transaction', 'Sign a transaction with a wallet\'s private key
528
540
  'For EVM: returns signed raw transaction hex. For Solana: returns base64 signed transaction. ' +
529
541
  'Does NOT broadcast — use send_transaction for sign + broadcast.', {
530
542
  wallet_id: z.number().int().describe('Wallet ID'),
531
- to: z.string().describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
543
+ to: AddressSchema.describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
532
544
  chain_id: z.number().int().optional().describe('Chain ID (defaults to wallet\'s default)'),
533
545
  value: z.string().default('0').describe('Value in wei/lamports (decimal string)'),
534
546
  data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
@@ -570,7 +582,7 @@ server.tool('send_transaction', 'Sign and broadcast a transaction. ' +
570
582
  'Returns the transaction hash (EVM) or signature (Solana) on success. ' +
571
583
  'The transaction is signed server-side and broadcast via RPC.', {
572
584
  wallet_id: z.number().int().describe('Wallet ID'),
573
- to: z.string().describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
585
+ to: AddressSchema.describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
574
586
  chain_id: z.number().int().optional().describe('Chain ID (defaults to wallet\'s default)'),
575
587
  value: z.string().default('0').describe('Value in wei/lamports (decimal string)'),
576
588
  data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
@@ -612,7 +624,7 @@ server.tool('transfer', 'Send native tokens (ETH, AVAX, BNB, POL, PLS, SOL) to a
612
624
  'Specify the amount in human-readable format (e.g. "0.1" for 0.1 ETH). ' +
613
625
  'The amount is converted to wei/lamports automatically. Signs and broadcasts the transaction.', {
614
626
  wallet_id: z.number().int().describe('Wallet ID to send from'),
615
- to: z.string().describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
627
+ to: AddressSchema.describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
616
628
  amount: z.string().describe('Amount to send in human-readable format (e.g. "0.1" for 0.1 ETH)'),
617
629
  chain_id: z.number().int().describe('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)'),
618
630
  }, async ({ wallet_id, to, amount, chain_id }) => {
@@ -675,7 +687,7 @@ server.tool('transfer_token', 'Send ERC-20 tokens (EVM) or SPL tokens (Solana) t
675
687
  'Signs and broadcasts the transaction. Use get_chains to find stablecoin addresses.', {
676
688
  wallet_id: z.number().int().describe('Wallet ID to send from'),
677
689
  token: z.string().describe('Token address (0x-prefixed ERC-20 contract for EVM, Base58 mint for Solana)'),
678
- to: z.string().describe('Recipient address (0x-prefixed for EVM, Base58 for Solana)'),
690
+ to: AddressSchema.describe('Recipient address (0x-prefixed for EVM, Base58 for Solana)'),
679
691
  amount: z.string().describe('Amount in human-readable format (e.g. "100" for 100 USDC)'),
680
692
  chain_id: z.number().int().describe('Chain ID'),
681
693
  decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
@@ -14,6 +14,7 @@
14
14
  import { readFileSync } from 'node:fs';
15
15
  import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction, LAMPORTS_PER_SOL, } from '@solana/web3.js';
16
16
  import bs58 from 'bs58';
17
+ import { lookupTrustedDecimals } from './x402-payment.js';
17
18
  /* ── Program IDs ─────────────────────────────────────────────────── */
18
19
  const TOKEN_PROGRAM = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
19
20
  const TOKEN_2022_PROGRAM = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
@@ -114,6 +115,34 @@ function assertWithinSolCap(lamports) {
114
115
  throw new Error(`Blocked by local guard: amount exceeds AGENTWALLET_MAX_TX_SOL (${cap} SOL).`);
115
116
  }
116
117
  }
118
+ /**
119
+ * Bound SPL movement. AGENTWALLET_MAX_TX_SOL only ever covered native SOL, so
120
+ * an SPL transfer, which is how USDC moves on Solana and the whole point of the
121
+ * x402 rail here, went out with no local ceiling at all. Mirrors
122
+ * AGENTWALLET_MAX_TX_TOKEN on the EVM side and uses the same variable so an
123
+ * operator sets one token cap, not one per chain family.
124
+ *
125
+ * Decimals for the cap come from the trusted table when the mint is known.
126
+ * The caller-supplied value is not used for the ceiling: an inflated one would
127
+ * inflate the cap exactly like the x402 bypass. When the mint is unknown we
128
+ * assume 6, the tightest common value, so it fails closed.
129
+ */
130
+ function assertWithinSplCap(mint, rawAmount) {
131
+ const cap = (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim();
132
+ if (!cap)
133
+ return;
134
+ if (!/^\d+(\.\d+)?$/.test(cap)) {
135
+ throw new Error(`AGENTWALLET_MAX_TX_TOKEN must be a decimal number, got "${cap}".`);
136
+ }
137
+ const decimals = lookupTrustedDecimals(900, mint) ?? 6;
138
+ const [whole, frac = ''] = cap.split('.');
139
+ const capRaw = BigInt(whole + frac.padEnd(decimals, '0').slice(0, decimals));
140
+ if (rawAmount > capRaw) {
141
+ throw new Error(`Blocked by local guard: SPL transfer of ${rawAmount} base units exceeds ` +
142
+ `AGENTWALLET_MAX_TX_TOKEN (${cap}, evaluated at ${decimals} decimals). ` +
143
+ `Raise the cap deliberately if this is intended.`);
144
+ }
145
+ }
117
146
  /* ── Reads ───────────────────────────────────────────────────────── */
118
147
  export async function localSolBalance(chainId = 900) {
119
148
  const conn = connection(chainId);
@@ -161,6 +190,11 @@ export async function localSplBalance(mintStr, chainId = 900) {
161
190
  export async function localSolTransfer(to, lamports, chainId = 900) {
162
191
  const amount = BigInt(lamports);
163
192
  assertWithinSolCap(amount);
193
+ // Number() silently loses precision past 2**53 lamports. Refuse rather than
194
+ // sign a transaction for an amount other than the one that was requested.
195
+ if (amount > BigInt(Number.MAX_SAFE_INTEGER)) {
196
+ throw new Error(`Amount ${lamports} lamports exceeds safe integer range and was refused.`);
197
+ }
164
198
  const conn = connection(chainId);
165
199
  const payer = getSolanaKeypair();
166
200
  const tx = new Transaction().add(SystemProgram.transfer({
@@ -216,6 +250,9 @@ function createAtaIdempotentIx(payer, ata, owner, mint, tokenProgram) {
216
250
  * not exist, using the idempotent instruction so a race cannot fail the send.
217
251
  */
218
252
  export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId = 900) {
253
+ // Checked before any RPC round trip: a blocked transfer should cost nothing
254
+ // and should not announce itself to the RPC provider first.
255
+ assertWithinSplCap(mintStr, BigInt(rawAmount));
219
256
  const conn = connection(chainId);
220
257
  const payer = getSolanaKeypair();
221
258
  const mint = new PublicKey(mintStr);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentwallet-mcp",
3
3
  "mcpName": "io.github.hifriendbot/agentwallet",
4
- "version": "1.10.2",
4
+ "version": "1.10.3",
5
5
  "description": "MCP server for AgentWallet: self-custody local signing on EVM and Solana, or hosted wallets. Sign, broadcast, pay x402 invoices, and accept x402 payments.",
6
6
  "type": "module",
7
7
  "main": "build/index.js",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "start": "node build/index.js",
19
- "test": "node test/x402-payment.test.mjs && node test/token-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs",
19
+ "test": "node test/x402-payment.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
22
  "keywords": [