agentwallet-mcp 1.8.0 → 1.10.0
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 +97 -32
- package/build/index.js +238 -2
- package/build/local-solana.d.ts +58 -0
- package/build/local-solana.js +252 -0
- package/build/local-wallet.d.ts +94 -0
- package/build/local-wallet.js +225 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,36 +1,99 @@
|
|
|
1
1
|
# AgentWallet MCP Server
|
|
2
2
|
|
|
3
|
-
Permissionless wallet infrastructure for AI agents. Create wallets, sign transactions, and broadcast on-chain
|
|
3
|
+
Permissionless wallet infrastructure for AI agents. Create wallets, sign transactions, and broadcast on-chain, on any EVM chain and Solana. Built-in guards. No KYC. The only AI agent wallet that accepts crypto for its own API fees.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Your keys can stay on your machine.** Set one environment variable and every signature happens in your own process. No server sees the key, no company can freeze the wallet, and you can verify that claim by reading `src/local-wallet.ts` or by running the server with the API pointed at a closed port.
|
|
6
|
+
|
|
7
|
+
**No KYC. No KYT. No approval process. No transaction monitoring. No one can block your wallet. Pay with USDC on-chain, no credit card required.**
|
|
8
|
+
|
|
9
|
+
## Two modes
|
|
10
|
+
|
|
11
|
+
| | Local (self-custody) | Hosted (custodial) |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Who holds the key | You, in your own process | Encrypted on AgentWallet servers |
|
|
14
|
+
| Set up | One env var, no account needed | API key |
|
|
15
|
+
| Chains | EVM + Solana | EVM + Solana |
|
|
16
|
+
| Can anyone freeze it | No | Yes, that is what pause is for |
|
|
17
|
+
| Spend guards | `AGENTWALLET_MAX_TX_NATIVE`, `AGENTWALLET_MAX_TX_SOL`, `AGENTWALLET_MAX_AUTOPAY` | Server-side limits, pause, rate limits |
|
|
18
|
+
| Paywalls, usage, billing | Needs an API key too | Included |
|
|
19
|
+
|
|
20
|
+
Run `wallet_mode` at any time and the server will tell you which one you are in, and which address it controls.
|
|
21
|
+
|
|
22
|
+
### Local signing in one variable
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"agentwallet": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "agentwallet-mcp"],
|
|
30
|
+
"env": {
|
|
31
|
+
"AGENTWALLET_PRIVATE_KEY": "0xyour_key",
|
|
32
|
+
"AGENTWALLET_RPC_8453": "https://your-own-rpc",
|
|
33
|
+
"AGENTWALLET_MAX_TX_NATIVE": "0.05"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use `AGENTWALLET_KEYFILE=/path/to/key` instead if you would rather keep the key out of your shell config. The key is read once, never written to disk, never logged, and never included in an error message.
|
|
41
|
+
|
|
42
|
+
`AGENTWALLET_RPC_<chainId>` (or `AGENTWALLET_RPC_URL` for all chains) points at an endpoint you trust. Without it a public RPC is used, and a public RPC can see which addresses you ask about.
|
|
43
|
+
|
|
44
|
+
`AGENTWALLET_MAX_TX_NATIVE` is a per-transaction ceiling in native units. In hosted mode the server enforces limits; in local mode there is no server, so this guard is the only one there is. Set it.
|
|
45
|
+
|
|
46
|
+
### Solana local signing
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
"env": {
|
|
50
|
+
"AGENTWALLET_SOLANA_KEY": "[12,34...]",
|
|
51
|
+
"AGENTWALLET_SOLANA_RPC": "https://your-own-rpc",
|
|
52
|
+
"AGENTWALLET_MAX_TX_SOL": "1"
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Accepts whichever format you already have: a `solana-keygen` id.json array, a base58 secret key as exported by Phantom, or base64. Use `AGENTWALLET_SOLANA_KEYFILE` to point at a file instead. Native SOL and SPL token transfers are both signed locally, and a missing associated token account is created for the recipient automatically.
|
|
57
|
+
|
|
58
|
+
Set either key, or both. They are independent: run EVM locally and Solana hosted, or the reverse.
|
|
59
|
+
|
|
60
|
+
**An operation with no matching local key is refused, never silently routed to the hosted signer.** If you have an EVM key configured and ask for a Solana transfer with no Solana key, the server stops and tells you which variable is missing. Quietly moving funds onto a key you do not hold, while you believe you are in self-custody, is the worst thing this server could do.
|
|
61
|
+
|
|
62
|
+
### About the dependency tree
|
|
63
|
+
|
|
64
|
+
Local signing uses [viem](https://viem.sh) for EVM and `@solana/web3.js` for Solana, plus `bs58` for key parsing. SPL instructions are built by hand rather than with `@solana/spl-token`, because that package pulls in `bigint-buffer`, which carries a high severity buffer overflow advisory. A wallet has no business shipping that to save a dozen lines of instruction encoding.
|
|
65
|
+
|
|
66
|
+
`npm audit` currently reports issues inside `@modelcontextprotocol/sdk`'s HTTP transport dependencies. This server speaks stdio, so that code never loads, and the SDK is not something this package can patch. Run the audit yourself. Publishing a tree you can inspect is the point.
|
|
6
67
|
|
|
7
68
|
<p align="center">
|
|
8
|
-
<img src="assets/demo.svg" alt="AgentWallet demo
|
|
69
|
+
<img src="assets/demo.svg" alt="AgentWallet demo, AI agent pays x402 invoice automatically" width="800">
|
|
9
70
|
</p>
|
|
10
71
|
|
|
11
72
|
## Features
|
|
12
73
|
|
|
13
|
-
- **
|
|
14
|
-
- **EVM + Solana
|
|
15
|
-
- **SOL + SPL tokens
|
|
16
|
-
- **Built-in guards
|
|
17
|
-
- **x402 payments
|
|
18
|
-
- **
|
|
19
|
-
- **Permissionless
|
|
20
|
-
- **30-second setup
|
|
74
|
+
- **31 MCP tools**: create wallets, send transactions, approve tokens, wrap ETH, transfer SPL tokens, pay and accept x402 payments, verify custody mode, and more
|
|
75
|
+
- **EVM + Solana**: Ethereum, Base, Polygon, BSC, Arbitrum, Optimism, Avalanche, Zora, PulseChain, Solana, and any other EVM-compatible chain
|
|
76
|
+
- **SOL + SPL tokens**: native SOL transfers and SPL token transfers (USDC, USDT, etc.) with automatic account creation
|
|
77
|
+
- **Built-in guards**: daily spending limits, gas price protection, emergency pause, rate limiting, replay protection, and on-chain verification, all active by default
|
|
78
|
+
- **x402 payments**: pay for x402-enabled APIs automatically, or accept x402 payments on your own endpoints (EVM and Solana)
|
|
79
|
+
- **Self-custody option**: run local mode and the key never leaves your machine. In hosted mode, keys are encrypted at rest, decrypted only during signing, and zeroed from memory immediately after. Either way you can export and walk away.
|
|
80
|
+
- **Permissionless**: No KYC. No KYT. No identity verification. No approval process. No compliance gatekeeping. Sign up, get an API key, and start transacting immediately.
|
|
81
|
+
- **30-second setup**: three lines of config. No SDK to install. No dependencies to manage.
|
|
21
82
|
|
|
22
83
|
## Pricing
|
|
23
84
|
|
|
24
|
-
- **$0.00345 per operation**
|
|
85
|
+
- **$0.00345 per operation**
|
|
25
86
|
- **6,000 free operations/month**
|
|
26
|
-
- **$0.0005 per x402 verification**
|
|
87
|
+
- **$0.0005 per x402 verification**
|
|
27
88
|
- **1,000 free x402 verifications/month**
|
|
28
|
-
- **Pay with USDC on-chain** via x402
|
|
29
|
-
- No monthly fee, no tiers
|
|
89
|
+
- **Pay with USDC on-chain** via x402, no credit card required
|
|
90
|
+
- No monthly fee, no tiers, just pay as you go
|
|
91
|
+
|
|
92
|
+
Competitor comparisons are kept at [hifriendbot.com/wallet/#pricing](https://hifriendbot.com/wallet/#pricing) with the date they were last verified. They live there rather than here because a published npm README cannot be corrected when someone else changes their prices.
|
|
30
93
|
|
|
31
94
|
## Quick Start
|
|
32
95
|
|
|
33
|
-
Get your free API key at [hifriendbot.com/wallet](https://hifriendbot.com/wallet)
|
|
96
|
+
Get your free API key at [hifriendbot.com/wallet](https://hifriendbot.com/wallet), no credit card required, no KYC, no approval wait.
|
|
34
97
|
|
|
35
98
|
### Claude Desktop / OpenClaw
|
|
36
99
|
|
|
@@ -115,6 +178,8 @@ Add to your settings:
|
|
|
115
178
|
| `delete_paywall` | Delete a paywall |
|
|
116
179
|
| `get_paywall_payments` | View payment history for a paywall |
|
|
117
180
|
| `get_x402_revenue` | Aggregate revenue stats across all paywalls |
|
|
181
|
+
| `wallet_mode` | Report whether signing is local (self-custody) or hosted, and which address is in use |
|
|
182
|
+
| `export_wallet_key` | How to export a hosted wallet key and move to self-custody |
|
|
118
183
|
| `buy_verification_credits` | Buy x402 verification credits with USDC on-chain |
|
|
119
184
|
| `get_usage` | Check your monthly usage and billing |
|
|
120
185
|
| `get_chains` | List all supported chains |
|
|
@@ -196,14 +261,14 @@ When an agent hits the paywall URL:
|
|
|
196
261
|
3. Retries with proof of payment
|
|
197
262
|
4. Receives the protected content
|
|
198
263
|
|
|
199
|
-
On-chain verification ensures every payment is real. Replay protection prevents double-spending. Revenue tracking shows you who paid, how much, and when. 1,000 free verifications/month, then $0.0005 each
|
|
264
|
+
On-chain verification ensures every payment is real. Replay protection prevents double-spending. Revenue tracking shows you who paid, how much, and when. 1,000 free verifications/month, then $0.0005 each. See the [pricing comparison](https://hifriendbot.com/wallet/#pricing) for how that stacks up.
|
|
200
265
|
|
|
201
266
|
## How We Compare
|
|
202
267
|
|
|
203
268
|
| Feature | Coinbase CDP | AgentWallet |
|
|
204
269
|
|---------|-------------|-------------|
|
|
205
270
|
| Setup Time | Install SDK + configure | **3 lines of config** |
|
|
206
|
-
| Approval Process | Identity verification required | **None
|
|
271
|
+
| Approval Process | Identity verification required | **None, instant access** |
|
|
207
272
|
| KYC Required | Yes | **No** |
|
|
208
273
|
| KYT / Transaction Monitoring | Yes | **No** |
|
|
209
274
|
| Can Block Your Wallet | Yes | **No** |
|
|
@@ -218,27 +283,27 @@ On-chain verification ensures every payment is real. Replay protection prevents
|
|
|
218
283
|
| Token Tools | Yes | **ERC-20 + SPL (29 tools)** |
|
|
219
284
|
| MCP Server | Yes | **Yes** |
|
|
220
285
|
|
|
221
|
-
## Pay with Crypto
|
|
286
|
+
## Pay with Crypto, No Credit Card Required
|
|
222
287
|
|
|
223
|
-
AgentWallet is the only AI agent wallet infrastructure that accepts crypto for its own API fees. Every competitor
|
|
288
|
+
AgentWallet is the only AI agent wallet infrastructure that accepts crypto for its own API fees. Every competitor, Coinbase CDP, Circle, MoonPay, Crossmint, Turnkey, requires a credit card or monthly invoice. With AgentWallet, your agent can pay for operations with USDC on-chain via the x402 protocol. No credit card, no invoice, no billing portal. Just on-chain payments.
|
|
224
289
|
|
|
225
290
|
When your agent exceeds the free tier (6,000 ops/month) without a credit card configured, the API returns HTTP 402 with USDC payment instructions. Your agent pays on-chain, retries with proof of payment, and the operation executes. Fully automated via the MCP server.
|
|
226
291
|
|
|
227
|
-
You can also pre-purchase x402 verification credits with USDC using the `buy_verification_credits` tool
|
|
292
|
+
You can also pre-purchase x402 verification credits with USDC using the `buy_verification_credits` tool, keeping your paywalls running beyond the free 1,000 verifications/month without needing a credit card.
|
|
228
293
|
|
|
229
294
|
## Built-in Guards
|
|
230
295
|
|
|
231
|
-
All guards are active by default
|
|
232
|
-
|
|
233
|
-
- **Encrypted at rest
|
|
234
|
-
- **Memory zeroing
|
|
235
|
-
- **Daily spending limits
|
|
236
|
-
- **Gas price protection
|
|
237
|
-
- **Emergency pause
|
|
238
|
-
- **Rate limiting
|
|
239
|
-
- **Replay protection
|
|
240
|
-
- **On-chain verification
|
|
241
|
-
- Bug bounty program: $50
|
|
296
|
+
All guards are active by default, no configuration required.
|
|
297
|
+
|
|
298
|
+
- **Encrypted at rest**: private keys encrypted before storage and never leave the server
|
|
299
|
+
- **Memory zeroing**: keys wiped from memory immediately after every signing operation
|
|
300
|
+
- **Daily spending limits**: set a per-wallet daily cap in USD, enforced automatically on every transaction
|
|
301
|
+
- **Gas price protection**: transactions blocked when gas prices spike above safe thresholds
|
|
302
|
+
- **Emergency pause**: instantly freeze any wallet or all wallets with one click
|
|
303
|
+
- **Rate limiting**: API requests capped per minute to prevent abuse and brute force attacks
|
|
304
|
+
- **Replay protection**: every x402 payment verified on-chain with unique transaction tracking
|
|
305
|
+
- **On-chain verification**: x402 payments verified directly on the blockchain with finalized commitment
|
|
306
|
+
- Bug bounty program: $50,$500 for responsible disclosure ([details](https://hifriendbot.com/wallet))
|
|
242
307
|
|
|
243
308
|
## Links
|
|
244
309
|
|
package/build/index.js
CHANGED
|
@@ -13,16 +13,155 @@ 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
|
import { deriveX402Payment, isWithinCap } from './x402-payment.js';
|
|
16
|
+
import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
|
|
17
|
+
import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, } from './local-solana.js';
|
|
18
|
+
/** True when either chain family is running non-custodially. */
|
|
19
|
+
function anyLocalMode() {
|
|
20
|
+
return isLocalMode() || isSolanaLocalMode();
|
|
21
|
+
}
|
|
16
22
|
// ─── Configuration ──────────────────────────────────────────────
|
|
17
23
|
const API_BASE = process.env.AGENTWALLET_API_URL || 'https://hifriendbot.com/wp-json/agentwallet/v1';
|
|
18
24
|
const API_USER = process.env.AGENTWALLET_USER || '';
|
|
19
25
|
const API_PASS = process.env.AGENTWALLET_PASS || ''; // WordPress application password
|
|
20
26
|
const X402_WALLET_ID = process.env.AGENTWALLET_WALLET_ID || ''; // Wallet ID for x402 auto-pay
|
|
27
|
+
const DASHBOARD_URL = process.env.AGENTWALLET_DASHBOARD_URL || 'https://hifriendbot.com/wallet/';
|
|
28
|
+
/* ── Local-mode routing ──────────────────────────────────────────
|
|
29
|
+
Translates the REST paths the tools already speak into in-process signing
|
|
30
|
+
calls. Anything not listed here falls through to the hosted API, so
|
|
31
|
+
non-custodial concerns (paywalls, billing, usage) still work if the operator
|
|
32
|
+
has also configured credentials. */
|
|
33
|
+
const NOT_HANDLED = Symbol('not-handled');
|
|
34
|
+
function localChainId(body, qs) {
|
|
35
|
+
const fromBody = body?.chain_id;
|
|
36
|
+
if (typeof fromBody === 'number')
|
|
37
|
+
return fromBody;
|
|
38
|
+
const fromQs = qs?.get('chain_id');
|
|
39
|
+
if (fromQs)
|
|
40
|
+
return parseInt(fromQs, 10);
|
|
41
|
+
const fromEnv = (process.env.AGENTWALLET_CHAIN_ID || '').trim();
|
|
42
|
+
if (fromEnv)
|
|
43
|
+
return parseInt(fromEnv, 10);
|
|
44
|
+
return 8453; // Base, matching the custodial default
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Refuse rather than silently hand the operation to someone else's signer.
|
|
48
|
+
*
|
|
49
|
+
* Reached when the operator is running non-custodially for one chain family but
|
|
50
|
+
* has supplied no key for the other. Falling through to the hosted API here
|
|
51
|
+
* would move funds onto a key they do not hold, while they believe they are in
|
|
52
|
+
* self-custody. That is the single worst thing this server could do, so it
|
|
53
|
+
* stops and says exactly which variable is missing.
|
|
54
|
+
*/
|
|
55
|
+
function refuseMissingLocalKey(family) {
|
|
56
|
+
const wanted = family === 'solana'
|
|
57
|
+
? 'AGENTWALLET_SOLANA_KEY (or AGENTWALLET_SOLANA_KEYFILE)'
|
|
58
|
+
: 'AGENTWALLET_PRIVATE_KEY (or AGENTWALLET_KEYFILE)';
|
|
59
|
+
throw new Error(`This is a ${family === 'solana' ? 'Solana' : 'EVM'} operation and no local ${family === 'solana' ? 'Solana' : 'EVM'} key is configured, ` +
|
|
60
|
+
`but this server is running in local signing mode. Refusing rather than routing it through the hosted ` +
|
|
61
|
+
`custodial signer while you believe you hold the keys. Set ${wanted} to sign it here, or unset the other ` +
|
|
62
|
+
`local key variables to use the hosted path for everything.`);
|
|
63
|
+
}
|
|
64
|
+
async function routeLocally(path, method, body) {
|
|
65
|
+
const [rawPath, rawQs = ''] = path.split('?');
|
|
66
|
+
const qs = new URLSearchParams(rawQs);
|
|
67
|
+
// POST /wallets/{id}/send
|
|
68
|
+
if (method === 'POST' && /^\/wallets\/[^/]*\/send$/.test(rawPath)) {
|
|
69
|
+
const chainId = localChainId(body, qs);
|
|
70
|
+
const solanaOp = SOLANA_CHAIN_IDS.has(chainId) || Boolean(body?.token_mint);
|
|
71
|
+
if (solanaOp) {
|
|
72
|
+
if (!isSolanaLocalMode())
|
|
73
|
+
refuseMissingLocalKey('solana');
|
|
74
|
+
const to = String(body?.to || '');
|
|
75
|
+
const value = String(body?.value ?? '0');
|
|
76
|
+
if (body?.token_mint) {
|
|
77
|
+
return localSplTransfer(String(body.token_mint), to, value, Number(body.token_decimals ?? 6), chainId);
|
|
78
|
+
}
|
|
79
|
+
return localSolTransfer(to, value, chainId);
|
|
80
|
+
}
|
|
81
|
+
if (!isLocalMode())
|
|
82
|
+
refuseMissingLocalKey('evm');
|
|
83
|
+
const to = String(body?.to || '');
|
|
84
|
+
const value = BigInt(String(body?.value ?? '0'));
|
|
85
|
+
const data = String(body?.data || '');
|
|
86
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(to)) {
|
|
87
|
+
throw new Error(`Local mode expects an EVM address, got "${to}".`);
|
|
88
|
+
}
|
|
89
|
+
const hasData = data && data !== '0x';
|
|
90
|
+
return localSend(chainId, to, value, hasData ? data : undefined);
|
|
91
|
+
}
|
|
92
|
+
// GET /wallets/{id}/balance
|
|
93
|
+
if (method === 'GET' && /^\/wallets\/[^/]*\/balance$/.test(rawPath)) {
|
|
94
|
+
const chainId = localChainId(body, qs);
|
|
95
|
+
if (SOLANA_CHAIN_IDS.has(chainId)) {
|
|
96
|
+
if (!isSolanaLocalMode())
|
|
97
|
+
refuseMissingLocalKey('solana');
|
|
98
|
+
return localSolBalance(chainId);
|
|
99
|
+
}
|
|
100
|
+
if (!isLocalMode())
|
|
101
|
+
refuseMissingLocalKey('evm');
|
|
102
|
+
return localNativeBalance(chainId);
|
|
103
|
+
}
|
|
104
|
+
// GET /wallets/{id}/token-balance
|
|
105
|
+
if (method === 'GET' && /^\/wallets\/[^/]*\/token-balance$/.test(rawPath)) {
|
|
106
|
+
const chainId = localChainId(body, qs);
|
|
107
|
+
const token0 = qs.get('token') || qs.get('token_address') || '';
|
|
108
|
+
if (SOLANA_CHAIN_IDS.has(chainId)) {
|
|
109
|
+
if (!isSolanaLocalMode())
|
|
110
|
+
refuseMissingLocalKey('solana');
|
|
111
|
+
return localSplBalance(token0, chainId);
|
|
112
|
+
}
|
|
113
|
+
if (!isLocalMode())
|
|
114
|
+
refuseMissingLocalKey('evm');
|
|
115
|
+
const token = token0;
|
|
116
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(token)) {
|
|
117
|
+
throw new Error(`Local mode needs an EVM token address, got "${token}".`);
|
|
118
|
+
}
|
|
119
|
+
return localTokenBalance(chainId, token);
|
|
120
|
+
}
|
|
121
|
+
// POST /wallets/{id}/sign (message signing only; tx signing goes through send)
|
|
122
|
+
if (method === 'POST' && /^\/wallets\/[^/]*\/sign$/.test(rawPath)) {
|
|
123
|
+
const message = body?.message;
|
|
124
|
+
if (typeof message === 'string' && message)
|
|
125
|
+
return localSignMessage(message);
|
|
126
|
+
throw new Error('Local mode signs messages in-process. For transactions use send_transaction, ' +
|
|
127
|
+
'which signs and broadcasts locally in one step.');
|
|
128
|
+
}
|
|
129
|
+
// Wallet listing and lookup describe whichever local keys are configured.
|
|
130
|
+
if (method === 'GET' && rawPath === '/wallets') {
|
|
131
|
+
const wallets = [];
|
|
132
|
+
if (isLocalMode())
|
|
133
|
+
wallets.push(localWalletRecord());
|
|
134
|
+
if (isSolanaLocalMode())
|
|
135
|
+
wallets.push(solanaWalletRecord());
|
|
136
|
+
return { wallets };
|
|
137
|
+
}
|
|
138
|
+
if (method === 'GET' && /^\/wallets\/[^/]+$/.test(rawPath)) {
|
|
139
|
+
return isLocalMode() ? localWalletRecord() : solanaWalletRecord();
|
|
140
|
+
}
|
|
141
|
+
if (method === 'POST' && rawPath === '/wallets') {
|
|
142
|
+
throw new Error('Local signing mode uses the single key you supplied, so there is nothing to create. ' +
|
|
143
|
+
'Generate another key yourself and run a second instance with a different ' +
|
|
144
|
+
'AGENTWALLET_PRIVATE_KEY, or unset it to create hosted wallets.');
|
|
145
|
+
}
|
|
146
|
+
if (/^\/wallets\/[^/]*\/(pause|unpause)$/.test(rawPath) || (method === 'DELETE' && /^\/wallets\/[^/]+$/.test(rawPath))) {
|
|
147
|
+
throw new Error('Pause, unpause and delete are custodial controls: they work by refusing to sign on the server. ' +
|
|
148
|
+
'In local mode the key is yours, so nothing can freeze it. Stop the process or move the funds.');
|
|
149
|
+
}
|
|
150
|
+
return NOT_HANDLED;
|
|
151
|
+
}
|
|
21
152
|
/**
|
|
22
153
|
* Make an API call. If the response is 402 and auto-pay is configured,
|
|
23
154
|
* automatically pay via x402 and retry the request.
|
|
24
155
|
*/
|
|
25
156
|
async function api(path, method = 'GET', body, extraHeaders) {
|
|
157
|
+
/* Local mode intercepts here rather than inside each tool. Routing at the
|
|
158
|
+
single choke point every funds operation already passes through means no
|
|
159
|
+
tool can quietly stay custodial because someone forgot to update it. */
|
|
160
|
+
if (anyLocalMode()) {
|
|
161
|
+
const handled = await routeLocally(path, method, body);
|
|
162
|
+
if (handled !== NOT_HANDLED)
|
|
163
|
+
return handled;
|
|
164
|
+
}
|
|
26
165
|
const url = `${API_BASE}${path}`;
|
|
27
166
|
const headers = {
|
|
28
167
|
'Content-Type': 'application/json',
|
|
@@ -37,8 +176,8 @@ async function api(path, method = 'GET', body, extraHeaders) {
|
|
|
37
176
|
}
|
|
38
177
|
const res = await fetch(url, options);
|
|
39
178
|
const data = await res.json();
|
|
40
|
-
// Handle 402 Payment Required — auto-pay if wallet configured
|
|
41
|
-
if (res.status === 402 && X402_WALLET_ID && !extraHeaders?.['X-PAYMENT']) {
|
|
179
|
+
// Handle 402 Payment Required — auto-pay if a wallet or a local key is configured
|
|
180
|
+
if (res.status === 402 && (X402_WALLET_ID || anyLocalMode()) && !extraHeaders?.['X-PAYMENT']) {
|
|
42
181
|
return handleX402Payment(data, path, method, body);
|
|
43
182
|
}
|
|
44
183
|
if (!res.ok) {
|
|
@@ -1096,10 +1235,107 @@ server.tool('get_x402_revenue', 'Get aggregate x402 revenue statistics across al
|
|
|
1096
1235
|
const data = await api('/x402/revenue');
|
|
1097
1236
|
return jsonResponse(data);
|
|
1098
1237
|
});
|
|
1238
|
+
// ─── Tool: wallet_mode ──────────────────────────────────────────
|
|
1239
|
+
server.tool('wallet_mode', 'Report whether this server is signing locally (self-custody, the private key never leaves this machine) ' +
|
|
1240
|
+
'or through the hosted AgentWallet API (custodial). Use this to verify custody before moving funds.', {}, async () => {
|
|
1241
|
+
if (!anyLocalMode()) {
|
|
1242
|
+
return jsonResponse({
|
|
1243
|
+
mode: 'custodial',
|
|
1244
|
+
custody: 'agentwallet',
|
|
1245
|
+
signing: 'AgentWallet servers hold an encrypted key and sign on your behalf.',
|
|
1246
|
+
api_base: API_BASE,
|
|
1247
|
+
to_self_custody: 'Set AGENTWALLET_PRIVATE_KEY for EVM and/or AGENTWALLET_SOLANA_KEY for Solana, then restart. ' +
|
|
1248
|
+
'Use export_wallet_key first if you want to carry an existing hosted wallet across.',
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
const report = {
|
|
1252
|
+
mode: 'local',
|
|
1253
|
+
custody: 'self',
|
|
1254
|
+
signing: 'Signed in this process. Keys are never sent to AgentWallet or anyone else.',
|
|
1255
|
+
max_autopay: process.env.AGENTWALLET_MAX_AUTOPAY || '1',
|
|
1256
|
+
};
|
|
1257
|
+
if (isLocalMode()) {
|
|
1258
|
+
let rpc = 'default public endpoint';
|
|
1259
|
+
try {
|
|
1260
|
+
rpc = resolveRpcUrl(parseInt(process.env.AGENTWALLET_CHAIN_ID || '8453', 10));
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* chain has no default; not worth failing the report over */ }
|
|
1263
|
+
report.evm = {
|
|
1264
|
+
address: getLocalAddress(),
|
|
1265
|
+
rpc_endpoint: rpc,
|
|
1266
|
+
per_tx_cap_native: process.env.AGENTWALLET_MAX_TX_NATIVE || 'not set',
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
report.evm = 'no local EVM key. EVM operations are refused, not sent to the hosted signer.';
|
|
1271
|
+
}
|
|
1272
|
+
if (isSolanaLocalMode()) {
|
|
1273
|
+
let rpc = 'default public endpoint';
|
|
1274
|
+
try {
|
|
1275
|
+
rpc = resolveSolanaRpc(900);
|
|
1276
|
+
}
|
|
1277
|
+
catch { /* fall through to the default label */ }
|
|
1278
|
+
report.solana = {
|
|
1279
|
+
address: getSolanaAddress(),
|
|
1280
|
+
rpc_endpoint: rpc,
|
|
1281
|
+
per_tx_cap_sol: process.env.AGENTWALLET_MAX_TX_SOL || 'not set',
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
report.solana = 'no local Solana key. Solana operations are refused, not sent to the hosted signer.';
|
|
1286
|
+
}
|
|
1287
|
+
return jsonResponse(report);
|
|
1288
|
+
});
|
|
1289
|
+
// ─── Tool: export_wallet_key ────────────────────────────────────
|
|
1290
|
+
server.tool('export_wallet_key', 'Explain how to export the private key of a hosted (custodial) AgentWallet so it can be moved ' +
|
|
1291
|
+
'to self-custody or any other wallet. Export itself is deliberately human-gated and is not ' +
|
|
1292
|
+
'performed by this tool.', {
|
|
1293
|
+
wallet_id: z.number().int().optional().describe('Hosted wallet ID to export'),
|
|
1294
|
+
}, async ({ wallet_id }) => {
|
|
1295
|
+
if (anyLocalMode()) {
|
|
1296
|
+
return jsonResponse({
|
|
1297
|
+
exportable: true,
|
|
1298
|
+
mode: 'local',
|
|
1299
|
+
note: 'You are already in self-custody. The keys are the ones you supplied via AGENTWALLET_PRIVATE_KEY, ' +
|
|
1300
|
+
'AGENTWALLET_SOLANA_KEY or their KEYFILE variants, and this server keeps no copy beyond this process.',
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
return jsonResponse({
|
|
1304
|
+
exportable: true,
|
|
1305
|
+
mode: 'custodial',
|
|
1306
|
+
how: `Sign in at ${DASHBOARD_URL} and use Export key on the wallet. ` +
|
|
1307
|
+
'The key is shown once and the export is logged.',
|
|
1308
|
+
wallet_id: wallet_id ?? 'all wallets are exportable',
|
|
1309
|
+
why_not_here: 'Export is gated behind a browser login rather than the API key on purpose. An API key can ' +
|
|
1310
|
+
'spend within your limits; if it could also export keys, a leaked key would mean instant total loss. ' +
|
|
1311
|
+
'Your funds stay portable either way: nothing here is locked in.',
|
|
1312
|
+
after_export: 'Set AGENTWALLET_PRIVATE_KEY to the exported key and restart to run non-custodially, ' +
|
|
1313
|
+
'or import it into any wallet you like.',
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1099
1316
|
// ─── Start ──────────────────────────────────────────────────────
|
|
1100
1317
|
async function main() {
|
|
1101
1318
|
const transport = new StdioServerTransport();
|
|
1102
1319
|
await server.connect(transport);
|
|
1320
|
+
/* stderr, so it never corrupts the stdio JSON-RPC stream. Announcing custody
|
|
1321
|
+
at startup means an operator sees which mode they are in without asking. */
|
|
1322
|
+
if (anyLocalMode()) {
|
|
1323
|
+
try {
|
|
1324
|
+
const parts = [];
|
|
1325
|
+
if (isLocalMode())
|
|
1326
|
+
parts.push(`EVM ${getLocalAddress()}`);
|
|
1327
|
+
if (isSolanaLocalMode())
|
|
1328
|
+
parts.push(`Solana ${getSolanaAddress()}`);
|
|
1329
|
+
console.error(`AgentWallet MCP: LOCAL signing mode. ${parts.join(', ')}. Keys never leave this machine.`);
|
|
1330
|
+
}
|
|
1331
|
+
catch (e) {
|
|
1332
|
+
console.error(`AgentWallet MCP: local signing configured but a key could not be loaded: ${e.message}`);
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
console.error('AgentWallet MCP: custodial mode via ' + API_BASE + '. Run wallet_mode for details.');
|
|
1338
|
+
}
|
|
1103
1339
|
}
|
|
1104
1340
|
main().catch((error) => {
|
|
1105
1341
|
console.error('AgentWallet MCP server failed to start:', error);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (non-custodial) Solana signing.
|
|
3
|
+
*
|
|
4
|
+
* Same contract as local-wallet.ts on the EVM side: the secret key is loaded
|
|
5
|
+
* once into this process, never written anywhere, never logged, and never put
|
|
6
|
+
* in an error message. Transactions are signed here and submitted straight to
|
|
7
|
+
* an RPC endpoint. The AgentWallet API is not involved.
|
|
8
|
+
*
|
|
9
|
+
* SPL transfers are built from raw instructions rather than @solana/spl-token
|
|
10
|
+
* on purpose. That package pulls in bigint-buffer, which carries a high
|
|
11
|
+
* severity buffer overflow advisory, and a wallet has no business shipping
|
|
12
|
+
* that when the two instructions it needs are a dozen lines each.
|
|
13
|
+
*/
|
|
14
|
+
import { Keypair } from '@solana/web3.js';
|
|
15
|
+
export declare function isSolanaLocalMode(): boolean;
|
|
16
|
+
export declare function getSolanaKeypair(): Keypair;
|
|
17
|
+
export declare function getSolanaAddress(): string;
|
|
18
|
+
export declare function resolveSolanaRpc(chainId?: number): string;
|
|
19
|
+
export declare function localSolBalance(chainId?: number): Promise<{
|
|
20
|
+
address: string;
|
|
21
|
+
chain_id: number;
|
|
22
|
+
balance_lamports: string;
|
|
23
|
+
balance: string;
|
|
24
|
+
mode: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function localSplBalance(mintStr: string, chainId?: number): Promise<{
|
|
27
|
+
address: string;
|
|
28
|
+
chain_id: number;
|
|
29
|
+
mint: string;
|
|
30
|
+
token_account: string;
|
|
31
|
+
balance_raw: string;
|
|
32
|
+
balance: string;
|
|
33
|
+
decimals: number;
|
|
34
|
+
mode: string;
|
|
35
|
+
}>;
|
|
36
|
+
export interface LocalSolResult {
|
|
37
|
+
signature: string;
|
|
38
|
+
from: string;
|
|
39
|
+
to: string;
|
|
40
|
+
value: string;
|
|
41
|
+
chain_id: number;
|
|
42
|
+
mode: 'local';
|
|
43
|
+
signed_locally: true;
|
|
44
|
+
}
|
|
45
|
+
export declare function localSolTransfer(to: string, lamports: string, chainId?: number): Promise<LocalSolResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Transfer SPL tokens. The recipient's associated account is created if it does
|
|
48
|
+
* not exist, using the idempotent instruction so a race cannot fail the send.
|
|
49
|
+
*/
|
|
50
|
+
export declare function localSplTransfer(mintStr: string, to: string, rawAmount: string, decimals: number, chainId?: number): Promise<LocalSolResult>;
|
|
51
|
+
export declare function solanaWalletRecord(): {
|
|
52
|
+
id: string;
|
|
53
|
+
address: string;
|
|
54
|
+
wallet_type: string;
|
|
55
|
+
mode: string;
|
|
56
|
+
custody: string;
|
|
57
|
+
note: string;
|
|
58
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (non-custodial) Solana signing.
|
|
3
|
+
*
|
|
4
|
+
* Same contract as local-wallet.ts on the EVM side: the secret key is loaded
|
|
5
|
+
* once into this process, never written anywhere, never logged, and never put
|
|
6
|
+
* in an error message. Transactions are signed here and submitted straight to
|
|
7
|
+
* an RPC endpoint. The AgentWallet API is not involved.
|
|
8
|
+
*
|
|
9
|
+
* SPL transfers are built from raw instructions rather than @solana/spl-token
|
|
10
|
+
* on purpose. That package pulls in bigint-buffer, which carries a high
|
|
11
|
+
* severity buffer overflow advisory, and a wallet has no business shipping
|
|
12
|
+
* that when the two instructions it needs are a dozen lines each.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { Connection, Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction, sendAndConfirmTransaction, LAMPORTS_PER_SOL, } from '@solana/web3.js';
|
|
16
|
+
import bs58 from 'bs58';
|
|
17
|
+
/* ── Program IDs ─────────────────────────────────────────────────── */
|
|
18
|
+
const TOKEN_PROGRAM = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
|
19
|
+
const TOKEN_2022_PROGRAM = new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
|
|
20
|
+
const ASSOCIATED_TOKEN_PROGRAM = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
|
|
21
|
+
/* ── Key loading ─────────────────────────────────────────────────── */
|
|
22
|
+
let cached = null;
|
|
23
|
+
/**
|
|
24
|
+
* Accepts every format a Solana user is likely to already have:
|
|
25
|
+
* - solana-keygen id.json, a JSON array of 64 bytes
|
|
26
|
+
* - base58 secret key, what Phantom and friends export
|
|
27
|
+
* - base64, 64 bytes, or the 96-byte sodium keypair AgentWallet exports
|
|
28
|
+
* for hosted Solana wallets (its first 64 bytes are the secret key)
|
|
29
|
+
*/
|
|
30
|
+
function parseSolanaKey(raw) {
|
|
31
|
+
const text = raw.trim();
|
|
32
|
+
if (text.startsWith('[')) {
|
|
33
|
+
const arr = JSON.parse(text);
|
|
34
|
+
if (!Array.isArray(arr) || arr.length < 64) {
|
|
35
|
+
throw new Error('Solana key JSON must be an array of at least 64 bytes.');
|
|
36
|
+
}
|
|
37
|
+
return Keypair.fromSecretKey(Uint8Array.from(arr.slice(0, 64)));
|
|
38
|
+
}
|
|
39
|
+
// base64 tends to contain characters base58 never does.
|
|
40
|
+
if (/[+/=]/.test(text)) {
|
|
41
|
+
const buf = Buffer.from(text, 'base64');
|
|
42
|
+
if (buf.length !== 64 && buf.length !== 96) {
|
|
43
|
+
throw new Error(`Base64 Solana key must decode to 64 or 96 bytes, got ${buf.length}.`);
|
|
44
|
+
}
|
|
45
|
+
return Keypair.fromSecretKey(Uint8Array.from(buf.subarray(0, 64)));
|
|
46
|
+
}
|
|
47
|
+
const decoded = bs58.decode(text);
|
|
48
|
+
if (decoded.length !== 64) {
|
|
49
|
+
throw new Error(`Base58 Solana key must decode to 64 bytes, got ${decoded.length}.`);
|
|
50
|
+
}
|
|
51
|
+
return Keypair.fromSecretKey(decoded);
|
|
52
|
+
}
|
|
53
|
+
export function isSolanaLocalMode() {
|
|
54
|
+
return Boolean((process.env.AGENTWALLET_SOLANA_KEY || '').trim() ||
|
|
55
|
+
(process.env.AGENTWALLET_SOLANA_KEYFILE || '').trim());
|
|
56
|
+
}
|
|
57
|
+
export function getSolanaKeypair() {
|
|
58
|
+
if (cached)
|
|
59
|
+
return cached;
|
|
60
|
+
const inline = (process.env.AGENTWALLET_SOLANA_KEY || '').trim();
|
|
61
|
+
const file = (process.env.AGENTWALLET_SOLANA_KEYFILE || '').trim();
|
|
62
|
+
let raw = inline;
|
|
63
|
+
if (!raw && file) {
|
|
64
|
+
try {
|
|
65
|
+
raw = readFileSync(file, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new Error(`Could not read AGENTWALLET_SOLANA_KEYFILE at ${file}: ${e.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!raw)
|
|
72
|
+
throw new Error('Solana local signing is not configured.');
|
|
73
|
+
try {
|
|
74
|
+
cached = parseSolanaKey(raw);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
// Re-thrown deliberately without the key material in the message.
|
|
78
|
+
throw new Error(`Invalid Solana key: ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
return cached;
|
|
81
|
+
}
|
|
82
|
+
export function getSolanaAddress() {
|
|
83
|
+
return getSolanaKeypair().publicKey.toBase58();
|
|
84
|
+
}
|
|
85
|
+
/* ── Connection ──────────────────────────────────────────────────── */
|
|
86
|
+
const DEFAULT_SOLANA_RPC = {
|
|
87
|
+
900: 'https://api.mainnet-beta.solana.com',
|
|
88
|
+
901: 'https://api.devnet.solana.com',
|
|
89
|
+
902: 'https://api.testnet.solana.com',
|
|
90
|
+
};
|
|
91
|
+
export function resolveSolanaRpc(chainId = 900) {
|
|
92
|
+
const explicit = (process.env.AGENTWALLET_SOLANA_RPC || '').trim();
|
|
93
|
+
if (explicit)
|
|
94
|
+
return explicit;
|
|
95
|
+
const fallback = DEFAULT_SOLANA_RPC[chainId];
|
|
96
|
+
if (fallback)
|
|
97
|
+
return fallback;
|
|
98
|
+
throw new Error(`No Solana RPC for chain ${chainId}. Set AGENTWALLET_SOLANA_RPC.`);
|
|
99
|
+
}
|
|
100
|
+
function connection(chainId = 900) {
|
|
101
|
+
return new Connection(resolveSolanaRpc(chainId), 'confirmed');
|
|
102
|
+
}
|
|
103
|
+
/* ── Guards ──────────────────────────────────────────────────────── */
|
|
104
|
+
function assertWithinSolCap(lamports) {
|
|
105
|
+
const cap = (process.env.AGENTWALLET_MAX_TX_SOL || '').trim();
|
|
106
|
+
if (!cap)
|
|
107
|
+
return;
|
|
108
|
+
if (!/^\d+(\.\d+)?$/.test(cap)) {
|
|
109
|
+
throw new Error(`AGENTWALLET_MAX_TX_SOL must be a decimal number, got "${cap}".`);
|
|
110
|
+
}
|
|
111
|
+
const [whole, frac = ''] = cap.split('.');
|
|
112
|
+
const capLamports = BigInt(whole + frac.padEnd(9, '0').slice(0, 9));
|
|
113
|
+
if (lamports > capLamports) {
|
|
114
|
+
throw new Error(`Blocked by local guard: amount exceeds AGENTWALLET_MAX_TX_SOL (${cap} SOL).`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/* ── Reads ───────────────────────────────────────────────────────── */
|
|
118
|
+
export async function localSolBalance(chainId = 900) {
|
|
119
|
+
const conn = connection(chainId);
|
|
120
|
+
const owner = getSolanaKeypair().publicKey;
|
|
121
|
+
const lamports = await conn.getBalance(owner);
|
|
122
|
+
return {
|
|
123
|
+
address: owner.toBase58(),
|
|
124
|
+
chain_id: chainId,
|
|
125
|
+
balance_lamports: String(lamports),
|
|
126
|
+
balance: String(lamports / LAMPORTS_PER_SOL),
|
|
127
|
+
mode: 'local',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Which token program owns this mint: classic SPL or Token-2022. */
|
|
131
|
+
async function tokenProgramFor(conn, mint) {
|
|
132
|
+
const info = await conn.getAccountInfo(mint);
|
|
133
|
+
if (!info)
|
|
134
|
+
throw new Error(`Mint ${mint.toBase58()} not found on this cluster.`);
|
|
135
|
+
if (info.owner.equals(TOKEN_2022_PROGRAM))
|
|
136
|
+
return TOKEN_2022_PROGRAM;
|
|
137
|
+
return TOKEN_PROGRAM;
|
|
138
|
+
}
|
|
139
|
+
function associatedTokenAddress(owner, mint, tokenProgram) {
|
|
140
|
+
const [ata] = PublicKey.findProgramAddressSync([owner.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM);
|
|
141
|
+
return ata;
|
|
142
|
+
}
|
|
143
|
+
export async function localSplBalance(mintStr, chainId = 900) {
|
|
144
|
+
const conn = connection(chainId);
|
|
145
|
+
const owner = getSolanaKeypair().publicKey;
|
|
146
|
+
const mint = new PublicKey(mintStr);
|
|
147
|
+
const tokenProgram = await tokenProgramFor(conn, mint);
|
|
148
|
+
const ata = associatedTokenAddress(owner, mint, tokenProgram);
|
|
149
|
+
const res = await conn.getTokenAccountBalance(ata).catch(() => null);
|
|
150
|
+
return {
|
|
151
|
+
address: owner.toBase58(),
|
|
152
|
+
chain_id: chainId,
|
|
153
|
+
mint: mintStr,
|
|
154
|
+
token_account: ata.toBase58(),
|
|
155
|
+
balance_raw: res?.value.amount ?? '0',
|
|
156
|
+
balance: res?.value.uiAmountString ?? '0',
|
|
157
|
+
decimals: res?.value.decimals ?? 0,
|
|
158
|
+
mode: 'local',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export async function localSolTransfer(to, lamports, chainId = 900) {
|
|
162
|
+
const amount = BigInt(lamports);
|
|
163
|
+
assertWithinSolCap(amount);
|
|
164
|
+
const conn = connection(chainId);
|
|
165
|
+
const payer = getSolanaKeypair();
|
|
166
|
+
const tx = new Transaction().add(SystemProgram.transfer({
|
|
167
|
+
fromPubkey: payer.publicKey,
|
|
168
|
+
toPubkey: new PublicKey(to),
|
|
169
|
+
lamports: Number(amount),
|
|
170
|
+
}));
|
|
171
|
+
const signature = await sendAndConfirmTransaction(conn, tx, [payer]);
|
|
172
|
+
return {
|
|
173
|
+
signature,
|
|
174
|
+
from: payer.publicKey.toBase58(),
|
|
175
|
+
to,
|
|
176
|
+
value: lamports,
|
|
177
|
+
chain_id: chainId,
|
|
178
|
+
mode: 'local',
|
|
179
|
+
signed_locally: true,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** SPL transferChecked, opcode 12: amount as u64 LE followed by decimals. */
|
|
183
|
+
function transferCheckedIx(tokenProgram, source, mint, dest, owner, amount, decimals) {
|
|
184
|
+
const data = Buffer.alloc(10);
|
|
185
|
+
data.writeUInt8(12, 0);
|
|
186
|
+
data.writeBigUInt64LE(amount, 1);
|
|
187
|
+
data.writeUInt8(decimals, 9);
|
|
188
|
+
return new TransactionInstruction({
|
|
189
|
+
programId: tokenProgram,
|
|
190
|
+
keys: [
|
|
191
|
+
{ pubkey: source, isSigner: false, isWritable: true },
|
|
192
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
193
|
+
{ pubkey: dest, isSigner: false, isWritable: true },
|
|
194
|
+
{ pubkey: owner, isSigner: true, isWritable: false },
|
|
195
|
+
],
|
|
196
|
+
data,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** Associated token account create, idempotent variant (opcode 1). */
|
|
200
|
+
function createAtaIdempotentIx(payer, ata, owner, mint, tokenProgram) {
|
|
201
|
+
return new TransactionInstruction({
|
|
202
|
+
programId: ASSOCIATED_TOKEN_PROGRAM,
|
|
203
|
+
keys: [
|
|
204
|
+
{ pubkey: payer, isSigner: true, isWritable: true },
|
|
205
|
+
{ pubkey: ata, isSigner: false, isWritable: true },
|
|
206
|
+
{ pubkey: owner, isSigner: false, isWritable: false },
|
|
207
|
+
{ pubkey: mint, isSigner: false, isWritable: false },
|
|
208
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
209
|
+
{ pubkey: tokenProgram, isSigner: false, isWritable: false },
|
|
210
|
+
],
|
|
211
|
+
data: Buffer.from([1]),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Transfer SPL tokens. The recipient's associated account is created if it does
|
|
216
|
+
* not exist, using the idempotent instruction so a race cannot fail the send.
|
|
217
|
+
*/
|
|
218
|
+
export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId = 900) {
|
|
219
|
+
const conn = connection(chainId);
|
|
220
|
+
const payer = getSolanaKeypair();
|
|
221
|
+
const mint = new PublicKey(mintStr);
|
|
222
|
+
const recipient = new PublicKey(to);
|
|
223
|
+
const tokenProgram = await tokenProgramFor(conn, mint);
|
|
224
|
+
const sourceAta = associatedTokenAddress(payer.publicKey, mint, tokenProgram);
|
|
225
|
+
const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
|
|
226
|
+
const tx = new Transaction();
|
|
227
|
+
const destInfo = await conn.getAccountInfo(destAta);
|
|
228
|
+
if (!destInfo) {
|
|
229
|
+
tx.add(createAtaIdempotentIx(payer.publicKey, destAta, recipient, mint, tokenProgram));
|
|
230
|
+
}
|
|
231
|
+
tx.add(transferCheckedIx(tokenProgram, sourceAta, mint, destAta, payer.publicKey, BigInt(rawAmount), decimals));
|
|
232
|
+
const signature = await sendAndConfirmTransaction(conn, tx, [payer]);
|
|
233
|
+
return {
|
|
234
|
+
signature,
|
|
235
|
+
from: payer.publicKey.toBase58(),
|
|
236
|
+
to,
|
|
237
|
+
value: rawAmount,
|
|
238
|
+
chain_id: chainId,
|
|
239
|
+
mode: 'local',
|
|
240
|
+
signed_locally: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
export function solanaWalletRecord() {
|
|
244
|
+
return {
|
|
245
|
+
id: 'local-solana',
|
|
246
|
+
address: getSolanaAddress(),
|
|
247
|
+
wallet_type: 'solana',
|
|
248
|
+
mode: 'local',
|
|
249
|
+
custody: 'self',
|
|
250
|
+
note: 'Signed in-process. The secret key is never sent to AgentWallet servers.',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (non-custodial) signing mode.
|
|
3
|
+
*
|
|
4
|
+
* When AGENTWALLET_PRIVATE_KEY or AGENTWALLET_KEYFILE is set, every signing
|
|
5
|
+
* operation happens in this process with a key that never leaves the machine.
|
|
6
|
+
* The AgentWallet API is not called, not consulted, and not trusted for any
|
|
7
|
+
* funds-moving step. Transactions are broadcast straight to an RPC endpoint.
|
|
8
|
+
*
|
|
9
|
+
* This is the mode that makes the custody claim verifiable: an operator can
|
|
10
|
+
* read this file, run the server offline against their own RPC, and confirm the
|
|
11
|
+
* key is never transmitted. Nothing here writes the key to disk, logs it, or
|
|
12
|
+
* puts it in an error message.
|
|
13
|
+
*
|
|
14
|
+
* EVM only for now. Solana local signing is not implemented; those calls stay
|
|
15
|
+
* on the custodial path and say so explicitly rather than silently downgrading.
|
|
16
|
+
*/
|
|
17
|
+
import { type Address, type Hex } from 'viem';
|
|
18
|
+
/** True when the server is running non-custodially. */
|
|
19
|
+
export declare function isLocalMode(): boolean;
|
|
20
|
+
export declare function getLocalAccount(): {
|
|
21
|
+
address: Address;
|
|
22
|
+
nonceManager?: import("viem").NonceManager | undefined;
|
|
23
|
+
sign: (parameters: {
|
|
24
|
+
hash: import("viem").Hash;
|
|
25
|
+
}) => Promise<Hex>;
|
|
26
|
+
signAuthorization: (parameters: import("viem").AuthorizationRequest) => Promise<import("viem/accounts").SignAuthorizationReturnType>;
|
|
27
|
+
signMessage: ({ message }: {
|
|
28
|
+
message: import("viem").SignableMessage;
|
|
29
|
+
}) => Promise<Hex>;
|
|
30
|
+
signTransaction: <serializer extends import("viem").SerializeTransactionFn<import("viem").TransactionSerializable> = import("viem").SerializeTransactionFn<import("viem").TransactionSerializable>, transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]>(transaction: transaction, options?: {
|
|
31
|
+
serializer?: serializer | undefined;
|
|
32
|
+
} | undefined) => Promise<Hex>;
|
|
33
|
+
signTypedData: <const typedData extends import("viem").TypedData | Record<string, unknown>, primaryType extends keyof typedData | "EIP712Domain" = keyof typedData>(parameters: import("viem").TypedDataDefinition<typedData, primaryType>) => Promise<Hex>;
|
|
34
|
+
publicKey: Hex;
|
|
35
|
+
source: "privateKey";
|
|
36
|
+
type: "local";
|
|
37
|
+
};
|
|
38
|
+
/** The address the local key controls. Safe to log. */
|
|
39
|
+
export declare function getLocalAddress(): Address;
|
|
40
|
+
export declare function resolveRpcUrl(chainId: number): string;
|
|
41
|
+
export declare function localNativeBalance(chainId: number): Promise<{
|
|
42
|
+
address: `0x${string}`;
|
|
43
|
+
chain_id: number;
|
|
44
|
+
balance_wei: string;
|
|
45
|
+
balance: string;
|
|
46
|
+
mode: string;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function localTokenBalance(chainId: number, token: Address): Promise<{
|
|
49
|
+
address: `0x${string}`;
|
|
50
|
+
chain_id: number;
|
|
51
|
+
token: `0x${string}`;
|
|
52
|
+
symbol: string;
|
|
53
|
+
decimals: number;
|
|
54
|
+
balance_raw: string;
|
|
55
|
+
balance: string;
|
|
56
|
+
mode: string;
|
|
57
|
+
}>;
|
|
58
|
+
export interface LocalSendResult {
|
|
59
|
+
tx_hash: string;
|
|
60
|
+
from: string;
|
|
61
|
+
to: string;
|
|
62
|
+
value: string;
|
|
63
|
+
chain_id: number;
|
|
64
|
+
mode: 'local';
|
|
65
|
+
signed_locally: true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sign and broadcast a transaction. viem fills nonce, gas and EIP-1559 fees
|
|
69
|
+
* from the RPC unless they are supplied.
|
|
70
|
+
*/
|
|
71
|
+
export declare function localSend(chainId: number, to: Address, valueWei: bigint, data?: Hex): Promise<LocalSendResult>;
|
|
72
|
+
/** ERC-20 transfer, encoded locally. */
|
|
73
|
+
export declare function localTransferToken(chainId: number, token: Address, to: Address, rawAmount: string): Promise<LocalSendResult>;
|
|
74
|
+
/** ERC-20 approve, encoded locally. */
|
|
75
|
+
export declare function localApproveToken(chainId: number, token: Address, spender: Address, rawAmount: string): Promise<LocalSendResult>;
|
|
76
|
+
/** Sign a message without broadcasting anything. */
|
|
77
|
+
export declare function localSignMessage(message: string): Promise<{
|
|
78
|
+
address: string;
|
|
79
|
+
message: string;
|
|
80
|
+
signature: string;
|
|
81
|
+
mode: 'local';
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* One-line summary for tools that report wallet identity. Deliberately mirrors
|
|
85
|
+
* the shape of a custodial wallet record so callers do not need to branch.
|
|
86
|
+
*/
|
|
87
|
+
export declare function localWalletRecord(): {
|
|
88
|
+
id: string;
|
|
89
|
+
address: `0x${string}`;
|
|
90
|
+
wallet_type: string;
|
|
91
|
+
mode: string;
|
|
92
|
+
custody: string;
|
|
93
|
+
note: string;
|
|
94
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local (non-custodial) signing mode.
|
|
3
|
+
*
|
|
4
|
+
* When AGENTWALLET_PRIVATE_KEY or AGENTWALLET_KEYFILE is set, every signing
|
|
5
|
+
* operation happens in this process with a key that never leaves the machine.
|
|
6
|
+
* The AgentWallet API is not called, not consulted, and not trusted for any
|
|
7
|
+
* funds-moving step. Transactions are broadcast straight to an RPC endpoint.
|
|
8
|
+
*
|
|
9
|
+
* This is the mode that makes the custody claim verifiable: an operator can
|
|
10
|
+
* read this file, run the server offline against their own RPC, and confirm the
|
|
11
|
+
* key is never transmitted. Nothing here writes the key to disk, logs it, or
|
|
12
|
+
* puts it in an error message.
|
|
13
|
+
*
|
|
14
|
+
* EVM only for now. Solana local signing is not implemented; those calls stay
|
|
15
|
+
* on the custodial path and say so explicitly rather than silently downgrading.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { createPublicClient, createWalletClient, http, encodeFunctionData, parseAbi, formatUnits, } from 'viem';
|
|
19
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
20
|
+
/* ── Key loading ─────────────────────────────────────────────────── */
|
|
21
|
+
/**
|
|
22
|
+
* Read the key once at startup. Kept module-private: nothing exports the key
|
|
23
|
+
* itself, only an account object that can sign.
|
|
24
|
+
*/
|
|
25
|
+
let cachedAccount = null;
|
|
26
|
+
let keyLoadError = null;
|
|
27
|
+
function loadKeyMaterial() {
|
|
28
|
+
const inline = (process.env.AGENTWALLET_PRIVATE_KEY || '').trim();
|
|
29
|
+
if (inline)
|
|
30
|
+
return inline;
|
|
31
|
+
const file = (process.env.AGENTWALLET_KEYFILE || '').trim();
|
|
32
|
+
if (file) {
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(file, 'utf8').trim();
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
keyLoadError = `Could not read AGENTWALLET_KEYFILE at ${file}: ${e.message}`;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function normalizeKey(raw) {
|
|
44
|
+
const hex = raw.startsWith('0x') ? raw.slice(2) : raw;
|
|
45
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
46
|
+
throw new Error('Invalid private key. Expected 64 hex characters, with or without a 0x prefix. ' +
|
|
47
|
+
'The value itself is never logged.');
|
|
48
|
+
}
|
|
49
|
+
return `0x${hex}`;
|
|
50
|
+
}
|
|
51
|
+
/** True when the server is running non-custodially. */
|
|
52
|
+
export function isLocalMode() {
|
|
53
|
+
return Boolean((process.env.AGENTWALLET_PRIVATE_KEY || '').trim() ||
|
|
54
|
+
(process.env.AGENTWALLET_KEYFILE || '').trim());
|
|
55
|
+
}
|
|
56
|
+
export function getLocalAccount() {
|
|
57
|
+
if (cachedAccount)
|
|
58
|
+
return cachedAccount;
|
|
59
|
+
if (keyLoadError)
|
|
60
|
+
throw new Error(keyLoadError);
|
|
61
|
+
const raw = loadKeyMaterial();
|
|
62
|
+
if (!raw)
|
|
63
|
+
throw new Error('Local signing mode is not configured.');
|
|
64
|
+
if (keyLoadError)
|
|
65
|
+
throw new Error(keyLoadError);
|
|
66
|
+
cachedAccount = privateKeyToAccount(normalizeKey(raw));
|
|
67
|
+
return cachedAccount;
|
|
68
|
+
}
|
|
69
|
+
/** The address the local key controls. Safe to log. */
|
|
70
|
+
export function getLocalAddress() {
|
|
71
|
+
return getLocalAccount().address;
|
|
72
|
+
}
|
|
73
|
+
/* ── Chains and RPC ──────────────────────────────────────────────── */
|
|
74
|
+
/**
|
|
75
|
+
* Public RPC endpoints used only when the operator has not supplied their own.
|
|
76
|
+
* Anyone serious about privacy should set AGENTWALLET_RPC_<chainId>, because
|
|
77
|
+
* a public RPC sees every address you query.
|
|
78
|
+
*/
|
|
79
|
+
const DEFAULT_RPC = {
|
|
80
|
+
1: 'https://eth.llamarpc.com',
|
|
81
|
+
10: 'https://mainnet.optimism.io',
|
|
82
|
+
56: 'https://bsc-dataseed.binance.org',
|
|
83
|
+
137: 'https://polygon-rpc.com',
|
|
84
|
+
8453: 'https://mainnet.base.org',
|
|
85
|
+
42161: 'https://arb1.arbitrum.io/rpc',
|
|
86
|
+
43114: 'https://api.avax.network/ext/bc/C/rpc',
|
|
87
|
+
7777777: 'https://rpc.zora.energy',
|
|
88
|
+
369: 'https://rpc.pulsechain.com',
|
|
89
|
+
};
|
|
90
|
+
export function resolveRpcUrl(chainId) {
|
|
91
|
+
const perChain = (process.env[`AGENTWALLET_RPC_${chainId}`] || '').trim();
|
|
92
|
+
if (perChain)
|
|
93
|
+
return perChain;
|
|
94
|
+
const generic = (process.env.AGENTWALLET_RPC_URL || '').trim();
|
|
95
|
+
if (generic)
|
|
96
|
+
return generic;
|
|
97
|
+
const fallback = DEFAULT_RPC[chainId];
|
|
98
|
+
if (fallback)
|
|
99
|
+
return fallback;
|
|
100
|
+
throw new Error(`No RPC endpoint for chain ${chainId} in local mode. ` +
|
|
101
|
+
`Set AGENTWALLET_RPC_${chainId} to an endpoint you trust.`);
|
|
102
|
+
}
|
|
103
|
+
function clients(chainId) {
|
|
104
|
+
const url = resolveRpcUrl(chainId);
|
|
105
|
+
const transport = http(url);
|
|
106
|
+
// The chain object is deliberately minimal: viem only needs the id for
|
|
107
|
+
// signing, and not pinning a chain definition keeps any EVM network usable.
|
|
108
|
+
const chain = { id: chainId, name: `chain-${chainId}`, nativeCurrency: { name: 'Native', symbol: 'NATIVE', decimals: 18 }, rpcUrls: { default: { http: [url] } } };
|
|
109
|
+
return {
|
|
110
|
+
pub: createPublicClient({ chain, transport }),
|
|
111
|
+
wallet: createWalletClient({ account: getLocalAccount(), chain, transport }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/* ── Spend guards ────────────────────────────────────────────────── */
|
|
115
|
+
/**
|
|
116
|
+
* Per-transaction ceiling for local mode, in native units (ETH, MATIC...).
|
|
117
|
+
* Custodial mode enforces limits server-side; local mode has no server, so the
|
|
118
|
+
* guard has to live here or it does not exist at all.
|
|
119
|
+
*/
|
|
120
|
+
function assertWithinNativeCap(valueWei) {
|
|
121
|
+
const cap = (process.env.AGENTWALLET_MAX_TX_NATIVE || '').trim();
|
|
122
|
+
if (!cap)
|
|
123
|
+
return;
|
|
124
|
+
if (!/^\d+(\.\d+)?$/.test(cap)) {
|
|
125
|
+
throw new Error(`AGENTWALLET_MAX_TX_NATIVE must be a decimal number, got "${cap}".`);
|
|
126
|
+
}
|
|
127
|
+
const [whole, frac = ''] = cap.split('.');
|
|
128
|
+
const capWei = BigInt(whole + frac.padEnd(18, '0').slice(0, 18));
|
|
129
|
+
if (valueWei > capWei) {
|
|
130
|
+
throw new Error(`Blocked by local guard: transaction value exceeds AGENTWALLET_MAX_TX_NATIVE (${cap}). ` +
|
|
131
|
+
`Raise the cap deliberately if this is intended.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/* ── Read operations ─────────────────────────────────────────────── */
|
|
135
|
+
export async function localNativeBalance(chainId) {
|
|
136
|
+
const { pub } = clients(chainId);
|
|
137
|
+
const address = getLocalAddress();
|
|
138
|
+
const wei = await pub.getBalance({ address });
|
|
139
|
+
return { address, chain_id: chainId, balance_wei: wei.toString(), balance: formatUnits(wei, 18), mode: 'local' };
|
|
140
|
+
}
|
|
141
|
+
const ERC20_ABI = parseAbi([
|
|
142
|
+
'function balanceOf(address) view returns (uint256)',
|
|
143
|
+
'function decimals() view returns (uint8)',
|
|
144
|
+
'function symbol() view returns (string)',
|
|
145
|
+
'function transfer(address,uint256) returns (bool)',
|
|
146
|
+
'function approve(address,uint256) returns (bool)',
|
|
147
|
+
'function allowance(address,address) view returns (uint256)',
|
|
148
|
+
]);
|
|
149
|
+
export async function localTokenBalance(chainId, token) {
|
|
150
|
+
const { pub } = clients(chainId);
|
|
151
|
+
const address = getLocalAddress();
|
|
152
|
+
const [raw, decimals, symbol] = await Promise.all([
|
|
153
|
+
pub.readContract({ address: token, abi: ERC20_ABI, functionName: 'balanceOf', args: [address] }),
|
|
154
|
+
pub.readContract({ address: token, abi: ERC20_ABI, functionName: 'decimals' }).catch(() => 18),
|
|
155
|
+
pub.readContract({ address: token, abi: ERC20_ABI, functionName: 'symbol' }).catch(() => ''),
|
|
156
|
+
]);
|
|
157
|
+
return {
|
|
158
|
+
address, chain_id: chainId, token, symbol,
|
|
159
|
+
decimals: Number(decimals),
|
|
160
|
+
balance_raw: raw.toString(),
|
|
161
|
+
balance: formatUnits(raw, Number(decimals)),
|
|
162
|
+
mode: 'local',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Sign and broadcast a transaction. viem fills nonce, gas and EIP-1559 fees
|
|
167
|
+
* from the RPC unless they are supplied.
|
|
168
|
+
*/
|
|
169
|
+
export async function localSend(chainId, to, valueWei, data) {
|
|
170
|
+
assertWithinNativeCap(valueWei);
|
|
171
|
+
const { wallet } = clients(chainId);
|
|
172
|
+
const hash = await wallet.sendTransaction({
|
|
173
|
+
to,
|
|
174
|
+
value: valueWei,
|
|
175
|
+
...(data ? { data } : {}),
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
tx_hash: hash,
|
|
179
|
+
from: getLocalAddress(),
|
|
180
|
+
to,
|
|
181
|
+
value: valueWei.toString(),
|
|
182
|
+
chain_id: chainId,
|
|
183
|
+
mode: 'local',
|
|
184
|
+
signed_locally: true,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** ERC-20 transfer, encoded locally. */
|
|
188
|
+
export async function localTransferToken(chainId, token, to, rawAmount) {
|
|
189
|
+
const data = encodeFunctionData({
|
|
190
|
+
abi: ERC20_ABI,
|
|
191
|
+
functionName: 'transfer',
|
|
192
|
+
args: [to, BigInt(rawAmount)],
|
|
193
|
+
});
|
|
194
|
+
const res = await localSend(chainId, token, 0n, data);
|
|
195
|
+
return { ...res, to, value: rawAmount };
|
|
196
|
+
}
|
|
197
|
+
/** ERC-20 approve, encoded locally. */
|
|
198
|
+
export async function localApproveToken(chainId, token, spender, rawAmount) {
|
|
199
|
+
const data = encodeFunctionData({
|
|
200
|
+
abi: ERC20_ABI,
|
|
201
|
+
functionName: 'approve',
|
|
202
|
+
args: [spender, BigInt(rawAmount)],
|
|
203
|
+
});
|
|
204
|
+
const res = await localSend(chainId, token, 0n, data);
|
|
205
|
+
return { ...res, to: spender, value: rawAmount };
|
|
206
|
+
}
|
|
207
|
+
/** Sign a message without broadcasting anything. */
|
|
208
|
+
export async function localSignMessage(message) {
|
|
209
|
+
const signature = await getLocalAccount().signMessage({ message });
|
|
210
|
+
return { address: getLocalAddress(), message, signature, mode: 'local' };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* One-line summary for tools that report wallet identity. Deliberately mirrors
|
|
214
|
+
* the shape of a custodial wallet record so callers do not need to branch.
|
|
215
|
+
*/
|
|
216
|
+
export function localWalletRecord() {
|
|
217
|
+
return {
|
|
218
|
+
id: 'local',
|
|
219
|
+
address: getLocalAddress(),
|
|
220
|
+
wallet_type: 'evm',
|
|
221
|
+
mode: 'local',
|
|
222
|
+
custody: 'self',
|
|
223
|
+
note: 'Signed in-process. The private key is never sent to AgentWallet servers.',
|
|
224
|
+
};
|
|
225
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentwallet-mcp",
|
|
3
3
|
"mcpName": "io.github.hifriendbot/agentwallet",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "MCP server for AgentWallet:
|
|
4
|
+
"version": "1.10.0",
|
|
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",
|
|
8
8
|
"bin": {
|
|
@@ -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",
|
|
19
|
+
"test": "node test/x402-payment.test.mjs && node test/local-mode.test.mjs",
|
|
20
20
|
"prepublishOnly": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
@@ -43,7 +43,10 @@
|
|
|
43
43
|
"x402",
|
|
44
44
|
"http-402",
|
|
45
45
|
"agent-payments",
|
|
46
|
-
"agentwallet"
|
|
46
|
+
"agentwallet",
|
|
47
|
+
"self-custody",
|
|
48
|
+
"non-custodial",
|
|
49
|
+
"local-signing"
|
|
47
50
|
],
|
|
48
51
|
"author": "HiFriendbot (https://hifriendbot.com)",
|
|
49
52
|
"license": "MIT",
|
|
@@ -53,6 +56,9 @@
|
|
|
53
56
|
},
|
|
54
57
|
"dependencies": {
|
|
55
58
|
"@modelcontextprotocol/sdk": "^1.11.4",
|
|
59
|
+
"@solana/web3.js": "^1.98.4",
|
|
60
|
+
"bs58": "^6.0.0",
|
|
61
|
+
"viem": "^2.55.10",
|
|
56
62
|
"zod": "^3.24.0"
|
|
57
63
|
},
|
|
58
64
|
"devDependencies": {
|