agentwallet-mcp 1.14.6 → 1.14.8
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 +5 -4
- package/build/index.js +106 -24
- package/build/ledger.d.ts +5 -2
- package/build/local-solana.d.ts +10 -0
- package/build/local-solana.js +51 -21
- package/build/policy.d.ts +22 -169
- package/package.json +5 -5
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`, 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. `increaseAllowance` is judged on the resulting total: the current allowance is read first and the call refuses if it cannot be, so two compliant increments cannot stack a standing allowance above the cap. 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. For a token outside the registry the RPC is the only source of decimals, and an RPC you do not control could scale the amount and the cap together; pin such tokens with `AGENTWALLET_TOKEN_DECIMALS` (for example `8453:0x<token>=6`) and the pin is used everywhere the RPC's answer would have been.
|
|
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. ERC-721 and ERC-1155 calldata (`setApprovalForAll`, `safeTransferFrom`) is refused while the cap is set, since nothing prices an NFT; without the cap it passes the way an ERC-20 transfer of any size does, except that an operator grant is an unlimited approval and is refused everywhere by default (see `AGENTWALLET_ALLOW_UNLIMITED_APPROVAL`). 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. `increaseAllowance` is judged on the resulting total: the current allowance is read first and the call refuses if it cannot be, so two compliant increments cannot stack a standing allowance above the cap. 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. For a token outside the registry the RPC is the only source of decimals, and an RPC you do not control could scale the amount and the cap together; pin such tokens with `AGENTWALLET_TOKEN_DECIMALS` (for example `8453:0x<token>=6`) and the pin is used everywhere the RPC's answer would have been.
|
|
47
47
|
|
|
48
48
|
### Solana local signing
|
|
49
49
|
|
|
@@ -256,7 +256,7 @@ pay_x402(
|
|
|
256
256
|
|
|
257
257
|
**Asset risk.** `check_token_risk` (and every `approve_token` result) reports honeypot, tax, owner-power, verified-source, holder-concentration and liquidity flags from GoPlus Security, with an on-chain fallback. A warning for the caller to weigh, never a block.
|
|
258
258
|
|
|
259
|
-
**Proof.** Real settlements are listed in [PAYMENTS.md](https://github.com/hifriendbot/agentwallet-mcp/blob/main/PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas.
|
|
259
|
+
**Proof.** Real settlements are listed in [PAYMENTS.md](https://github.com/hifriendbot/agentwallet-mcp/blob/main/PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas. The same list, rendered with Basescan links, is at [hifriendbot.com/wallet/payments](https://hifriendbot.com/wallet/payments/), and a five-minute walkthrough from install to a real settlement is at [hifriendbot.com/wallet/quickstart](https://hifriendbot.com/wallet/quickstart/). Security policy, fixed disclosures and the release history: [hifriendbot.com/security](https://hifriendbot.com/security/) and [hifriendbot.com/changelog](https://hifriendbot.com/changelog/).
|
|
260
260
|
|
|
261
261
|
## Pay any x402 API from Claude in three steps
|
|
262
262
|
|
|
@@ -340,6 +340,7 @@ Which guards apply depends on who holds the key.
|
|
|
340
340
|
- `AGENTWALLET_MAX_FEE_NATIVE`: the most one transaction may burn in fees, gas x max fee per gas, in native units (default `0.01`). The value cap alone did not bound fees: a node answering an absurd tip or gas estimate could take the whole balance as priority fee on a cap-compliant transfer. Caller-supplied `gas_limit` / `max_fee` / `priority_fee` are honoured and then bounded by this cap
|
|
341
341
|
- `AGENTWALLET_MAX_TX_TOKEN`: ceiling per ERC-20 transfer or approval, in human units of the token
|
|
342
342
|
- `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS`: set to `1` to let calldata the token cap cannot price reach a token contract or Permit2 (refused by default while the cap is set)
|
|
343
|
+
- `AGENTWALLET_ALLOW_UNLIMITED_APPROVAL`: set to `1` to let an unlimited ERC-20 allowance (`approve_token` with `"max"`, `approve_permit2` with no amount) or an NFT operator grant (`setApprovalForAll`) through while no spend policy is set; refused by default, and always refused under a policy
|
|
343
344
|
- `AGENTWALLET_TOKEN_DECIMALS`: pin decimals for tokens outside the registry (`8453:0x<token>=6,<mint>=9`); a pin is used for amounts and caps instead of asking the RPC
|
|
344
345
|
- `AGENTWALLET_MAX_TX_SOL`: ceiling per native SOL transfer (also charged for the rent of a recipient token account an SPL send has to create). It does not cover SPL token amounts: `AGENTWALLET_MAX_TX_TOKEN` does, on Solana as on EVM
|
|
345
346
|
- `AGENTWALLET_MAX_AUTOPAY`: ceiling per x402 auto-payment in stablecoin units (default `1`); `AGENTWALLET_AUTOPAY_ASSETS` opts native or non-stable assets in; `AGENTWALLET_X402_MAX_TIMEOUT` bounds how long a signed authorization stays valid (default 3600 seconds)
|
|
@@ -374,8 +375,8 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
|
|
|
374
375
|
```
|
|
375
376
|
|
|
376
377
|
- **`budget`**: rolling dollar budgets (last 24 hours, last 30 days, lifetime) across every chain, counting x402 payments, stablecoin transfers and stablecoin approvals in registry stablecoins. A spend the budget cannot price (a token outside the registry) is refused while a budget is set, unless `"unpriced_spends": "allow"`. An SPL send is priced at the mint's own decimals; a `token_decimals` hint that disagrees with the mint is refused before any request is made.
|
|
377
|
-
- **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract).
|
|
378
|
-
- **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, or the same calldata through `send_transaction`) is what the spender will take later, so it is priced, checked and reserved on the ledger when it is granted, the spender is checked against the recipient lists, and an unlimited approval (2^128 base units or more) is refused
|
|
378
|
+
- **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract) and, in local Solana mode, the rent an SPL send pays to create the recipient's token account (the recipient owns that account and can close it for the lamports; the ledger records it as its own line, method `rent`, to the recipient, and `assets.allow` does not need to list `native` for it).
|
|
379
|
+
- **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, or the same calldata through `send_transaction`) is what the spender will take later, so it is priced, checked and reserved on the ledger when it is granted, the spender is checked against the recipient lists, and an unlimited approval (2^128 base units or more) is refused, since no budget can count it; grant an exact amount. An NFT operator grant (`setApprovalForAll`, ERC-777 `authorizeOperator`) is unbounded by construction and refused the same way, and an ERC-721 or ERC-1155 transfer is a spend of the collection to the recipient that no budget can price, so it is refused under a dollar budget unless `unpriced_spends` is `"allow"`, with the recipient checked either way. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted.
|
|
379
380
|
- **`hosts`**, **`per_host`**: which endpoints `pay_x402` may pay, and how much per host per day, per month and per payment. A denied host is never contacted, not even for a quote. A host is matched by its canonical DNS name (lowercase, the root label's trailing dot removed), so `api.example.com.` and its percent-encoded spelling are `api.example.com` to every list and to the ledger's per-host sums.
|
|
380
381
|
- **`recipients`**: where funds may go (transfers, x402 `payTo`, approval spenders, contracts that receive native value). The allow list covers those funds destinations; the contract a zero-value call is aimed at (a token being approved, a router) is checked against the deny list and the chain list.
|
|
381
382
|
- **`chains`**, **`assets`**: allow lists; an asset entry can be scoped `"8453:0x..."` or be the word `native`.
|
package/build/index.js
CHANGED
|
@@ -13,13 +13,15 @@ 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
15
|
// Validation errors used to echo an unknown key or enum value unbounded, a reflector for a second-stage injection (round 6).
|
|
16
|
-
z.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
z.config({
|
|
17
|
+
customError: (issue) => {
|
|
18
|
+
const cut = (v) => { const t = String(v); return t.length > 80 ? t.slice(0, 80) + '…' : t; };
|
|
19
|
+
if (issue.code === 'unrecognized_keys')
|
|
20
|
+
return `Unrecognized key(s) in object: ${issue.keys.map((k) => `'${cut(k)}'`).join(', ')}`;
|
|
21
|
+
if (issue.code === 'invalid_value')
|
|
22
|
+
return `Invalid enum value. Expected ${issue.values.map((o) => `'${String(o)}'`).join(' | ')}, received '${cut(issue.input)}'`;
|
|
23
|
+
return undefined;
|
|
24
|
+
},
|
|
23
25
|
});
|
|
24
26
|
import { assertPublicUrl, safeFetch, finalUrlOf } from './ssrf-guard.js';
|
|
25
27
|
import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, assetLabel, autopayAssetAllowed, autopayEnvCap, effectiveAutopayCap, maxAuthWindowSeconds, parseDecimalPins, approvalRefusal, authorizationResource, resolveNetworkChainId, normalizeRawAmount, parseAutopayAssets, assetSupportsExact, approvalResourceUrl, } from './x402-payment.js';
|
|
@@ -30,7 +32,7 @@ import { WRAPPED_NATIVE } from './wrapped-native.js';
|
|
|
30
32
|
import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, permit2Address, } from './x402-permit2.js';
|
|
31
33
|
import { assessTokenRisk } from './token-risk.js';
|
|
32
34
|
import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, decodeAbiString as decodeDomainString, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, settlementVerdict, legacyReceiptAllowed, sanitizeRequired, } from './x402-eip3009.js';
|
|
33
|
-
import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, } from './local-solana.js';
|
|
35
|
+
import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, splAccountRent, } from './local-solana.js';
|
|
34
36
|
import { localSimulate } from './local-wallet.js';
|
|
35
37
|
import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, hostOf, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
|
|
36
38
|
import { Ledger } from './ledger.js';
|
|
@@ -92,7 +94,10 @@ function reserveSpend(input) {
|
|
|
92
94
|
const chain = chainAllowed(p, input.chainId);
|
|
93
95
|
if (!chain.ok)
|
|
94
96
|
throw new Error(chain.reason + ' Nothing was signed.');
|
|
95
|
-
|
|
97
|
+
// Rent is SOL by nature, not by choice: the operator allowed the token, and
|
|
98
|
+
// the account that holds it is paid for in the chain's coin, so assets.allow
|
|
99
|
+
// does not have to list native for the rent line. native_budget counts it.
|
|
100
|
+
const asset = input.method === 'rent' ? { ok: true } : assetAllowed(p, input.chainId, input.asset);
|
|
96
101
|
if (!asset.ok)
|
|
97
102
|
throw new Error(asset.reason + ' Nothing was signed.');
|
|
98
103
|
const to = recipientAllowed(p, input.to);
|
|
@@ -105,7 +110,7 @@ function reserveSpend(input) {
|
|
|
105
110
|
/** The same checks with nothing recorded, for quotes and simulations. */
|
|
106
111
|
function spendVerdict(input) {
|
|
107
112
|
const p = POL();
|
|
108
|
-
for (const r of [chainAllowed(p, input.chainId), assetAllowed(p, input.chainId, input.asset), recipientAllowed(p, input.to)])
|
|
113
|
+
for (const r of [chainAllowed(p, input.chainId), input.method === 'rent' ? { ok: true } : assetAllowed(p, input.chainId, input.asset), recipientAllowed(p, input.to)])
|
|
109
114
|
if (!r.ok)
|
|
110
115
|
return r;
|
|
111
116
|
const limits = limitsFor(input.host);
|
|
@@ -129,8 +134,43 @@ const SPEND_LAYOUTS = {
|
|
|
129
134
|
'39509351': { kind: 'approval', what: 'increaseAllowance', toIndex: 0, amountIndex: 1 }, // increaseAllowance(address,uint256)
|
|
130
135
|
cae9ca51: { kind: 'approval', what: 'approveAndCall', toIndex: 0, amountIndex: 1 }, // approveAndCall(address,uint256,bytes)
|
|
131
136
|
'87517c45': { kind: 'approval', what: 'Permit2 approve', toIndex: 1, amountIndex: 2, tokenIndex: 0, permit2Only: true }, // Permit2.approve(address,address,uint160,uint48)
|
|
137
|
+
// ERC-721 / ERC-1155 movers and operator grants (reported privately
|
|
138
|
+
// 2026-10-26, 1.14.8): none of these was decoded, so an NFT approval or
|
|
139
|
+
// transfer fell through to 'contract' with value 0 and reached the signer
|
|
140
|
+
// under a policy with no budget line, no ledger line and no recipient check.
|
|
141
|
+
// An operator grant is an unbounded approval by construction (`operator`;
|
|
142
|
+
// amountIndex -1 means there is no amount word, otherwise the bool at that
|
|
143
|
+
// word says grant or revoke). A transfer moves a token no budget can price
|
|
144
|
+
// (`nft`; amountIndex -1 means one token), so under a dollar budget it is an
|
|
145
|
+
// unpriced spend, refused unless unpriced_spends allows it, and its
|
|
146
|
+
// recipient is checked either way.
|
|
147
|
+
a22cb465: { kind: 'approval', what: 'setApprovalForAll', toIndex: 0, amountIndex: 1, nft: true, operator: true }, // setApprovalForAll(address,bool)
|
|
148
|
+
'959b8c3f': { kind: 'approval', what: 'ERC-777 authorizeOperator', toIndex: 0, amountIndex: -1, operator: true }, // authorizeOperator(address)
|
|
149
|
+
'42842e0e': { kind: 'token', what: 'ERC-721 safeTransferFrom', toIndex: 1, amountIndex: -1, nft: true }, // safeTransferFrom(address,address,uint256)
|
|
150
|
+
b88d4fde: { kind: 'token', what: 'ERC-721 safeTransferFrom', toIndex: 1, amountIndex: -1, nft: true }, // safeTransferFrom(address,address,uint256,bytes)
|
|
151
|
+
f242432a: { kind: 'token', what: 'ERC-1155 safeTransferFrom', toIndex: 1, amountIndex: 3, nft: true }, // safeTransferFrom(address,address,uint256,uint256,bytes)
|
|
152
|
+
'2eb2c2d6': { kind: 'token', what: 'ERC-1155 safeBatchTransferFrom', toIndex: 1, amountIndex: 3, nft: true, batch: true }, // safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)
|
|
132
153
|
};
|
|
133
154
|
const UNBOUNDED_APPROVAL = 1n << 128n;
|
|
155
|
+
const UINT256_MAX = (1n << 256n) - 1n;
|
|
156
|
+
/**
|
|
157
|
+
* The sum of the `amounts` array of an ERC-1155 safeBatchTransferFrom, read
|
|
158
|
+
* through the caller's word reader so a short word refuses the whole call.
|
|
159
|
+
* `offsetWord` holds the array's byte offset from the start of the arguments.
|
|
160
|
+
*/
|
|
161
|
+
function batchTotal(wordAt, offsetWord) {
|
|
162
|
+
const offset = BigInt('0x' + wordAt(offsetWord));
|
|
163
|
+
if (offset % 32n !== 0n || offset > 1n << 20n)
|
|
164
|
+
throw new Error('policy: calldata for ERC-1155 safeBatchTransferFrom has a malformed amounts offset; refused rather than treated as a plain contract call. Nothing was signed.');
|
|
165
|
+
const base = Number(offset / 32n);
|
|
166
|
+
const count = BigInt('0x' + wordAt(base));
|
|
167
|
+
if (count > 4096n)
|
|
168
|
+
throw new Error('policy: calldata for ERC-1155 safeBatchTransferFrom names more than 4096 amounts; refused. Nothing was signed.');
|
|
169
|
+
let total = 0n;
|
|
170
|
+
for (let i = 0; i < Number(count); i++)
|
|
171
|
+
total += BigInt('0x' + wordAt(base + 1 + i));
|
|
172
|
+
return total;
|
|
173
|
+
}
|
|
134
174
|
async function describeSend(body, chainId) {
|
|
135
175
|
if (body.token_mint) {
|
|
136
176
|
const mint = String(body.token_mint);
|
|
@@ -141,7 +181,19 @@ async function describeSend(body, chainId) {
|
|
|
141
181
|
// reached the custodian (reported privately 2026-10-24, 1.14.5). The chain
|
|
142
182
|
// would have refused the TransferChecked, but the ledger was already wrong.
|
|
143
183
|
const given = Number.isInteger(d) && d >= 0 && d <= 36 ? d : undefined;
|
|
144
|
-
|
|
184
|
+
const to = String(body.to || '');
|
|
185
|
+
const decimals = await verifiedDecimals(chainId, mint, given);
|
|
186
|
+
// In local mode this process is the payer: when the recipient has no token
|
|
187
|
+
// account yet, the send creates one and the rent is native value leaving
|
|
188
|
+
// the wallet, reserved here from the same reading the signer will make.
|
|
189
|
+
// The hosted custodian pays its own rent and bounds it on its side.
|
|
190
|
+
let carried;
|
|
191
|
+
if (isSolanaLocalMode() && to) {
|
|
192
|
+
const rent = await splAccountRent(mint, to, chainId);
|
|
193
|
+
if (rent > 0n)
|
|
194
|
+
carried = { raw: rent.toString(), to, rent: true };
|
|
195
|
+
}
|
|
196
|
+
return { kind: 'spl', to, asset: mint, raw: String(body.value ?? '0'), decimals, ...(carried ? { carried } : {}) };
|
|
145
197
|
}
|
|
146
198
|
const data = String(body.data || '');
|
|
147
199
|
const to = String(body.to || '');
|
|
@@ -160,14 +212,27 @@ async function describeSend(body, chainId) {
|
|
|
160
212
|
};
|
|
161
213
|
const token = layout.tokenIndex === undefined ? to : '0x' + wordAt(layout.tokenIndex).slice(24);
|
|
162
214
|
const counterparty = '0x' + wordAt(layout.toIndex).slice(24);
|
|
163
|
-
const amount = BigInt('0x' + wordAt(layout.amountIndex));
|
|
164
215
|
// The call's own value does not vanish because the calldata was
|
|
165
216
|
// recognised: the contract keeps whatever rides along. It is a second
|
|
166
217
|
// spend, native, to the contract (reported privately 2026-10-23, 1.14.4).
|
|
167
218
|
const carried = BigInt(value) > 0n ? { raw: value, to } : undefined;
|
|
219
|
+
if (layout.operator) {
|
|
220
|
+
// setApprovalForAll(operator, false) takes a grant away and moves
|
|
221
|
+
// nothing: a zero-value call to the collection, reserving nothing.
|
|
222
|
+
const granted = layout.amountIndex < 0 || BigInt('0x' + wordAt(layout.amountIndex)) !== 0n;
|
|
223
|
+
if (!granted)
|
|
224
|
+
return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
|
|
225
|
+
return { kind: 'approval', to: counterparty, asset: token, raw: UINT256_MAX.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
|
|
226
|
+
}
|
|
227
|
+
const amount = layout.amountIndex < 0 ? 1n : layout.batch ? batchTotal(wordAt, layout.amountIndex) : BigInt('0x' + wordAt(layout.amountIndex));
|
|
168
228
|
if (layout.kind === 'approval' && amount >= UNBOUNDED_APPROVAL) {
|
|
169
229
|
return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
|
|
170
230
|
}
|
|
231
|
+
if (layout.nft) {
|
|
232
|
+
// A collection has no decimals and no price: whole tokens, unpriced,
|
|
233
|
+
// to the recipient, recorded under the collection's address.
|
|
234
|
+
return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals: 0, ...(carried ? { carried } : {}) };
|
|
235
|
+
}
|
|
171
236
|
// Decimals matter here only to price a registry stablecoin (resolved
|
|
172
237
|
// offline) and to print the ledger amount. A token the registry and the
|
|
173
238
|
// node cannot place is unpriced whatever its decimals, so a failed lookup
|
|
@@ -221,12 +286,27 @@ function assertSendTarget(d, chainId) {
|
|
|
221
286
|
* recipients.allow and .deny bind who may be allowed to pull; the native
|
|
222
287
|
* value of a contract call is a spend to that contract; the native value
|
|
223
288
|
* carried on a token or approval call is a second spend, native, to the
|
|
224
|
-
* called contract (1.14.4)
|
|
289
|
+
* called contract (1.14.4); the rent a local SPL send pays for the recipient's
|
|
290
|
+
* token account is a native spend to the recipient, method 'rent' (1.14.7).
|
|
291
|
+
* Throws for an unbounded approval: under a policy always, and otherwise
|
|
292
|
+
* unless the operator allowed it.
|
|
225
293
|
*/
|
|
294
|
+
const unlimitedApprovalsAllowed = () => !POLICY.path && process.env.AGENTWALLET_ALLOW_UNLIMITED_APPROVAL === '1';
|
|
295
|
+
const unlimitedApprovalsReport = () => POLICY.path
|
|
296
|
+
? 'refused (a spend policy is set; no budget can count an unlimited allowance)'
|
|
297
|
+
: unlimitedApprovalsAllowed() ? 'allowed by the operator (AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1)' : 'refused by default (the operator can set AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set)';
|
|
226
298
|
function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
|
|
227
|
-
if (d.kind === 'approval' && d.unbounded
|
|
228
|
-
|
|
229
|
-
|
|
299
|
+
if (d.kind === 'approval' && d.unbounded) {
|
|
300
|
+
// The README promised an unlimited approval was "refused outright" while
|
|
301
|
+
// the code refused it only under a policy (reported privately 2026-10-26,
|
|
302
|
+
// 1.14.8). Now the default everywhere; only the operator opens it, and
|
|
303
|
+
// only where no policy is set.
|
|
304
|
+
if (POLICY.path)
|
|
305
|
+
throw new Error(`policy: an unlimited approval of ${assetLabel(chainId, d.asset)} to ${d.to} cannot be counted against any budget, and under a spend policy every approval is a bounded spend. ` +
|
|
306
|
+
`Approve an exact amount instead. Nothing was signed.`);
|
|
307
|
+
if (!unlimitedApprovalsAllowed())
|
|
308
|
+
throw new Error(`an unlimited approval of ${assetLabel(chainId, d.asset)} to ${d.to} is refused by default: it is a standing grant over the whole balance. ` +
|
|
309
|
+
`Approve an exact amount instead. Only the operator can allow unlimited approvals (AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1, never under a spend policy). Nothing was signed.`);
|
|
230
310
|
}
|
|
231
311
|
const base = { tool, mode: anyLocalMode() ? 'local' : 'hosted', wallet: anyLocalMode() ? 'local' : wallet, chainId, host: ctx.host ?? null, resource: ctx.resource ?? null };
|
|
232
312
|
const out = [];
|
|
@@ -238,7 +318,7 @@ function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
|
|
|
238
318
|
});
|
|
239
319
|
}
|
|
240
320
|
if (d.carried) {
|
|
241
|
-
out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: 'native' });
|
|
321
|
+
out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: d.carried.rent ? 'rent' : 'native' });
|
|
242
322
|
}
|
|
243
323
|
return out;
|
|
244
324
|
}
|
|
@@ -1049,7 +1129,7 @@ function validateGuardEnv() {
|
|
|
1049
1129
|
throw new Error(`Unknown environment variable ${k}. AgentWallet refuses to start on an unrecognised AGENTWALLET_* name so that a misspelled cap cannot run uncapped. Known names: ${[...KNOWN_ENV].join(', ')}, AGENTWALLET_RPC_<chainId>, AGENTWALLET_SOLANA_RPC_<chainId>.`);
|
|
1050
1130
|
}
|
|
1051
1131
|
}
|
|
1052
|
-
const KNOWN_ENV = new Set(["AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS", "AGENTWALLET_API_URL", "AGENTWALLET_APPROVALS", "AGENTWALLET_AUTOPAY_ASSETS", "AGENTWALLET_CHAIN_ID", "AGENTWALLET_DASHBOARD_URL", "AGENTWALLET_KEYFILE", "AGENTWALLET_KEYFILE_PASSPHRASE", "AGENTWALLET_KEYFILE_PASSPHRASE_FILE", "AGENTWALLET_LEDGER", "AGENTWALLET_LEGACY_PAYWALL_ORIGINS", "AGENTWALLET_MAX_AUTOPAY", "AGENTWALLET_MAX_AUTOPAY_PER_HOUR", "AGENTWALLET_MAX_FEE_NATIVE", "AGENTWALLET_MAX_TX_NATIVE", "AGENTWALLET_MAX_TX_SOL", "AGENTWALLET_MAX_TX_TOKEN", "AGENTWALLET_PASS", "AGENTWALLET_PERMIT2_AUTO_APPROVE", "AGENTWALLET_POLICY", "AGENTWALLET_PRIVATE_KEY", "AGENTWALLET_RPC_URL", "AGENTWALLET_SOLANA_KEY", "AGENTWALLET_SOLANA_KEYFILE", "AGENTWALLET_SOLANA_RPC", "AGENTWALLET_TOKEN_DECIMALS", "AGENTWALLET_TOKEN_RISK", "AGENTWALLET_USER", "AGENTWALLET_WALLET_ID", "AGENTWALLET_WEBHOOK_SECRET", "AGENTWALLET_WEBHOOK_URL", "AGENTWALLET_X402_MAX_TIMEOUT"]);
|
|
1132
|
+
const KNOWN_ENV = new Set(["AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS", "AGENTWALLET_ALLOW_UNLIMITED_APPROVAL", "AGENTWALLET_API_URL", "AGENTWALLET_APPROVALS", "AGENTWALLET_AUTOPAY_ASSETS", "AGENTWALLET_CHAIN_ID", "AGENTWALLET_DASHBOARD_URL", "AGENTWALLET_KEYFILE", "AGENTWALLET_KEYFILE_PASSPHRASE", "AGENTWALLET_KEYFILE_PASSPHRASE_FILE", "AGENTWALLET_LEDGER", "AGENTWALLET_LEGACY_PAYWALL_ORIGINS", "AGENTWALLET_MAX_AUTOPAY", "AGENTWALLET_MAX_AUTOPAY_PER_HOUR", "AGENTWALLET_MAX_FEE_NATIVE", "AGENTWALLET_MAX_TX_NATIVE", "AGENTWALLET_MAX_TX_SOL", "AGENTWALLET_MAX_TX_TOKEN", "AGENTWALLET_PASS", "AGENTWALLET_PERMIT2_AUTO_APPROVE", "AGENTWALLET_POLICY", "AGENTWALLET_PRIVATE_KEY", "AGENTWALLET_RPC_URL", "AGENTWALLET_SOLANA_KEY", "AGENTWALLET_SOLANA_KEYFILE", "AGENTWALLET_SOLANA_RPC", "AGENTWALLET_TOKEN_DECIMALS", "AGENTWALLET_TOKEN_RISK", "AGENTWALLET_USER", "AGENTWALLET_WALLET_ID", "AGENTWALLET_WEBHOOK_SECRET", "AGENTWALLET_WEBHOOK_URL", "AGENTWALLET_X402_MAX_TIMEOUT"]);
|
|
1053
1133
|
/**
|
|
1054
1134
|
* Register a tool whose arguments are strict: an unknown key (max_payemnt for
|
|
1055
1135
|
* max_payment) is an error that names it, not a silently dropped guard. The
|
|
@@ -1175,7 +1255,7 @@ function resolveChainId(network) {
|
|
|
1175
1255
|
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');
|
|
1176
1256
|
const server = new McpServer({
|
|
1177
1257
|
name: 'agentwallet',
|
|
1178
|
-
version: '1.14.
|
|
1258
|
+
version: '1.14.8',
|
|
1179
1259
|
}, {
|
|
1180
1260
|
instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
|
|
1181
1261
|
? '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 environment caps (AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL per transaction, AGENTWALLET_MAX_AUTOPAY and AGENTWALLET_AUTOPAY_ASSETS for x402, AGENTWALLET_X402_MAX_TIMEOUT for authorization windows). There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
|
|
@@ -1520,12 +1600,12 @@ tool('call_contract', 'Execute a read-only call against a smart contract (eth_ca
|
|
|
1520
1600
|
// ─── Tool: approve_token ────────────────────────────────────────
|
|
1521
1601
|
tool('approve_token', 'Approve a spender contract to transfer ERC-20 tokens on your behalf. ' +
|
|
1522
1602
|
'Required before interacting with any DeFi protocol (DEXs, lending, etc.). ' +
|
|
1523
|
-
'
|
|
1524
|
-
'Under a spend policy an approval is a spend: it counts against the budgets
|
|
1603
|
+
'Specify an exact amount. "max" (an unlimited allowance) is refused by default and always under a spend policy; only the operator can allow it, with AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set. ' +
|
|
1604
|
+
'Under a spend policy an approval is a spend: it counts against the budgets and the spender is checked as a recipient.', {
|
|
1525
1605
|
wallet_id: z.number().int().describe('Wallet ID'),
|
|
1526
1606
|
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
1527
1607
|
spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address to approve as spender'),
|
|
1528
|
-
amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000")
|
|
1608
|
+
amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"); "max" means unlimited and is refused unless the operator allowed it'),
|
|
1529
1609
|
chain_id: z.number().int().describe('Chain ID'),
|
|
1530
1610
|
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'),
|
|
1531
1611
|
}, async ({ wallet_id, token, spender, amount, chain_id, decimals: givenDecimals }) => {
|
|
@@ -2425,8 +2505,8 @@ tool('check_approval', 'Status of a payment approval created when pay_x402 excee
|
|
|
2425
2505
|
'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' })));
|
|
2426
2506
|
// ─── Tool: approve_permit2 ──────────────────────────────────────
|
|
2427
2507
|
tool('approve_permit2', 'One-time ERC-20 approval of a token to the Permit2 contract, needed before paying x402 "upto" endpoints with that token. ' +
|
|
2428
|
-
'A normal on-chain transaction (needs gas).
|
|
2429
|
-
'
|
|
2508
|
+
'A normal on-chain transaction (needs gas). Pass amount to grant a bounded allowance; with no amount it asks for the maximum, which is refused by default, under AGENTWALLET_MAX_TX_TOKEN, and always under a spend policy (where the allowance counts against the budgets). ' +
|
|
2509
|
+
'Only the operator can allow a maximum approval, with AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set. ' +
|
|
2430
2510
|
'Permit2 itself only moves what each signed authorization allows.', {
|
|
2431
2511
|
wallet_id: z.number().int().describe('Wallet ID'),
|
|
2432
2512
|
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token address (e.g. USDC on Base)'),
|
|
@@ -2890,6 +2970,7 @@ tool('wallet_mode', 'Report whether this server is signing locally (self-custody
|
|
|
2890
2970
|
api_base: redactUrl(API_BASE),
|
|
2891
2971
|
spend_policy: POLICY.path ? { path: POLICY.path, sha256: POLICY.hash, name: POLICY.policy.name ?? null, ledger: POLICY.ledgerPath, see: 'spend_policy' } : 'none (AGENTWALLET_POLICY not set)',
|
|
2892
2972
|
client_guards: {
|
|
2973
|
+
unlimited_approvals: unlimitedApprovalsReport(),
|
|
2893
2974
|
autopay_cap_usd: autopayEnvCap(),
|
|
2894
2975
|
internal_autopay_armed: Boolean(X402_WALLET_ID),
|
|
2895
2976
|
max_autopay_per_hour: AUTOPAY_PER_HOUR,
|
|
@@ -2916,6 +2997,7 @@ tool('wallet_mode', 'Report whether this server is signing locally (self-custody
|
|
|
2916
2997
|
x402_max_timeout_seconds: maxAuthWindowSeconds(),
|
|
2917
2998
|
per_tx_cap_token: (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim() || 'not set (ERC-20 and SPL transfers are uncapped)',
|
|
2918
2999
|
allow_unknown_token_calls: process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1',
|
|
3000
|
+
unlimited_approvals: unlimitedApprovalsReport(),
|
|
2919
3001
|
token_decimals_pins: parseDecimalPins(process.env.AGENTWALLET_TOKEN_DECIMALS).size,
|
|
2920
3002
|
token_risk_lookup: process.env.AGENTWALLET_TOKEN_RISK !== '0',
|
|
2921
3003
|
permit2_auto_approve: process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1',
|
package/build/ledger.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type Spent } from './policy.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* 'approval' is an ERC-20 allowance granted (approve, increaseAllowance, Permit2.approve): a spend the spender executes later, counted when granted.
|
|
4
|
+
* 'rent' is the SOL an SPL send pays to create the recipient's token account (1.14.7): native, to the recipient, who owns the account and can close it for the lamports.
|
|
5
|
+
*/
|
|
6
|
+
export type SpendMethod = 'eip3009' | 'permit2' | 'transfer' | 'native' | 'spl' | 'autopay' | 'approval' | 'rent';
|
|
4
7
|
export interface SpendEntry {
|
|
5
8
|
seq: number;
|
|
6
9
|
ts: string;
|
package/build/local-solana.d.ts
CHANGED
|
@@ -76,6 +76,16 @@ export declare function localSolTransfer(to: string, lamports: string, chainId?:
|
|
|
76
76
|
* not exist, using the idempotent instruction so a race cannot fail the send.
|
|
77
77
|
*/
|
|
78
78
|
export declare function localSplTransfer(mintStr: string, to: string, rawAmount: string, decimals: number, chainId?: number): Promise<LocalSolResult>;
|
|
79
|
+
/**
|
|
80
|
+
* The SOL an SPL send to `to` spends on creating the recipient's token
|
|
81
|
+
* account, in lamports; 0n when the account exists. The recipient owns that
|
|
82
|
+
* account and can close it to keep the lamports, so it is native value leaving
|
|
83
|
+
* the wallet and the spend policy reserves it against native_budget before the
|
|
84
|
+
* send is built (reported privately 2026-10-26: it was checked against the
|
|
85
|
+
* per-transaction SOL cap only, and fresh recipients took SOL past the daily
|
|
86
|
+
* budget while the ledger held token lines alone).
|
|
87
|
+
*/
|
|
88
|
+
export declare function splAccountRent(mintStr: string, to: string, chainId?: number): Promise<bigint>;
|
|
79
89
|
export declare function solanaWalletRecord(): {
|
|
80
90
|
id: string;
|
|
81
91
|
address: string;
|
package/build/local-solana.js
CHANGED
|
@@ -448,31 +448,15 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
|
|
|
448
448
|
const mint = new PublicKey(mintStr);
|
|
449
449
|
const recipient = new PublicKey(to);
|
|
450
450
|
await assertRecipientIsWallet(conn, recipient);
|
|
451
|
-
const { tokenProgram,
|
|
451
|
+
const { tokenProgram, destAta, rent } = await destinationAccount(conn, mint, recipient);
|
|
452
452
|
const sourceAta = associatedTokenAddress(payer.publicKey, mint, tokenProgram);
|
|
453
|
-
const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
|
|
454
453
|
const tx = new Transaction();
|
|
455
|
-
|
|
456
|
-
// The associated-account program creates into an address that already holds
|
|
457
|
-
// plain SOL (system-owned, no data): it tops up the rent, allocates and
|
|
458
|
-
// assigns. Only an address holding something else cannot become the token
|
|
459
|
-
// account (2026-10-04 round 2; the 1.13.5 refusal of any occupant let anyone
|
|
460
|
-
// block a recipient for the price of the rent-exempt minimum).
|
|
461
|
-
const prefunded = destInfo !== null && destInfo.owner.equals(SystemProgram.programId) && destInfo.data.length === 0;
|
|
462
|
-
if (destInfo && !destInfo.owner.equals(tokenProgram) && !prefunded) {
|
|
463
|
-
throw new Error(`Refusing to send: the recipient's token account address ${destAta.toBase58()} is occupied by an account owned by ${destInfo.owner.toBase58()}, so no token account can be created there.`);
|
|
464
|
-
}
|
|
465
|
-
if (!destInfo || prefunded) {
|
|
454
|
+
if (rent > 0n) {
|
|
466
455
|
// Creating the recipient's token account costs the payer rent in SOL, an
|
|
467
456
|
// outflow the SOL cap must see (0.002 SOL a time adds up over many sends).
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
// node says already sits at the address is not credited: both are numbers a
|
|
472
|
-
// lying RPC used to zero so the create went out under the cap (round 4).
|
|
473
|
-
const claimed = Number(await conn.getMinimumBalanceForRentExemption(ataSize));
|
|
474
|
-
const rent = Math.max(Number.isFinite(claimed) ? Math.trunc(claimed) : 0, (128 + ataSize) * 6960);
|
|
475
|
-
assertWithinSolCap(BigInt(rent));
|
|
457
|
+
// The policy reserved the same figure against native_budget before this
|
|
458
|
+
// call (1.14.7); the cap is the per-transaction floor under it.
|
|
459
|
+
assertWithinSolCap(rent);
|
|
476
460
|
tx.add(createAtaIdempotentIx(payer.publicKey, destAta, recipient, mint, tokenProgram));
|
|
477
461
|
}
|
|
478
462
|
tx.add(transferCheckedIx(tokenProgram, sourceAta, mint, destAta, payer.publicKey, BigInt(rawAmount), decimals));
|
|
@@ -487,6 +471,52 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
|
|
|
487
471
|
signed_locally: true,
|
|
488
472
|
};
|
|
489
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* The recipient's associated account for a mint, and the rent the payer funds
|
|
476
|
+
* when it has to be created: 0 when it already exists. One reading shared by
|
|
477
|
+
* the send and by the policy's reservation, so the ledger line and the
|
|
478
|
+
* instruction carry the same figure.
|
|
479
|
+
*
|
|
480
|
+
* The associated-account program creates into an address that already holds
|
|
481
|
+
* plain SOL (system-owned, no data): it tops up the rent, allocates and
|
|
482
|
+
* assigns. Only an address holding something else cannot become the token
|
|
483
|
+
* account (2026-10-04 round 2; the 1.13.5 refusal of any occupant let anyone
|
|
484
|
+
* block a recipient for the price of the rent-exempt minimum).
|
|
485
|
+
*
|
|
486
|
+
* Rent follows the account's real size, which for Token-2022 is above 165;
|
|
487
|
+
* SOL already sitting at the address reduces what the payer tops up. The
|
|
488
|
+
* node's rent figure is floored at the cluster constant and the SOL the node
|
|
489
|
+
* says already sits at the address is not credited: both are numbers a lying
|
|
490
|
+
* RPC used to zero so the create went out under the cap (round 4).
|
|
491
|
+
*/
|
|
492
|
+
async function destinationAccount(conn, mint, recipient) {
|
|
493
|
+
const { tokenProgram, ataSize } = await mintLayout(conn, mint);
|
|
494
|
+
const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
|
|
495
|
+
const destInfo = await conn.getAccountInfo(destAta);
|
|
496
|
+
const prefunded = destInfo !== null && destInfo.owner.equals(SystemProgram.programId) && destInfo.data.length === 0;
|
|
497
|
+
if (destInfo && !destInfo.owner.equals(tokenProgram) && !prefunded) {
|
|
498
|
+
throw new Error(`Refusing to send: the recipient's token account address ${destAta.toBase58()} is occupied by an account owned by ${destInfo.owner.toBase58()}, so no token account can be created there.`);
|
|
499
|
+
}
|
|
500
|
+
if (destInfo && !prefunded)
|
|
501
|
+
return { tokenProgram, destAta, rent: 0n };
|
|
502
|
+
const claimed = Number(await conn.getMinimumBalanceForRentExemption(ataSize));
|
|
503
|
+
const rent = Math.max(Number.isFinite(claimed) ? Math.trunc(claimed) : 0, (128 + ataSize) * 6960);
|
|
504
|
+
return { tokenProgram, destAta, rent: BigInt(rent) };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The SOL an SPL send to `to` spends on creating the recipient's token
|
|
508
|
+
* account, in lamports; 0n when the account exists. The recipient owns that
|
|
509
|
+
* account and can close it to keep the lamports, so it is native value leaving
|
|
510
|
+
* the wallet and the spend policy reserves it against native_budget before the
|
|
511
|
+
* send is built (reported privately 2026-10-26: it was checked against the
|
|
512
|
+
* per-transaction SOL cap only, and fresh recipients took SOL past the daily
|
|
513
|
+
* budget while the ledger held token lines alone).
|
|
514
|
+
*/
|
|
515
|
+
export async function splAccountRent(mintStr, to, chainId = 900) {
|
|
516
|
+
const conn = connection(chainId);
|
|
517
|
+
await assertCluster(conn, chainId);
|
|
518
|
+
return (await destinationAccount(conn, new PublicKey(mintStr), new PublicKey(to))).rent;
|
|
519
|
+
}
|
|
490
520
|
const TOKEN_ACCOUNT_SIZE = 165;
|
|
491
521
|
const MINT_SIZE = 82;
|
|
492
522
|
/** Token-2022 pads a mint to the token-account size and tags the next byte: 1 = mint, 2 = token account. */
|
package/build/policy.d.ts
CHANGED
|
@@ -2,197 +2,50 @@ import { z } from 'zod';
|
|
|
2
2
|
export declare const PolicySchema: z.ZodObject<{
|
|
3
3
|
version: z.ZodLiteral<1>;
|
|
4
4
|
name: z.ZodOptional<z.ZodString>;
|
|
5
|
-
/** Rolling budgets in stablecoin (dollar) units across every chain: x402 payments and stablecoin transfers. */
|
|
6
5
|
budget: z.ZodOptional<z.ZodObject<{
|
|
7
6
|
daily: z.ZodOptional<z.ZodString>;
|
|
8
7
|
monthly: z.ZodOptional<z.ZodString>;
|
|
9
8
|
total: z.ZodOptional<z.ZodString>;
|
|
10
|
-
},
|
|
11
|
-
total?: string | undefined;
|
|
12
|
-
daily?: string | undefined;
|
|
13
|
-
monthly?: string | undefined;
|
|
14
|
-
}, {
|
|
15
|
-
total?: string | undefined;
|
|
16
|
-
daily?: string | undefined;
|
|
17
|
-
monthly?: string | undefined;
|
|
18
|
-
}>>;
|
|
19
|
-
/** Rolling budgets for the chain's own coin, keyed by chain id, in that coin's units. */
|
|
9
|
+
}, z.core.$strict>>;
|
|
20
10
|
native_budget: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
21
11
|
daily: z.ZodOptional<z.ZodString>;
|
|
22
12
|
monthly: z.ZodOptional<z.ZodString>;
|
|
23
13
|
total: z.ZodOptional<z.ZodString>;
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}, {
|
|
29
|
-
total?: string | undefined;
|
|
30
|
-
daily?: string | undefined;
|
|
31
|
-
monthly?: string | undefined;
|
|
32
|
-
}>>>;
|
|
33
|
-
/** What to do with a spend the budget cannot price (a token outside the registry). Default refuse when a budget is set. */
|
|
34
|
-
unpriced_spends: z.ZodOptional<z.ZodEnum<["refuse", "allow"]>>;
|
|
35
|
-
hosts: z.ZodOptional<z.ZodObject<{
|
|
36
|
-
allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
37
|
-
deny: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
38
|
-
}, "strict", z.ZodTypeAny, {
|
|
39
|
-
allow?: string[] | undefined;
|
|
40
|
-
deny?: string[] | undefined;
|
|
41
|
-
}, {
|
|
42
|
-
allow?: string[] | undefined;
|
|
43
|
-
deny?: string[] | undefined;
|
|
14
|
+
}, z.core.$strict>>>;
|
|
15
|
+
unpriced_spends: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
allow: "allow";
|
|
17
|
+
refuse: "refuse";
|
|
44
18
|
}>>;
|
|
45
|
-
|
|
19
|
+
hosts: z.ZodOptional<z.ZodObject<{
|
|
20
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
|
+
deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
22
|
+
}, z.core.$strict>>;
|
|
46
23
|
per_host: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
47
24
|
daily: z.ZodOptional<z.ZodString>;
|
|
48
25
|
monthly: z.ZodOptional<z.ZodString>;
|
|
49
26
|
per_payment: z.ZodOptional<z.ZodString>;
|
|
50
|
-
},
|
|
51
|
-
daily?: string | undefined;
|
|
52
|
-
monthly?: string | undefined;
|
|
53
|
-
per_payment?: string | undefined;
|
|
54
|
-
}, {
|
|
55
|
-
daily?: string | undefined;
|
|
56
|
-
monthly?: string | undefined;
|
|
57
|
-
per_payment?: string | undefined;
|
|
58
|
-
}>>>;
|
|
27
|
+
}, z.core.$strict>>>;
|
|
59
28
|
recipients: z.ZodOptional<z.ZodObject<{
|
|
60
|
-
allow: z.ZodOptional<z.ZodArray<z.ZodString
|
|
61
|
-
deny: z.ZodOptional<z.ZodArray<z.ZodString
|
|
62
|
-
},
|
|
63
|
-
allow?: string[] | undefined;
|
|
64
|
-
deny?: string[] | undefined;
|
|
65
|
-
}, {
|
|
66
|
-
allow?: string[] | undefined;
|
|
67
|
-
deny?: string[] | undefined;
|
|
68
|
-
}>>;
|
|
29
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strict>>;
|
|
69
32
|
chains: z.ZodOptional<z.ZodObject<{
|
|
70
|
-
allow: z.ZodOptional<z.ZodArray<z.ZodNumber
|
|
71
|
-
},
|
|
72
|
-
allow?: number[] | undefined;
|
|
73
|
-
}, {
|
|
74
|
-
allow?: number[] | undefined;
|
|
75
|
-
}>>;
|
|
33
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
34
|
+
}, z.core.$strict>>;
|
|
76
35
|
assets: z.ZodOptional<z.ZodObject<{
|
|
77
|
-
allow: z.ZodOptional<z.ZodArray<z.ZodString
|
|
78
|
-
},
|
|
79
|
-
allow?: string[] | undefined;
|
|
80
|
-
}, {
|
|
81
|
-
allow?: string[] | undefined;
|
|
82
|
-
}>>;
|
|
83
|
-
/** Ask the human (MCP elicitation) above `above`; in local mode an accepted ask may admit up to `max`, above the per-payment cap. */
|
|
36
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
37
|
+
}, z.core.$strict>>;
|
|
84
38
|
approval: z.ZodOptional<z.ZodObject<{
|
|
85
39
|
above: z.ZodOptional<z.ZodString>;
|
|
86
40
|
max: z.ZodOptional<z.ZodString>;
|
|
87
41
|
timeout_seconds: z.ZodOptional<z.ZodNumber>;
|
|
88
|
-
},
|
|
89
|
-
max?: string | undefined;
|
|
90
|
-
above?: string | undefined;
|
|
91
|
-
timeout_seconds?: number | undefined;
|
|
92
|
-
}, {
|
|
93
|
-
max?: string | undefined;
|
|
94
|
-
above?: string | undefined;
|
|
95
|
-
timeout_seconds?: number | undefined;
|
|
96
|
-
}>>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
97
43
|
tools: z.ZodOptional<z.ZodObject<{
|
|
98
|
-
allow: z.ZodOptional<z.ZodArray<z.ZodString
|
|
99
|
-
deny: z.ZodOptional<z.ZodArray<z.ZodString
|
|
100
|
-
},
|
|
101
|
-
allow?: string[] | undefined;
|
|
102
|
-
deny?: string[] | undefined;
|
|
103
|
-
}, {
|
|
104
|
-
allow?: string[] | undefined;
|
|
105
|
-
deny?: string[] | undefined;
|
|
106
|
-
}>>;
|
|
107
|
-
/** Ledger path; relative paths resolve against the policy file's directory. */
|
|
44
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
45
|
+
deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
}, z.core.$strict>>;
|
|
108
47
|
ledger: z.ZodOptional<z.ZodString>;
|
|
109
|
-
},
|
|
110
|
-
version: 1;
|
|
111
|
-
name?: string | undefined;
|
|
112
|
-
approval?: {
|
|
113
|
-
max?: string | undefined;
|
|
114
|
-
above?: string | undefined;
|
|
115
|
-
timeout_seconds?: number | undefined;
|
|
116
|
-
} | undefined;
|
|
117
|
-
budget?: {
|
|
118
|
-
total?: string | undefined;
|
|
119
|
-
daily?: string | undefined;
|
|
120
|
-
monthly?: string | undefined;
|
|
121
|
-
} | undefined;
|
|
122
|
-
native_budget?: Record<string, {
|
|
123
|
-
total?: string | undefined;
|
|
124
|
-
daily?: string | undefined;
|
|
125
|
-
monthly?: string | undefined;
|
|
126
|
-
}> | undefined;
|
|
127
|
-
unpriced_spends?: "allow" | "refuse" | undefined;
|
|
128
|
-
hosts?: {
|
|
129
|
-
allow?: string[] | undefined;
|
|
130
|
-
deny?: string[] | undefined;
|
|
131
|
-
} | undefined;
|
|
132
|
-
per_host?: Record<string, {
|
|
133
|
-
daily?: string | undefined;
|
|
134
|
-
monthly?: string | undefined;
|
|
135
|
-
per_payment?: string | undefined;
|
|
136
|
-
}> | undefined;
|
|
137
|
-
recipients?: {
|
|
138
|
-
allow?: string[] | undefined;
|
|
139
|
-
deny?: string[] | undefined;
|
|
140
|
-
} | undefined;
|
|
141
|
-
chains?: {
|
|
142
|
-
allow?: number[] | undefined;
|
|
143
|
-
} | undefined;
|
|
144
|
-
assets?: {
|
|
145
|
-
allow?: string[] | undefined;
|
|
146
|
-
} | undefined;
|
|
147
|
-
tools?: {
|
|
148
|
-
allow?: string[] | undefined;
|
|
149
|
-
deny?: string[] | undefined;
|
|
150
|
-
} | undefined;
|
|
151
|
-
ledger?: string | undefined;
|
|
152
|
-
}, {
|
|
153
|
-
version: 1;
|
|
154
|
-
name?: string | undefined;
|
|
155
|
-
approval?: {
|
|
156
|
-
max?: string | undefined;
|
|
157
|
-
above?: string | undefined;
|
|
158
|
-
timeout_seconds?: number | undefined;
|
|
159
|
-
} | undefined;
|
|
160
|
-
budget?: {
|
|
161
|
-
total?: string | undefined;
|
|
162
|
-
daily?: string | undefined;
|
|
163
|
-
monthly?: string | undefined;
|
|
164
|
-
} | undefined;
|
|
165
|
-
native_budget?: Record<string, {
|
|
166
|
-
total?: string | undefined;
|
|
167
|
-
daily?: string | undefined;
|
|
168
|
-
monthly?: string | undefined;
|
|
169
|
-
}> | undefined;
|
|
170
|
-
unpriced_spends?: "allow" | "refuse" | undefined;
|
|
171
|
-
hosts?: {
|
|
172
|
-
allow?: string[] | undefined;
|
|
173
|
-
deny?: string[] | undefined;
|
|
174
|
-
} | undefined;
|
|
175
|
-
per_host?: Record<string, {
|
|
176
|
-
daily?: string | undefined;
|
|
177
|
-
monthly?: string | undefined;
|
|
178
|
-
per_payment?: string | undefined;
|
|
179
|
-
}> | undefined;
|
|
180
|
-
recipients?: {
|
|
181
|
-
allow?: string[] | undefined;
|
|
182
|
-
deny?: string[] | undefined;
|
|
183
|
-
} | undefined;
|
|
184
|
-
chains?: {
|
|
185
|
-
allow?: number[] | undefined;
|
|
186
|
-
} | undefined;
|
|
187
|
-
assets?: {
|
|
188
|
-
allow?: string[] | undefined;
|
|
189
|
-
} | undefined;
|
|
190
|
-
tools?: {
|
|
191
|
-
allow?: string[] | undefined;
|
|
192
|
-
deny?: string[] | undefined;
|
|
193
|
-
} | undefined;
|
|
194
|
-
ledger?: string | undefined;
|
|
195
|
-
}>;
|
|
48
|
+
}, z.core.$strict>;
|
|
196
49
|
export type Policy = z.infer<typeof PolicySchema>;
|
|
197
50
|
export declare const PRECISION = 18;
|
|
198
51
|
/** Decimal string to an integer at 18 places. */
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/hifriendbot/agentwallet-mcp.git"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.14.
|
|
8
|
+
"version": "1.14.8",
|
|
9
9
|
"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.",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "build/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"start": "node build/index.js",
|
|
23
|
-
"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 && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs && node test/spend-policy.test.mjs && node test/policy-path-resolution.test.mjs",
|
|
23
|
+
"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 && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs && node test/spend-policy.test.mjs && node test/policy-path-resolution.test.mjs && node test/server-json-env.test.mjs",
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"node": ">=22.19.0"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
62
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
63
63
|
"@solana/web3.js": "^1.99.0",
|
|
64
64
|
"bs58": "^6.0.0",
|
|
65
|
-
"undici": "^8.
|
|
65
|
+
"undici": "^8.12.3",
|
|
66
66
|
"viem": "^2.56.9",
|
|
67
|
-
"zod": "^
|
|
67
|
+
"zod": "^4.6.7"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/node": "^20.0.0",
|