agentwallet-mcp 1.12.4 → 1.12.6
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 +76 -86
- package/build/local-wallet.js +78 -29
- package/build/wrapped-native.d.ts +11 -0
- package/build/wrapped-native.js +20 -0
- package/package.json +69 -69
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
|
|
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`, Permit2 `approve`, and `deposit()` / `withdraw(uint256)` on the chain's own wrapped-native contract (so `wrap_eth` and `unwrap_eth` keep working under the cap), 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
|
@@ -16,6 +16,7 @@ import { assertPublicUrl, safeFetch } from './ssrf-guard.js';
|
|
|
16
16
|
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, } 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';
|
|
@@ -467,82 +468,82 @@ function resolveChainId(network) {
|
|
|
467
468
|
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
469
|
const server = new McpServer({
|
|
469
470
|
name: 'agentwallet',
|
|
470
|
-
version: '1.12.
|
|
471
|
+
version: '1.12.6',
|
|
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
|
-
|
|
474
|
-
## Getting Started
|
|
475
|
-
1. Call list_wallets to see existing wallets, or create_wallet to make a new one.
|
|
476
|
-
2. Fund the wallet by sending tokens to its address from an external source.
|
|
477
|
-
3. Use transfer (native tokens) or transfer_token (ERC-20/SPL tokens) to send funds.
|
|
478
|
-
|
|
479
|
-
## Supported Chains
|
|
480
|
-
- **EVM chains:** Ethereum (1), Base (8453), Arbitrum (42161), Optimism (10), Polygon (137), Avalanche (43114), BSC (56), Zora (7777777), PulseChain (369)
|
|
481
|
-
- **Solana:** Mainnet (900), Devnet (901)
|
|
482
|
-
- Call get_chains for the full list with native tokens and stablecoin contract addresses.
|
|
483
|
-
|
|
484
|
-
## EVM vs Solana
|
|
485
|
-
- **Addresses:** EVM uses 0x-prefixed hex (42 chars). Solana uses Base58 (32-44 chars).
|
|
486
|
-
- **Native tokens:** EVM = ETH/POL/BNB/etc (18 decimals, wei). Solana = SOL (9 decimals, lamports).
|
|
487
|
-
- **Token standards:** EVM uses ERC-20. Solana uses SPL tokens.
|
|
488
|
-
- **EVM-only tools:** approve_token, get_allowance, wrap_eth, unwrap_eth, call_contract, get_token_info. These do not work on Solana.
|
|
489
|
-
- **Works on both:** create_wallet, transfer, transfer_token, send_transaction, sign_transaction, get_balance, get_token_balance.
|
|
490
|
-
|
|
491
|
-
## Common Workflows
|
|
492
|
-
|
|
493
|
-
### Send native tokens (ETH, SOL, etc.)
|
|
494
|
-
Use the transfer tool with a human-readable amount (e.g. "0.1" ETH). It auto-converts to wei/lamports.
|
|
495
|
-
|
|
496
|
-
### Send ERC-20 or SPL tokens
|
|
497
|
-
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.
|
|
498
|
-
|
|
499
|
-
### Interact with DeFi (EVM only)
|
|
500
|
-
1. Check allowance with get_allowance.
|
|
501
|
-
2. If needed, call approve_token to let the DeFi contract spend your tokens.
|
|
502
|
-
3. Use send_transaction with the contract's calldata to execute the interaction.
|
|
503
|
-
|
|
504
|
-
### Wrap/unwrap native tokens (EVM only)
|
|
505
|
-
Use wrap_eth to convert ETH → WETH (required by many DeFi protocols). Use unwrap_eth to convert back.
|
|
506
|
-
|
|
507
|
-
### Read on-chain data (EVM only)
|
|
508
|
-
Use call_contract for gas-free read-only calls (eth_call). Provide ABI-encoded calldata.
|
|
509
|
-
|
|
510
|
-
## x402 Payments — Paying for Resources
|
|
511
|
-
The pay_x402 tool handles the full x402 payment flow automatically:
|
|
512
|
-
1. Fetches the URL.
|
|
513
|
-
2. If the server returns HTTP 402, parses the payment requirements.
|
|
514
|
-
3. Pays on-chain using your wallet.
|
|
515
|
-
4. Retries the request with proof of payment.
|
|
516
|
-
Always set max_payment to prevent overspending (e.g. "1.00" for max 1 USDC).
|
|
517
|
-
|
|
518
|
-
## x402 Paywalls — Accepting Payments
|
|
519
|
-
Create paywalls to charge other agents for accessing your resources:
|
|
520
|
-
1. create_paywall — set price, token, chain, and the resource URL to protect.
|
|
521
|
-
2. Share the access URL. Clients that pay on-chain get the resource; others get HTTP 402.
|
|
522
|
-
3. list_paywalls and get_paywall_payments to track revenue.
|
|
523
|
-
4. get_x402_revenue for aggregate stats across all paywalls.
|
|
524
|
-
|
|
525
|
-
## Wallet Security
|
|
526
|
-
- Use pause_wallet to immediately freeze a wallet if compromised. No transactions can be signed while paused.
|
|
527
|
-
- Use unpause_wallet to resume operations.
|
|
528
|
-
- Use delete_wallet to permanently disable a wallet.
|
|
529
|
-
- Wallets have server-enforced spending limits.
|
|
530
|
-
|
|
531
|
-
## Tool Selection Guide
|
|
532
|
-
| Goal | Tool |
|
|
533
|
-
|------|------|
|
|
534
|
-
| Send ETH/SOL/native tokens | transfer |
|
|
535
|
-
| Send USDC/ERC-20/SPL tokens | transfer_token |
|
|
536
|
-
| Check native balance | get_balance |
|
|
537
|
-
| Check token balance | get_token_balance |
|
|
538
|
-
| Look up token details | get_token_info (EVM only) |
|
|
539
|
-
| Approve DeFi spending | approve_token (EVM only) |
|
|
540
|
-
| Check approval amount | get_allowance (EVM only) |
|
|
541
|
-
| Read smart contract | call_contract (EVM only) |
|
|
542
|
-
| Custom transaction | send_transaction |
|
|
543
|
-
| Sign without broadcasting | sign_transaction |
|
|
544
|
-
| Pay for x402 resource | pay_x402 |
|
|
545
|
-
| Charge for your resource | create_paywall |
|
|
473
|
+
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.
|
|
474
|
+
|
|
475
|
+
## Getting Started
|
|
476
|
+
1. Call list_wallets to see existing wallets, or create_wallet to make a new one.
|
|
477
|
+
2. Fund the wallet by sending tokens to its address from an external source.
|
|
478
|
+
3. Use transfer (native tokens) or transfer_token (ERC-20/SPL tokens) to send funds.
|
|
479
|
+
|
|
480
|
+
## Supported Chains
|
|
481
|
+
- **EVM chains:** Ethereum (1), Base (8453), Arbitrum (42161), Optimism (10), Polygon (137), Avalanche (43114), BSC (56), Zora (7777777), PulseChain (369)
|
|
482
|
+
- **Solana:** Mainnet (900), Devnet (901)
|
|
483
|
+
- Call get_chains for the full list with native tokens and stablecoin contract addresses.
|
|
484
|
+
|
|
485
|
+
## EVM vs Solana
|
|
486
|
+
- **Addresses:** EVM uses 0x-prefixed hex (42 chars). Solana uses Base58 (32-44 chars).
|
|
487
|
+
- **Native tokens:** EVM = ETH/POL/BNB/etc (18 decimals, wei). Solana = SOL (9 decimals, lamports).
|
|
488
|
+
- **Token standards:** EVM uses ERC-20. Solana uses SPL tokens.
|
|
489
|
+
- **EVM-only tools:** approve_token, get_allowance, wrap_eth, unwrap_eth, call_contract, get_token_info. These do not work on Solana.
|
|
490
|
+
- **Works on both:** create_wallet, transfer, transfer_token, send_transaction, sign_transaction, get_balance, get_token_balance.
|
|
491
|
+
|
|
492
|
+
## Common Workflows
|
|
493
|
+
|
|
494
|
+
### Send native tokens (ETH, SOL, etc.)
|
|
495
|
+
Use the transfer tool with a human-readable amount (e.g. "0.1" ETH). It auto-converts to wei/lamports.
|
|
496
|
+
|
|
497
|
+
### Send ERC-20 or SPL tokens
|
|
498
|
+
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.
|
|
499
|
+
|
|
500
|
+
### Interact with DeFi (EVM only)
|
|
501
|
+
1. Check allowance with get_allowance.
|
|
502
|
+
2. If needed, call approve_token to let the DeFi contract spend your tokens.
|
|
503
|
+
3. Use send_transaction with the contract's calldata to execute the interaction.
|
|
504
|
+
|
|
505
|
+
### Wrap/unwrap native tokens (EVM only)
|
|
506
|
+
Use wrap_eth to convert ETH → WETH (required by many DeFi protocols). Use unwrap_eth to convert back.
|
|
507
|
+
|
|
508
|
+
### Read on-chain data (EVM only)
|
|
509
|
+
Use call_contract for gas-free read-only calls (eth_call). Provide ABI-encoded calldata.
|
|
510
|
+
|
|
511
|
+
## x402 Payments — Paying for Resources
|
|
512
|
+
The pay_x402 tool handles the full x402 payment flow automatically:
|
|
513
|
+
1. Fetches the URL.
|
|
514
|
+
2. If the server returns HTTP 402, parses the payment requirements.
|
|
515
|
+
3. Pays on-chain using your wallet.
|
|
516
|
+
4. Retries the request with proof of payment.
|
|
517
|
+
Always set max_payment to prevent overspending (e.g. "1.00" for max 1 USDC).
|
|
518
|
+
|
|
519
|
+
## x402 Paywalls — Accepting Payments
|
|
520
|
+
Create paywalls to charge other agents for accessing your resources:
|
|
521
|
+
1. create_paywall — set price, token, chain, and the resource URL to protect.
|
|
522
|
+
2. Share the access URL. Clients that pay on-chain get the resource; others get HTTP 402.
|
|
523
|
+
3. list_paywalls and get_paywall_payments to track revenue.
|
|
524
|
+
4. get_x402_revenue for aggregate stats across all paywalls.
|
|
525
|
+
|
|
526
|
+
## Wallet Security
|
|
527
|
+
- Use pause_wallet to immediately freeze a wallet if compromised. No transactions can be signed while paused.
|
|
528
|
+
- Use unpause_wallet to resume operations.
|
|
529
|
+
- Use delete_wallet to permanently disable a wallet.
|
|
530
|
+
- Wallets have server-enforced spending limits.
|
|
531
|
+
|
|
532
|
+
## Tool Selection Guide
|
|
533
|
+
| Goal | Tool |
|
|
534
|
+
|------|------|
|
|
535
|
+
| Send ETH/SOL/native tokens | transfer |
|
|
536
|
+
| Send USDC/ERC-20/SPL tokens | transfer_token |
|
|
537
|
+
| Check native balance | get_balance |
|
|
538
|
+
| Check token balance | get_token_balance |
|
|
539
|
+
| Look up token details | get_token_info (EVM only) |
|
|
540
|
+
| Approve DeFi spending | approve_token (EVM only) |
|
|
541
|
+
| Check approval amount | get_allowance (EVM only) |
|
|
542
|
+
| Read smart contract | call_contract (EVM only) |
|
|
543
|
+
| Custom transaction | send_transaction |
|
|
544
|
+
| Sign without broadcasting | sign_transaction |
|
|
545
|
+
| Pay for x402 resource | pay_x402 |
|
|
546
|
+
| Charge for your resource | create_paywall |
|
|
546
547
|
| Check usage/billing | get_usage |`,
|
|
547
548
|
});
|
|
548
549
|
// ─── Tool: create_wallet ─────────────────────────────────────────
|
|
@@ -866,17 +867,6 @@ server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved
|
|
|
866
867
|
});
|
|
867
868
|
});
|
|
868
869
|
// ─── 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
870
|
// ─── Tool: wrap_eth ─────────────────────────────────────────────
|
|
881
871
|
server.tool('wrap_eth', 'Wrap native tokens (ETH, AVAX, BNB, POL, PLS) into their wrapped ERC-20 version (WETH, WAVAX, etc.). ' +
|
|
882
872
|
'Required for most DeFi protocols that use ERC-20 tokens instead of raw native tokens. ' +
|
package/build/local-wallet.js
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
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
|
+
import { wrappedNativeAddress } from './wrapped-native.js';
|
|
21
22
|
import { typedDataFor } from './x402-eip3009.js';
|
|
22
23
|
import { uptoTypedData } from './x402-permit2.js';
|
|
23
24
|
/* ── Key loading ─────────────────────────────────────────────────── */
|
|
@@ -195,8 +196,7 @@ export async function localTokenBalance(chainId, token) {
|
|
|
195
196
|
const decimalsCache = new Map();
|
|
196
197
|
/**
|
|
197
198
|
* 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.
|
|
199
|
+
* decimals() (cached on success). See DecimalsProbe for the other outcomes.
|
|
200
200
|
*/
|
|
201
201
|
async function probeTokenDecimals(chainId, token) {
|
|
202
202
|
const known = lookupTrustedDecimals(chainId, token);
|
|
@@ -206,28 +206,47 @@ async function probeTokenDecimals(chainId, token) {
|
|
|
206
206
|
const cached = decimalsCache.get(key);
|
|
207
207
|
if (typeof cached === 'number')
|
|
208
208
|
return cached;
|
|
209
|
+
let onChain;
|
|
209
210
|
try {
|
|
210
211
|
const { pub } = clients(chainId);
|
|
211
|
-
|
|
212
|
+
onChain = await pub.readContract({
|
|
212
213
|
address: token,
|
|
213
214
|
abi: ERC20_ABI,
|
|
214
215
|
functionName: 'decimals',
|
|
215
216
|
});
|
|
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
217
|
}
|
|
224
|
-
catch {
|
|
225
|
-
//
|
|
218
|
+
catch (err) {
|
|
219
|
+
// viem wraps every readContract failure in ContractFunctionExecutionError;
|
|
220
|
+
// the cause says whether the contract itself declined (revert, empty
|
|
221
|
+
// return: not an ERC-20) or the answer never arrived (RPC down, timeout,
|
|
222
|
+
// malformed response). Only the first is evidence of anything.
|
|
223
|
+
const declined = err instanceof BaseError && err.walk((e) => e instanceof ContractFunctionZeroDataError
|
|
224
|
+
|| e instanceof ContractFunctionRevertedError
|
|
225
|
+
|| e instanceof ExecutionRevertedError // nodes that report a revert as -32000 rather than 3
|
|
226
|
+
) !== null;
|
|
227
|
+
if (declined)
|
|
228
|
+
return 'not-a-token';
|
|
229
|
+
// A decode failure on non-empty data means the contract answered
|
|
230
|
+
// decimals() with something that is not a uint8: a token, just a broken one.
|
|
231
|
+
const decodeFailed = err instanceof BaseError && err.walk((e) => e instanceof AbiDecodingDataSizeTooSmallError || e instanceof AbiDecodingDataSizeInvalidError) !== null;
|
|
232
|
+
return decodeFailed ? 'unusable' : 'unreachable';
|
|
226
233
|
}
|
|
227
|
-
|
|
234
|
+
const d = Number(onChain);
|
|
235
|
+
// Reject nonsense rather than trusting it; an out-of-range value would
|
|
236
|
+
// widen the ceiling exactly like the old assumption did.
|
|
237
|
+
if (Number.isInteger(d) && d >= 0 && d <= 36) {
|
|
238
|
+
decimalsCache.set(key, d);
|
|
239
|
+
return d;
|
|
240
|
+
}
|
|
241
|
+
return 'unusable';
|
|
228
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Decimals to price a known transfer or approval at. Anything short of a
|
|
245
|
+
* usable answer is evaluated at 0, the only value that cannot fail open.
|
|
246
|
+
*/
|
|
229
247
|
async function resolveTokenDecimals(chainId, token) {
|
|
230
|
-
|
|
248
|
+
const probe = await probeTokenDecimals(chainId, token);
|
|
249
|
+
return typeof probe === 'number' ? probe : 0;
|
|
231
250
|
}
|
|
232
251
|
/** Canonical Permit2, the same address on every EVM chain it is deployed to. */
|
|
233
252
|
const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3';
|
|
@@ -237,6 +256,11 @@ const CAP_LAYOUTS = {
|
|
|
237
256
|
'095ea7b3': { amountIndex: 1, what: 'approval' }, // approve(address,uint256)
|
|
238
257
|
'39509351': { amountIndex: 1, what: 'allowance increase' }, // increaseAllowance(address,uint256)
|
|
239
258
|
'87517c45': { amountIndex: 2, tokenIndex: 0, what: 'Permit2 approval' }, // Permit2.approve(address,address,uint160,uint48)
|
|
259
|
+
// WETH9 calls, priced only when `to` is the chain's own wrapped-native
|
|
260
|
+
// contract (wrappedNativeOnly). deposit() carries its amount as msg.value,
|
|
261
|
+
// not in calldata, hence amountIndex -1; withdraw(uint256) has it at word 0.
|
|
262
|
+
d0e30db0: { amountIndex: -1, what: 'wrap of native into the wrapped token', wrappedNativeOnly: true }, // deposit()
|
|
263
|
+
'2e1a7d4d': { amountIndex: 0, what: 'unwrap of the wrapped token', wrappedNativeOnly: true }, // withdraw(uint256)
|
|
240
264
|
};
|
|
241
265
|
/**
|
|
242
266
|
* The selector table used to be an allowlist that returned early for anything
|
|
@@ -247,8 +271,15 @@ const CAP_LAYOUTS = {
|
|
|
247
271
|
* refused, because any state change there can move funds. Calls to other
|
|
248
272
|
* contracts (routers, bridges) still pass: they can only pull what an approval
|
|
249
273
|
* already allowed, and approvals are what this guard bounds.
|
|
274
|
+
*
|
|
275
|
+
* Reported privately 2026-09-27: that refusal also caught our own wrap_eth and
|
|
276
|
+
* unwrap_eth, whose WETH deposit() / withdraw(uint256) calldata was unpriced,
|
|
277
|
+
* so both tools died the moment the cap was set and the only way back was
|
|
278
|
+
* AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1, which weakens the guard for every
|
|
279
|
+
* call. Now both are priced, but only on the chain's own wrapped-native
|
|
280
|
+
* contract; the same selectors on any other token stay refused.
|
|
250
281
|
*/
|
|
251
|
-
async function assertWithinTokenCap(chainId, to, data) {
|
|
282
|
+
async function assertWithinTokenCap(chainId, to, data, valueWei = 0n) {
|
|
252
283
|
const cap = (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim();
|
|
253
284
|
if (!cap)
|
|
254
285
|
return;
|
|
@@ -259,24 +290,40 @@ async function assertWithinTokenCap(chainId, to, data) {
|
|
|
259
290
|
const selector = hex.slice(0, 8).toLowerCase();
|
|
260
291
|
const isPermit2 = to.toLowerCase() === PERMIT2;
|
|
261
292
|
const layout = CAP_LAYOUTS[selector];
|
|
262
|
-
const
|
|
293
|
+
const isWrappedNative = to.toLowerCase() === wrappedNativeAddress(chainId);
|
|
294
|
+
const priced = layout
|
|
295
|
+
&& (layout.tokenIndex === undefined || isPermit2)
|
|
296
|
+
&& (!layout.wrappedNativeOnly || isWrappedNative);
|
|
263
297
|
if (!priced) {
|
|
264
298
|
if (process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1')
|
|
265
299
|
return;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
300
|
+
// Only a contract that demonstrably declines decimals() is let through.
|
|
301
|
+
// "Answered nonsense" and "could not ask" both refuse: the first is a
|
|
302
|
+
// token with a broken or hostile decimals(), the second is no evidence at
|
|
303
|
+
// all, and this guard does not allow on no evidence (2026-09-26 report).
|
|
304
|
+
const probe = isPermit2 ? 'unusable' : await probeTokenDecimals(chainId, to);
|
|
305
|
+
if (probe !== 'not-a-token') {
|
|
306
|
+
const why = probe === 'unreachable'
|
|
307
|
+
? `the RPC for chain ${chainId} could not be reached to classify the target, so the guard cannot tell a token from a router`
|
|
308
|
+
: `the target is ${isPermit2 ? 'Permit2' : 'a token contract'} and this calldata is not one AGENTWALLET_MAX_TX_TOKEN can price`;
|
|
309
|
+
throw new Error(`Blocked by local guard: calldata selector 0x${selector} refused rather than let through uncapped: ${why}. ` +
|
|
310
|
+
`Use transfer, transferFrom, approve, increaseAllowance, Permit2 approve, ` +
|
|
311
|
+
`or deposit/withdraw on the chain's wrapped-native contract, ` +
|
|
312
|
+
`or set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1 to allow it deliberately.`);
|
|
272
313
|
}
|
|
273
314
|
return; // an ordinary contract call; it can only pull what an approval already allowed
|
|
274
315
|
}
|
|
275
316
|
const wordAt = (i) => hex.slice(8 + i * 64, 8 + (i + 1) * 64);
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
317
|
+
let amount;
|
|
318
|
+
if (layout.amountIndex < 0) {
|
|
319
|
+
amount = valueWei; // deposit(): what gets wrapped is the native value sent along
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
const word = wordAt(layout.amountIndex);
|
|
323
|
+
if (word.length !== 64)
|
|
324
|
+
return; // malformed; leave it to the node to reject
|
|
325
|
+
amount = BigInt('0x' + word);
|
|
326
|
+
}
|
|
280
327
|
let token = to;
|
|
281
328
|
if (layout.tokenIndex !== undefined) {
|
|
282
329
|
const tw = wordAt(layout.tokenIndex);
|
|
@@ -284,7 +331,9 @@ async function assertWithinTokenCap(chainId, to, data) {
|
|
|
284
331
|
return;
|
|
285
332
|
token = ('0x' + tw.slice(24));
|
|
286
333
|
}
|
|
287
|
-
|
|
334
|
+
// Every wrapped native in the table is a WETH9 clone with 18 decimals, the
|
|
335
|
+
// same figure wrap_eth and unwrap_eth encode with; no RPC needed to price it.
|
|
336
|
+
const decimals = layout.wrappedNativeOnly ? 18 : await resolveTokenDecimals(chainId, token);
|
|
288
337
|
const [whole, frac = ''] = cap.split('.');
|
|
289
338
|
const capRaw = BigInt(whole + frac.padEnd(decimals, '0').slice(0, decimals));
|
|
290
339
|
if (amount > capRaw) {
|
|
@@ -296,7 +345,7 @@ async function assertWithinTokenCap(chainId, to, data) {
|
|
|
296
345
|
export async function localSend(chainId, to, valueWei, data) {
|
|
297
346
|
assertWithinNativeCap(valueWei);
|
|
298
347
|
if (data && data.length >= 10)
|
|
299
|
-
await assertWithinTokenCap(chainId, to, data);
|
|
348
|
+
await assertWithinTokenCap(chainId, to, data, valueWei);
|
|
300
349
|
const { wallet } = clients(chainId);
|
|
301
350
|
const hash = await wallet.sendTransaction({
|
|
302
351
|
to,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapped-native (WETH9-style) token per EVM chain. Shared by the wrap_eth and
|
|
3
|
+
* unwrap_eth tools and by the local token cap, which prices deposit() and
|
|
4
|
+
* withdraw(uint256) only when they are aimed at this exact contract.
|
|
5
|
+
*/
|
|
6
|
+
export declare const WRAPPED_NATIVE: Record<number, {
|
|
7
|
+
address: string;
|
|
8
|
+
symbol: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** Lowercased wrapped-native address for `chainId`, or undefined when none is configured. */
|
|
11
|
+
export declare function wrappedNativeAddress(chainId: number): string | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapped-native (WETH9-style) token per EVM chain. Shared by the wrap_eth and
|
|
3
|
+
* unwrap_eth tools and by the local token cap, which prices deposit() and
|
|
4
|
+
* withdraw(uint256) only when they are aimed at this exact contract.
|
|
5
|
+
*/
|
|
6
|
+
export const WRAPPED_NATIVE = {
|
|
7
|
+
1: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH' },
|
|
8
|
+
8453: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
9
|
+
42161: { address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', symbol: 'WETH' },
|
|
10
|
+
10: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
11
|
+
137: { address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', symbol: 'WPOL' },
|
|
12
|
+
56: { address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', symbol: 'WBNB' },
|
|
13
|
+
43114: { address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', symbol: 'WAVAX' },
|
|
14
|
+
7777777: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
15
|
+
369: { address: '0xA1077a294dDE1B09bB078844df40758a5D0f9a27', symbol: 'WPLS' },
|
|
16
|
+
};
|
|
17
|
+
/** Lowercased wrapped-native address for `chainId`, or undefined when none is configured. */
|
|
18
|
+
export function wrappedNativeAddress(chainId) {
|
|
19
|
+
return WRAPPED_NATIVE[chainId]?.address.toLowerCase();
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agentwallet-mcp",
|
|
3
|
-
"mcpName": "io.github.hifriendbot/agentwallet",
|
|
4
|
-
"version": "1.12.
|
|
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
|
-
"type": "module",
|
|
7
|
-
"main": "build/index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"agentwallet-mcp": "build/index.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"build",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc",
|
|
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",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"mcp",
|
|
24
|
-
"model-context-protocol",
|
|
25
|
-
"wallet",
|
|
26
|
-
"ai-agent",
|
|
27
|
-
"blockchain",
|
|
28
|
-
"evm",
|
|
29
|
-
"ethereum",
|
|
30
|
-
"base",
|
|
31
|
-
"arbitrum",
|
|
32
|
-
"optimism",
|
|
33
|
-
"polygon",
|
|
34
|
-
"avalanche",
|
|
35
|
-
"bsc",
|
|
36
|
-
"zora",
|
|
37
|
-
"pulsechain",
|
|
38
|
-
"solana",
|
|
39
|
-
"spl-token",
|
|
40
|
-
"erc20",
|
|
41
|
-
"token-transfer",
|
|
42
|
-
"transaction-signing",
|
|
43
|
-
"x402",
|
|
44
|
-
"http-402",
|
|
45
|
-
"agent-payments",
|
|
46
|
-
"agentwallet",
|
|
47
|
-
"self-custody",
|
|
48
|
-
"non-custodial",
|
|
49
|
-
"local-signing"
|
|
50
|
-
],
|
|
51
|
-
"author": "HiFriendbot (https://hifriendbot.com)",
|
|
52
|
-
"license": "MIT",
|
|
53
|
-
"homepage": "https://hifriendbot.com/wallet/",
|
|
54
|
-
"engines": {
|
|
55
|
-
"node": ">=18.0.0"
|
|
56
|
-
},
|
|
57
|
-
"dependencies": {
|
|
58
|
-
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
59
|
-
"@solana/web3.js": "^1.98.4",
|
|
60
|
-
"bs58": "^6.0.0",
|
|
61
|
-
"undici": "^8.9.0",
|
|
62
|
-
"viem": "^2.55.10",
|
|
63
|
-
"zod": "^3.24.0"
|
|
64
|
-
},
|
|
65
|
-
"devDependencies": {
|
|
66
|
-
"@types/node": "^20.0.0",
|
|
67
|
-
"typescript": "^5.3.0"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "agentwallet-mcp",
|
|
3
|
+
"mcpName": "io.github.hifriendbot/agentwallet",
|
|
4
|
+
"version": "1.12.6",
|
|
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
|
+
"type": "module",
|
|
7
|
+
"main": "build/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"agentwallet-mcp": "build/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"build",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
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/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"wallet",
|
|
26
|
+
"ai-agent",
|
|
27
|
+
"blockchain",
|
|
28
|
+
"evm",
|
|
29
|
+
"ethereum",
|
|
30
|
+
"base",
|
|
31
|
+
"arbitrum",
|
|
32
|
+
"optimism",
|
|
33
|
+
"polygon",
|
|
34
|
+
"avalanche",
|
|
35
|
+
"bsc",
|
|
36
|
+
"zora",
|
|
37
|
+
"pulsechain",
|
|
38
|
+
"solana",
|
|
39
|
+
"spl-token",
|
|
40
|
+
"erc20",
|
|
41
|
+
"token-transfer",
|
|
42
|
+
"transaction-signing",
|
|
43
|
+
"x402",
|
|
44
|
+
"http-402",
|
|
45
|
+
"agent-payments",
|
|
46
|
+
"agentwallet",
|
|
47
|
+
"self-custody",
|
|
48
|
+
"non-custodial",
|
|
49
|
+
"local-signing"
|
|
50
|
+
],
|
|
51
|
+
"author": "HiFriendbot (https://hifriendbot.com)",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"homepage": "https://hifriendbot.com/wallet/",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
59
|
+
"@solana/web3.js": "^1.98.4",
|
|
60
|
+
"bs58": "^6.0.0",
|
|
61
|
+
"undici": "^8.9.0",
|
|
62
|
+
"viem": "^2.55.10",
|
|
63
|
+
"zod": "^3.24.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^20.0.0",
|
|
67
|
+
"typescript": "^5.3.0"
|
|
68
|
+
}
|
|
69
|
+
}
|