agentwallet-mcp 1.10.4 → 1.10.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/build/local-solana.js +34 -7
- package/build/local-wallet.js +43 -6
- package/package.json +2 -2
package/build/local-solana.js
CHANGED
|
@@ -122,19 +122,46 @@ function assertWithinSolCap(lamports) {
|
|
|
122
122
|
* AGENTWALLET_MAX_TX_TOKEN on the EVM side and uses the same variable so an
|
|
123
123
|
* operator sets one token cap, not one per chain family.
|
|
124
124
|
*
|
|
125
|
-
* Decimals for the cap come from the trusted table when the mint is known
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
* Decimals for the cap come from the trusted table when the mint is known, then
|
|
126
|
+
* from the mint account itself, and finally 0. The caller-supplied value is
|
|
127
|
+
* never used for the ceiling: an inflated one would inflate the cap exactly
|
|
128
|
+
* like the x402 bypass.
|
|
129
|
+
*
|
|
130
|
+
* 0 is the only safe fallback. Assuming 6 for an unknown mint is what AW-001
|
|
131
|
+
* exploited: for a mint with d decimals, a ceiling evaluated at 6 is 10**(6-d)
|
|
132
|
+
* times too permissive, and SPL mints with 0 to 5 decimals are common.
|
|
129
133
|
*/
|
|
130
|
-
|
|
134
|
+
const splDecimalsCache = new Map();
|
|
135
|
+
async function resolveMintDecimals(mint, chainId) {
|
|
136
|
+
const known = lookupTrustedDecimals(900, mint);
|
|
137
|
+
if (typeof known === 'number')
|
|
138
|
+
return known;
|
|
139
|
+
const cached = splDecimalsCache.get(mint);
|
|
140
|
+
if (typeof cached === 'number')
|
|
141
|
+
return cached;
|
|
142
|
+
try {
|
|
143
|
+
const conn = connection(chainId);
|
|
144
|
+
const info = await conn.getParsedAccountInfo(new PublicKey(mint));
|
|
145
|
+
const data = info?.value?.data;
|
|
146
|
+
const d = Number(data?.parsed?.info?.decimals);
|
|
147
|
+
if (Number.isInteger(d) && d >= 0 && d <= 36) {
|
|
148
|
+
splDecimalsCache.set(mint, d);
|
|
149
|
+
return d;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Unreachable RPC or non-standard mint: fall through to the safe floor.
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
async function assertWithinSplCap(mint, rawAmount, chainId = 900) {
|
|
131
158
|
const cap = (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim();
|
|
132
159
|
if (!cap)
|
|
133
160
|
return;
|
|
134
161
|
if (!/^\d+(\.\d+)?$/.test(cap)) {
|
|
135
162
|
throw new Error(`AGENTWALLET_MAX_TX_TOKEN must be a decimal number, got "${cap}".`);
|
|
136
163
|
}
|
|
137
|
-
const decimals =
|
|
164
|
+
const decimals = await resolveMintDecimals(mint, chainId);
|
|
138
165
|
const [whole, frac = ''] = cap.split('.');
|
|
139
166
|
const capRaw = BigInt(whole + frac.padEnd(decimals, '0').slice(0, decimals));
|
|
140
167
|
if (rawAmount > capRaw) {
|
|
@@ -252,7 +279,7 @@ function createAtaIdempotentIx(payer, ata, owner, mint, tokenProgram) {
|
|
|
252
279
|
export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId = 900) {
|
|
253
280
|
// Checked before any RPC round trip: a blocked transfer should cost nothing
|
|
254
281
|
// and should not announce itself to the RPC provider first.
|
|
255
|
-
assertWithinSplCap(mintStr, BigInt(rawAmount));
|
|
282
|
+
await assertWithinSplCap(mintStr, BigInt(rawAmount), chainId);
|
|
256
283
|
const conn = connection(chainId);
|
|
257
284
|
const payer = getSolanaKeypair();
|
|
258
285
|
const mint = new PublicKey(mintStr);
|
package/build/local-wallet.js
CHANGED
|
@@ -178,11 +178,48 @@ export async function localTokenBalance(chainId, token) {
|
|
|
178
178
|
* waiting to happen.
|
|
179
179
|
*
|
|
180
180
|
* Opt-in via AGENTWALLET_MAX_TX_TOKEN, expressed in human units of the token.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
181
|
+
*
|
|
182
|
+
* Decimals decide the ceiling, so getting them wrong breaks the guard. They are
|
|
183
|
+
* resolved in this order: the trusted registry, then the token's own decimals()
|
|
184
|
+
* on chain (cached), and finally 0. Assuming 0 for an unresolvable token is the
|
|
185
|
+
* only value that cannot fail open: any real token has decimals >= 0, so a cap
|
|
186
|
+
* evaluated at 0 is never larger than the true one.
|
|
187
|
+
*
|
|
188
|
+
* Do not "assume 6 because it is the smallest in common use". It is not: GUSD
|
|
189
|
+
* and EURS use 2, and some tokens use 0. For a token with d decimals, a ceiling
|
|
190
|
+
* evaluated at 6 is 10**(6-d) times too permissive, which is how AW-001 let a
|
|
191
|
+
* 2-decimal token move 10,000x the configured cap.
|
|
184
192
|
*/
|
|
185
|
-
|
|
193
|
+
const decimalsCache = new Map();
|
|
194
|
+
async function resolveTokenDecimals(chainId, token) {
|
|
195
|
+
const known = lookupTrustedDecimals(chainId, token);
|
|
196
|
+
if (typeof known === 'number')
|
|
197
|
+
return known;
|
|
198
|
+
const key = `${chainId}:${token.toLowerCase()}`;
|
|
199
|
+
const cached = decimalsCache.get(key);
|
|
200
|
+
if (typeof cached === 'number')
|
|
201
|
+
return cached;
|
|
202
|
+
try {
|
|
203
|
+
const { pub } = clients(chainId);
|
|
204
|
+
const onChain = await pub.readContract({
|
|
205
|
+
address: token,
|
|
206
|
+
abi: ERC20_ABI,
|
|
207
|
+
functionName: 'decimals',
|
|
208
|
+
});
|
|
209
|
+
const d = Number(onChain);
|
|
210
|
+
// Reject nonsense rather than trusting it; an out-of-range value would
|
|
211
|
+
// widen the ceiling exactly like the old assumption did.
|
|
212
|
+
if (Number.isInteger(d) && d >= 0 && d <= 36) {
|
|
213
|
+
decimalsCache.set(key, d);
|
|
214
|
+
return d;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Unreachable RPC, non-standard token, anything at all: fall through to 0.
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
async function assertWithinTokenCap(chainId, token, data) {
|
|
186
223
|
const cap = (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim();
|
|
187
224
|
if (!cap)
|
|
188
225
|
return;
|
|
@@ -200,7 +237,7 @@ function assertWithinTokenCap(chainId, token, data) {
|
|
|
200
237
|
if (word.length !== 64)
|
|
201
238
|
return; // malformed; leave it to the node to reject
|
|
202
239
|
const amount = BigInt('0x' + word);
|
|
203
|
-
const decimals =
|
|
240
|
+
const decimals = await resolveTokenDecimals(chainId, token);
|
|
204
241
|
const [whole, frac = ''] = cap.split('.');
|
|
205
242
|
const capRaw = BigInt(whole + frac.padEnd(decimals, '0').slice(0, decimals));
|
|
206
243
|
if (amount > capRaw) {
|
|
@@ -213,7 +250,7 @@ function assertWithinTokenCap(chainId, token, data) {
|
|
|
213
250
|
export async function localSend(chainId, to, valueWei, data) {
|
|
214
251
|
assertWithinNativeCap(valueWei);
|
|
215
252
|
if (data && data.length >= 10)
|
|
216
|
-
assertWithinTokenCap(chainId, to, data);
|
|
253
|
+
await assertWithinTokenCap(chainId, to, data);
|
|
217
254
|
const { wallet } = clients(chainId);
|
|
218
255
|
const hash = await wallet.sendTransaction({
|
|
219
256
|
to,
|
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.
|
|
4
|
+
"version": "1.10.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/token-cap.test.mjs && node test/spl-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/unknown-decimals-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": [
|