agentwallet-mcp 1.12.4 → 1.12.5
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 +1 -1
- package/build/index.js +1 -1
- package/build/local-wallet.js +45 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Use `AGENTWALLET_KEYFILE=/path/to/key` instead if you would rather keep the key
|
|
|
43
43
|
|
|
44
44
|
`AGENTWALLET_MAX_TX_NATIVE` is a per-transaction ceiling in native units (ETH, MATIC and so on). In hosted mode the server enforces limits; in local mode there is no server, so these guards are the only ones there are. Set them.
|
|
45
45
|
|
|
46
|
-
`AGENTWALLET_MAX_TX_TOKEN` is the equivalent ceiling for ERC-20 movement, in human units of the token. **Set this one too if you hold stablecoins.** The native cap cannot see a token transfer: an ERC-20 send carries `value = 0` with the amount in the calldata, so `AGENTWALLET_MAX_TX_NATIVE` alone leaves a USDC balance uncapped. `AGENTWALLET_MAX_TX_TOKEN` covers `transfer`, `transferFrom`, `approve`, `increaseAllowance` and Permit2 `approve`, the approvals because an unbounded allowance is a drain waiting to happen. Any other calldata sent to a token contract or to Permit2 is refused while the cap is set, because the guard cannot price it; set `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1` to allow such calls deliberately. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed. Decimals are resolved locally from the trusted registry, then the token's own `decimals()`; a token that resolves neither way is evaluated at 0 decimals, the only value that cannot fail open.
|
|
46
|
+
`AGENTWALLET_MAX_TX_TOKEN` is the equivalent ceiling for ERC-20 movement, in human units of the token. **Set this one too if you hold stablecoins.** The native cap cannot see a token transfer: an ERC-20 send carries `value = 0` with the amount in the calldata, so `AGENTWALLET_MAX_TX_NATIVE` alone leaves a USDC balance uncapped. `AGENTWALLET_MAX_TX_TOKEN` covers `transfer`, `transferFrom`, `approve`, `increaseAllowance` and Permit2 `approve`, the approvals because an unbounded allowance is a drain waiting to happen. Any other calldata sent to a token contract or to Permit2 is refused while the cap is set, because the guard cannot price it; set `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1` to allow such calls deliberately. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed; a contract counts as "other" only when it demonstrably declines `decimals()`, so an absurd or malformed answer, or an RPC that cannot be reached, refuses rather than allows. Decimals are resolved locally from the trusted registry, then the token's own `decimals()`; a token that resolves neither way is evaluated at 0 decimals, the only value that cannot fail open.
|
|
47
47
|
|
|
48
48
|
### Solana local signing
|
|
49
49
|
|
package/build/index.js
CHANGED
|
@@ -467,7 +467,7 @@ function resolveChainId(network) {
|
|
|
467
467
|
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
468
|
const server = new McpServer({
|
|
469
469
|
name: 'agentwallet',
|
|
470
|
-
version: '1.12.
|
|
470
|
+
version: '1.12.5',
|
|
471
471
|
}, {
|
|
472
472
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. Private keys are encrypted server-side and never exposed — agents sign and broadcast transactions without ever touching raw keys.
|
|
473
473
|
|
package/build/local-wallet.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* on the custodial path and say so explicitly rather than silently downgrading.
|
|
16
16
|
*/
|
|
17
17
|
import { readFileSync } from 'node:fs';
|
|
18
|
-
import { createPublicClient, createWalletClient, http, encodeFunctionData, parseAbi, formatUnits, } from 'viem';
|
|
18
|
+
import { createPublicClient, createWalletClient, http, encodeFunctionData, parseAbi, formatUnits, BaseError, ContractFunctionZeroDataError, ContractFunctionRevertedError, ExecutionRevertedError, AbiDecodingDataSizeTooSmallError, AbiDecodingDataSizeInvalidError, } from 'viem';
|
|
19
19
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
20
20
|
import { lookupTrustedDecimals } from './x402-payment.js';
|
|
21
21
|
import { typedDataFor } from './x402-eip3009.js';
|
|
@@ -195,8 +195,7 @@ export async function localTokenBalance(chainId, token) {
|
|
|
195
195
|
const decimalsCache = new Map();
|
|
196
196
|
/**
|
|
197
197
|
* Decimals for `token`: the trusted registry, then the contract's own
|
|
198
|
-
* decimals() (cached on success).
|
|
199
|
-
* an ERC-20 at all, which is how the guard tells a token from a router.
|
|
198
|
+
* decimals() (cached on success). See DecimalsProbe for the other outcomes.
|
|
200
199
|
*/
|
|
201
200
|
async function probeTokenDecimals(chainId, token) {
|
|
202
201
|
const known = lookupTrustedDecimals(chainId, token);
|
|
@@ -206,28 +205,47 @@ async function probeTokenDecimals(chainId, token) {
|
|
|
206
205
|
const cached = decimalsCache.get(key);
|
|
207
206
|
if (typeof cached === 'number')
|
|
208
207
|
return cached;
|
|
208
|
+
let onChain;
|
|
209
209
|
try {
|
|
210
210
|
const { pub } = clients(chainId);
|
|
211
|
-
|
|
211
|
+
onChain = await pub.readContract({
|
|
212
212
|
address: token,
|
|
213
213
|
abi: ERC20_ABI,
|
|
214
214
|
functionName: 'decimals',
|
|
215
215
|
});
|
|
216
|
-
const d = Number(onChain);
|
|
217
|
-
// Reject nonsense rather than trusting it; an out-of-range value would
|
|
218
|
-
// widen the ceiling exactly like the old assumption did.
|
|
219
|
-
if (Number.isInteger(d) && d >= 0 && d <= 36) {
|
|
220
|
-
decimalsCache.set(key, d);
|
|
221
|
-
return d;
|
|
222
|
-
}
|
|
223
216
|
}
|
|
224
|
-
catch {
|
|
225
|
-
//
|
|
217
|
+
catch (err) {
|
|
218
|
+
// viem wraps every readContract failure in ContractFunctionExecutionError;
|
|
219
|
+
// the cause says whether the contract itself declined (revert, empty
|
|
220
|
+
// return: not an ERC-20) or the answer never arrived (RPC down, timeout,
|
|
221
|
+
// malformed response). Only the first is evidence of anything.
|
|
222
|
+
const declined = err instanceof BaseError && err.walk((e) => e instanceof ContractFunctionZeroDataError
|
|
223
|
+
|| e instanceof ContractFunctionRevertedError
|
|
224
|
+
|| e instanceof ExecutionRevertedError // nodes that report a revert as -32000 rather than 3
|
|
225
|
+
) !== null;
|
|
226
|
+
if (declined)
|
|
227
|
+
return 'not-a-token';
|
|
228
|
+
// A decode failure on non-empty data means the contract answered
|
|
229
|
+
// decimals() with something that is not a uint8: a token, just a broken one.
|
|
230
|
+
const decodeFailed = err instanceof BaseError && err.walk((e) => e instanceof AbiDecodingDataSizeTooSmallError || e instanceof AbiDecodingDataSizeInvalidError) !== null;
|
|
231
|
+
return decodeFailed ? 'unusable' : 'unreachable';
|
|
226
232
|
}
|
|
227
|
-
|
|
233
|
+
const d = Number(onChain);
|
|
234
|
+
// Reject nonsense rather than trusting it; an out-of-range value would
|
|
235
|
+
// widen the ceiling exactly like the old assumption did.
|
|
236
|
+
if (Number.isInteger(d) && d >= 0 && d <= 36) {
|
|
237
|
+
decimalsCache.set(key, d);
|
|
238
|
+
return d;
|
|
239
|
+
}
|
|
240
|
+
return 'unusable';
|
|
228
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Decimals to price a known transfer or approval at. Anything short of a
|
|
244
|
+
* usable answer is evaluated at 0, the only value that cannot fail open.
|
|
245
|
+
*/
|
|
229
246
|
async function resolveTokenDecimals(chainId, token) {
|
|
230
|
-
|
|
247
|
+
const probe = await probeTokenDecimals(chainId, token);
|
|
248
|
+
return typeof probe === 'number' ? probe : 0;
|
|
231
249
|
}
|
|
232
250
|
/** Canonical Permit2, the same address on every EVM chain it is deployed to. */
|
|
233
251
|
const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3';
|
|
@@ -263,12 +281,18 @@ async function assertWithinTokenCap(chainId, to, data) {
|
|
|
263
281
|
if (!priced) {
|
|
264
282
|
if (process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1')
|
|
265
283
|
return;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
// Only a contract that demonstrably declines decimals() is let through.
|
|
285
|
+
// "Answered nonsense" and "could not ask" both refuse: the first is a
|
|
286
|
+
// token with a broken or hostile decimals(), the second is no evidence at
|
|
287
|
+
// all, and this guard does not allow on no evidence (2026-09-26 report).
|
|
288
|
+
const probe = isPermit2 ? 'unusable' : await probeTokenDecimals(chainId, to);
|
|
289
|
+
if (probe !== 'not-a-token') {
|
|
290
|
+
const why = probe === 'unreachable'
|
|
291
|
+
? `the RPC for chain ${chainId} could not be reached to classify the target, so the guard cannot tell a token from a router`
|
|
292
|
+
: `the target is ${isPermit2 ? 'Permit2' : 'a token contract'} and this calldata is not one AGENTWALLET_MAX_TX_TOKEN can price`;
|
|
293
|
+
throw new Error(`Blocked by local guard: calldata selector 0x${selector} refused rather than let through uncapped: ${why}. ` +
|
|
294
|
+
`Use transfer, transferFrom, approve, increaseAllowance or Permit2 approve, ` +
|
|
295
|
+
`or set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1 to allow it deliberately.`);
|
|
272
296
|
}
|
|
273
297
|
return; // an ordinary contract call; it can only pull what an approval already allowed
|
|
274
298
|
}
|
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.12.
|
|
4
|
+
"version": "1.12.5",
|
|
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/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-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/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-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": [
|