@agentlayer.tech/wallet 0.1.28 → 0.1.32
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/.openclaw/extensions/agent-wallet/README.md +5 -7
- package/.openclaw/extensions/agent-wallet/dist/index.js +35 -360
- package/.openclaw/extensions/agent-wallet/index.ts +35 -360
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +2 -45
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
- package/CHANGELOG.md +73 -0
- package/README.md +4 -0
- package/agent-wallet/.env.example +0 -12
- package/agent-wallet/README.md +18 -57
- package/agent-wallet/agent_wallet/bootstrap.py +28 -12
- package/agent-wallet/agent_wallet/btc_user_wallets.py +33 -7
- package/agent-wallet/agent_wallet/config.py +110 -29
- package/agent-wallet/agent_wallet/evm_user_wallets.py +4 -14
- package/agent-wallet/agent_wallet/openclaw_adapter.py +29 -687
- package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
- package/agent-wallet/agent_wallet/openclaw_runtime.py +3 -12
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
- package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
- package/agent-wallet/agent_wallet/providers/kamino.py +21 -4
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +0 -23
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
- package/agent-wallet/agent_wallet/providers/x402.py +4 -9
- package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
- package/agent-wallet/agent_wallet/user_wallets.py +4 -3
- package/agent-wallet/agent_wallet/wallet_layer/base.py +3 -103
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +8 -5
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +453 -1177
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +2 -8
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +2 -12
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +1 -1
- package/agent-wallet/examples/openclaw_user_wallet_example.py +1 -1
- package/agent-wallet/openclaw.plugin.json +1 -5
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +3 -5
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -12
- package/agent-wallet/scripts/build_release_bundle.py +1 -0
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1 -4
- package/agent-wallet/scripts/install_agent_wallet.py +114 -6
- package/agent-wallet/scripts/install_openclaw_local_config.py +10 -10
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +2 -4
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +2 -15
- package/agent-wallet/scripts/reveal_btc_seed.sh +7 -16
- package/agent-wallet/scripts/setup_btc_wallet.sh +7 -16
- package/agent-wallet/scripts/setup_evm_wallet.sh +1 -11
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +4 -1
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -6
- package/bin/openclaw-agent-wallet.mjs +356 -0
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +20 -0
- package/claude-code/plugins/agent-wallet/.mcp.json +14 -0
- package/claude-code/plugins/agent-wallet/README.md +65 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +39 -0
- package/claude-code/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +38 -0
- package/codex/plugins/agent-wallet/.mcp.json +15 -0
- package/codex/plugins/agent-wallet/README.md +39 -0
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +21 -0
- package/codex/plugins/agent-wallet/server.py +961 -0
- package/codex/plugins/agent-wallet/skills/wallet-operator/SKILL.md +18 -0
- package/hermes/plugins/agent_wallet/schemas.py +2 -2
- package/hermes/plugins/agent_wallet/tools.py +18 -4
- package/package.json +6 -1
- package/setup.sh +2 -0
- package/wdk-btc-wallet/src/local_vault.js +45 -68
- package/wdk-btc-wallet/src/server.js +1 -0
- package/wdk-evm-wallet/README.md +4 -3
- package/wdk-evm-wallet/src/config.js +15 -0
- package/wdk-evm-wallet/src/local_vault.js +45 -68
- package/wdk-evm-wallet/src/server.js +1 -0
- package/agent-wallet/agent_wallet/providers/houdini.py +0 -539
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "wallet-operator"
|
|
3
|
+
description: "Use when the user asks Codex to interact with the local AgentLayer wallet runtime. Prefer wallet tools over shell commands, preview writes first, and keep approval/signing semantics intact."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Wallet Operator
|
|
7
|
+
|
|
8
|
+
Use this plugin when the user wants Codex to work with the existing local AgentLayer wallet.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
|
|
12
|
+
- Do not create a new wallet unless the user explicitly asks for wallet provisioning outside this plugin.
|
|
13
|
+
- Prefer wallet tools over shelling out to chain CLIs, curl, or ad hoc scripts.
|
|
14
|
+
- For writes, start with `preview` or `intent_preview` when the tool supports it.
|
|
15
|
+
- Execute only after the user explicitly confirms the shown summary.
|
|
16
|
+
- On mainnet, restate the network, asset, amount, and destination before execute.
|
|
17
|
+
- Do not ask the user for `approval_token`. The bridge manages approval binding internally.
|
|
18
|
+
- If approval context is missing or stale, repeat preview instead of improvising.
|
|
@@ -48,7 +48,7 @@ AGENT_WALLET_INVOKE = {
|
|
|
48
48
|
},
|
|
49
49
|
"network": {
|
|
50
50
|
"type": "string",
|
|
51
|
-
"description": "Optional network override, such as
|
|
51
|
+
"description": "Optional network override, such as mainnet, bitcoin, ethereum, or base.",
|
|
52
52
|
},
|
|
53
53
|
"user_id": {
|
|
54
54
|
"type": "string",
|
|
@@ -149,7 +149,7 @@ AGENT_WALLET_EVM_STATUS = {
|
|
|
149
149
|
},
|
|
150
150
|
"network": {
|
|
151
151
|
"type": "string",
|
|
152
|
-
"description": "Optional EVM network hint, such as ethereum
|
|
152
|
+
"description": "Optional EVM network hint, such as ethereum or base.",
|
|
153
153
|
},
|
|
154
154
|
"service_url": {
|
|
155
155
|
"type": "string",
|
|
@@ -15,7 +15,7 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
SECRET_CONFIG_KEYS = {"privateKey", "masterKey", "approvalSecret"}
|
|
17
17
|
BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
|
|
18
|
-
PREVIEW_BOUND_SWAP_TOOLS = {"swap_solana_tokens"
|
|
18
|
+
PREVIEW_BOUND_SWAP_TOOLS = {"swap_solana_tokens"}
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _json(data: dict[str, Any]) -> str:
|
|
@@ -236,7 +236,15 @@ def _infer_backend_for_tool(tool_name: str) -> str | None:
|
|
|
236
236
|
or "jupiter" in tool_name
|
|
237
237
|
or "kamino" in tool_name
|
|
238
238
|
or "bags" in tool_name
|
|
239
|
-
or tool_name
|
|
239
|
+
or tool_name
|
|
240
|
+
in {
|
|
241
|
+
"transfer_sol",
|
|
242
|
+
"transfer_spl_token",
|
|
243
|
+
"sign_wallet_message",
|
|
244
|
+
"close_empty_token_accounts",
|
|
245
|
+
"get_wallet_portfolio",
|
|
246
|
+
"get_solana_token_prices",
|
|
247
|
+
}
|
|
240
248
|
):
|
|
241
249
|
return "solana_local"
|
|
242
250
|
return None
|
|
@@ -252,6 +260,8 @@ def _normalize_network_for_backend(backend: str, raw_network: Any) -> str:
|
|
|
252
260
|
"base-mainnet": "base",
|
|
253
261
|
}
|
|
254
262
|
normalized = aliases.get(network, network)
|
|
263
|
+
if normalized in {"sepolia", "base-sepolia", "base_sepolia"}:
|
|
264
|
+
raise RuntimeError("EVM testnets are no longer supported. Use ethereum or base.")
|
|
255
265
|
return normalized if normalized in {"ethereum", "base"} else "ethereum"
|
|
256
266
|
if backend == "wdk_btc_local":
|
|
257
267
|
aliases = {
|
|
@@ -261,7 +271,9 @@ def _normalize_network_for_backend(backend: str, raw_network: Any) -> str:
|
|
|
261
271
|
"mainnet": "bitcoin",
|
|
262
272
|
}
|
|
263
273
|
normalized = aliases.get(network, network)
|
|
264
|
-
|
|
274
|
+
if normalized in {"testnet", "regtest"}:
|
|
275
|
+
raise RuntimeError("Bitcoin testnet/regtest are no longer supported. Use bitcoin.")
|
|
276
|
+
return normalized if normalized == "bitcoin" else "bitcoin"
|
|
265
277
|
aliases = {
|
|
266
278
|
"solana": "mainnet",
|
|
267
279
|
"solana-mainnet": "mainnet",
|
|
@@ -269,7 +281,9 @@ def _normalize_network_for_backend(backend: str, raw_network: Any) -> str:
|
|
|
269
281
|
"mainnet-beta": "mainnet",
|
|
270
282
|
}
|
|
271
283
|
normalized = aliases.get(network, network)
|
|
272
|
-
|
|
284
|
+
if normalized in {"devnet", "testnet"}:
|
|
285
|
+
raise RuntimeError("Solana devnet/testnet are no longer supported. Use mainnet.")
|
|
286
|
+
return normalized if normalized == "mainnet" else "mainnet"
|
|
273
287
|
|
|
274
288
|
|
|
275
289
|
def _reject_secret_config(config: dict[str, Any]) -> None:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentlayer.tech/wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"agent-wallet/pyproject.toml",
|
|
43
43
|
".openclaw/AGENTS.md",
|
|
44
44
|
".openclaw/extensions/agent-wallet/",
|
|
45
|
+
"codex/plugins/agent-wallet/",
|
|
46
|
+
"claude-code/plugins/agent-wallet/",
|
|
45
47
|
"hermes/plugins/agent_wallet/",
|
|
46
48
|
"wdk-btc-wallet/src/",
|
|
47
49
|
"wdk-btc-wallet/bootstrap.sh",
|
|
@@ -59,6 +61,8 @@
|
|
|
59
61
|
"!agent-wallet/**/*.pyc",
|
|
60
62
|
"!hermes/**/__pycache__/**",
|
|
61
63
|
"!hermes/**/*.pyc",
|
|
64
|
+
"!codex/**/__pycache__/**",
|
|
65
|
+
"!codex/**/*.pyc",
|
|
62
66
|
"!agent-wallet/.pytest_cache/**",
|
|
63
67
|
"!agent-wallet/.runtime-venv/**",
|
|
64
68
|
"!**/node_modules/**",
|
|
@@ -67,6 +71,7 @@
|
|
|
67
71
|
],
|
|
68
72
|
"keywords": [
|
|
69
73
|
"openclaw",
|
|
74
|
+
"codex",
|
|
70
75
|
"agent-wallet",
|
|
71
76
|
"wallet",
|
|
72
77
|
"solana",
|
package/setup.sh
CHANGED
|
@@ -74,6 +74,8 @@ require_cmd npm
|
|
|
74
74
|
require_path "$INSTALLER" "Python installer"
|
|
75
75
|
require_path "${ROOT_DIR}/agent-wallet" "agent-wallet package"
|
|
76
76
|
require_path "${ROOT_DIR}/.openclaw/extensions/agent-wallet" "OpenClaw extension"
|
|
77
|
+
require_path "${ROOT_DIR}/codex/plugins/agent-wallet/.codex-plugin/plugin.json" "Codex plugin"
|
|
78
|
+
require_path "${ROOT_DIR}/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json" "Claude Code plugin"
|
|
77
79
|
require_path "${ROOT_DIR}/wdk-btc-wallet/package.json" "wdk-btc-wallet package"
|
|
78
80
|
require_path "${ROOT_DIR}/wdk-evm-wallet/package.json" "wdk-evm-wallet package"
|
|
79
81
|
|
|
@@ -26,14 +26,6 @@ function assertPositiveInteger(value, fieldName) {
|
|
|
26
26
|
return parsed;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function assertNonNegativeInteger(value, fieldName) {
|
|
30
|
-
const parsed = Number(value);
|
|
31
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
32
|
-
throw new Error(`${fieldName} must be a non-negative integer.`);
|
|
33
|
-
}
|
|
34
|
-
return parsed;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
function sanitizeLabel(label) {
|
|
38
30
|
const normalized = String(label ?? "").trim();
|
|
39
31
|
return normalized || "BTC Wallet";
|
|
@@ -67,6 +59,7 @@ async function encryptSeedPhrase({ seedPhrase, password, walletId }) {
|
|
|
67
59
|
cipher.final(),
|
|
68
60
|
]);
|
|
69
61
|
const tag = cipher.getAuthTag();
|
|
62
|
+
key.fill(0);
|
|
70
63
|
return {
|
|
71
64
|
version: VAULT_VERSION,
|
|
72
65
|
kdf: {
|
|
@@ -95,7 +88,12 @@ async function decryptSeedPhrase({ encrypted, password, walletId }) {
|
|
|
95
88
|
decipher.setAAD(Buffer.from(`wdk-btc-wallet:${walletId}:v${VAULT_VERSION}`, "utf8"));
|
|
96
89
|
decipher.setAuthTag(tag);
|
|
97
90
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
98
|
-
|
|
91
|
+
key.fill(0);
|
|
92
|
+
// The returned string is an unavoidable transient (V8 strings are immutable);
|
|
93
|
+
// the derived key and plaintext buffers are zeroized so no zeroizable secret lingers.
|
|
94
|
+
const seedPhrase = plaintext.toString("utf8");
|
|
95
|
+
plaintext.fill(0);
|
|
96
|
+
return seedPhrase;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
async function decryptSeedPhraseWithPasswordCheck(args) {
|
|
@@ -117,7 +115,6 @@ async function decryptSeedPhraseWithPasswordCheck(args) {
|
|
|
117
115
|
export class LocalBtcVault {
|
|
118
116
|
constructor(config) {
|
|
119
117
|
this.config = config;
|
|
120
|
-
this._unlocked = new Map();
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
async createWallet({
|
|
@@ -139,10 +136,9 @@ export class LocalBtcVault {
|
|
|
139
136
|
source: "created",
|
|
140
137
|
network,
|
|
141
138
|
});
|
|
142
|
-
await this.unlockWallet({ walletId: wallet.walletId, password, timeoutSeconds: 0 });
|
|
143
139
|
return {
|
|
144
140
|
...wallet,
|
|
145
|
-
unlocked:
|
|
141
|
+
unlocked: false,
|
|
146
142
|
unlockExpiresAt: null,
|
|
147
143
|
...(revealSeedPhrase ? { seedPhrase } : {}),
|
|
148
144
|
};
|
|
@@ -160,39 +156,35 @@ export class LocalBtcVault {
|
|
|
160
156
|
source: "imported",
|
|
161
157
|
network,
|
|
162
158
|
});
|
|
163
|
-
await this.unlockWallet({ walletId: wallet.walletId, password, timeoutSeconds: 0 });
|
|
164
159
|
return {
|
|
165
160
|
...wallet,
|
|
166
|
-
unlocked:
|
|
161
|
+
unlocked: false,
|
|
167
162
|
unlockExpiresAt: null,
|
|
168
163
|
};
|
|
169
164
|
}
|
|
170
165
|
|
|
171
166
|
async listWallets() {
|
|
172
|
-
this.#sweepExpiredUnlocked();
|
|
173
167
|
const registry = await this.#loadRegistry();
|
|
174
|
-
return registry.wallets.map((wallet) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
unlockExpiresAt: unlocked ? unlocked.expiresAt : null,
|
|
180
|
-
};
|
|
181
|
-
});
|
|
168
|
+
return registry.wallets.map((wallet) => ({
|
|
169
|
+
...wallet,
|
|
170
|
+
unlocked: false,
|
|
171
|
+
unlockExpiresAt: null,
|
|
172
|
+
}));
|
|
182
173
|
}
|
|
183
174
|
|
|
184
175
|
async getWallet({ walletId }) {
|
|
185
|
-
this.#sweepExpiredUnlocked();
|
|
186
176
|
const wallet = await this.#getWalletMetadata(assertNonEmptyString(walletId, "walletId"));
|
|
187
|
-
const unlocked = this._unlocked.get(wallet.walletId);
|
|
188
177
|
return {
|
|
189
178
|
...wallet,
|
|
190
|
-
unlocked:
|
|
191
|
-
unlockExpiresAt:
|
|
179
|
+
unlocked: false,
|
|
180
|
+
unlockExpiresAt: null,
|
|
192
181
|
};
|
|
193
182
|
}
|
|
194
183
|
|
|
195
|
-
|
|
184
|
+
// Deprecated: the wallet now uses a decrypt-on-demand model and never holds a
|
|
185
|
+
// plaintext seed in memory between requests. This endpoint only verifies the
|
|
186
|
+
// password so callers get feedback; it does not persist any unlocked state.
|
|
187
|
+
async unlockWallet({ walletId, password }) {
|
|
196
188
|
const metadata = await this.#getWalletMetadata(assertNonEmptyString(walletId, "walletId"));
|
|
197
189
|
const encrypted = await this.#loadEncryptedWallet(walletId);
|
|
198
190
|
const secret = await decryptSeedPhraseWithPasswordCheck({
|
|
@@ -203,28 +195,21 @@ export class LocalBtcVault {
|
|
|
203
195
|
if (!WDK.isValidSeed(secret)) {
|
|
204
196
|
throw new Error("Decrypted wallet seed phrase is invalid.");
|
|
205
197
|
}
|
|
206
|
-
const ttl =
|
|
207
|
-
timeoutSeconds === undefined || timeoutSeconds === null
|
|
208
|
-
? this.config.unlockTimeoutSeconds
|
|
209
|
-
: assertNonNegativeInteger(timeoutSeconds, "timeoutSeconds");
|
|
210
|
-
const expiresAt = ttl === 0 ? null : new Date(Date.now() + ttl * 1000).toISOString();
|
|
211
|
-
this._unlocked.set(walletId, {
|
|
212
|
-
seedPhrase: secret,
|
|
213
|
-
expiresAt,
|
|
214
|
-
});
|
|
215
198
|
return {
|
|
216
199
|
walletId,
|
|
217
200
|
label: metadata.label,
|
|
218
|
-
unlocked:
|
|
219
|
-
unlockExpiresAt:
|
|
201
|
+
unlocked: false,
|
|
202
|
+
unlockExpiresAt: null,
|
|
203
|
+
deprecated: true,
|
|
220
204
|
};
|
|
221
205
|
}
|
|
222
206
|
|
|
207
|
+
// Deprecated no-op: the wallet is always sealed at rest in the decrypt-on-demand model.
|
|
223
208
|
async lockWallet({ walletId }) {
|
|
224
|
-
this._unlocked.delete(assertNonEmptyString(walletId, "walletId"));
|
|
225
209
|
return {
|
|
226
|
-
walletId,
|
|
210
|
+
walletId: assertNonEmptyString(walletId, "walletId"),
|
|
227
211
|
unlocked: false,
|
|
212
|
+
deprecated: true,
|
|
228
213
|
};
|
|
229
214
|
}
|
|
230
215
|
|
|
@@ -283,25 +268,19 @@ export class LocalBtcVault {
|
|
|
283
268
|
};
|
|
284
269
|
await this.#saveRegistry(registry);
|
|
285
270
|
|
|
286
|
-
const unlocked = this._unlocked.get(id);
|
|
287
|
-
if (unlocked) {
|
|
288
|
-
this._unlocked.set(id, {
|
|
289
|
-
seedPhrase,
|
|
290
|
-
expiresAt: unlocked.expiresAt,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
271
|
return {
|
|
295
272
|
walletId: id,
|
|
296
273
|
label: metadata.label,
|
|
297
274
|
passwordChanged: true,
|
|
298
275
|
updatedAt,
|
|
299
|
-
unlocked:
|
|
300
|
-
unlockExpiresAt:
|
|
276
|
+
unlocked: false,
|
|
277
|
+
unlockExpiresAt: null,
|
|
301
278
|
};
|
|
302
279
|
}
|
|
303
280
|
|
|
304
|
-
|
|
281
|
+
// Decrypt-on-demand: the seed is decrypted just-in-time for a single signing
|
|
282
|
+
// request from the supplied password, never persisted between requests.
|
|
283
|
+
async resolveSeedPhrase({ walletId, seedPhrase, password }) {
|
|
305
284
|
if (typeof seedPhrase === "string" && seedPhrase.trim()) {
|
|
306
285
|
if (!WDK.isValidSeed(seedPhrase.trim())) {
|
|
307
286
|
throw new Error("seedPhrase must be a valid BIP-39 seed phrase.");
|
|
@@ -313,16 +292,23 @@ export class LocalBtcVault {
|
|
|
313
292
|
};
|
|
314
293
|
}
|
|
315
294
|
const id = assertNonEmptyString(walletId, "walletId");
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
295
|
+
if (typeof password !== "string" || !password.trim()) {
|
|
296
|
+
throw new Error("Wallet is locked. Provide password or seedPhrase explicitly.");
|
|
297
|
+
}
|
|
298
|
+
await this.#getWalletMetadata(id);
|
|
299
|
+
const encrypted = await this.#loadEncryptedWallet(id);
|
|
300
|
+
const secret = await decryptSeedPhraseWithPasswordCheck({
|
|
301
|
+
encrypted,
|
|
302
|
+
password: password.trim(),
|
|
303
|
+
walletId: id,
|
|
304
|
+
});
|
|
305
|
+
if (!WDK.isValidSeed(secret)) {
|
|
306
|
+
throw new Error("Decrypted wallet seed phrase is invalid.");
|
|
320
307
|
}
|
|
321
308
|
return {
|
|
322
|
-
seedPhrase:
|
|
323
|
-
source: "local-vault",
|
|
309
|
+
seedPhrase: secret,
|
|
310
|
+
source: "local-vault-jit",
|
|
324
311
|
walletId: id,
|
|
325
|
-
unlockExpiresAt: unlocked.expiresAt,
|
|
326
312
|
};
|
|
327
313
|
}
|
|
328
314
|
|
|
@@ -420,13 +406,4 @@ export class LocalBtcVault {
|
|
|
420
406
|
#registryPath() {
|
|
421
407
|
return path.join(this.config.dataDir, REGISTRY_FILE);
|
|
422
408
|
}
|
|
423
|
-
|
|
424
|
-
#sweepExpiredUnlocked() {
|
|
425
|
-
const now = Date.now();
|
|
426
|
-
for (const [walletId, state] of this._unlocked.entries()) {
|
|
427
|
-
if (state.expiresAt && Date.parse(state.expiresAt) <= now) {
|
|
428
|
-
this._unlocked.delete(walletId);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
409
|
}
|
package/wdk-evm-wallet/README.md
CHANGED
|
@@ -165,9 +165,10 @@ Gateway mode:
|
|
|
165
165
|
- `PROVIDER_GATEWAY_URL` defaults to `https://agent-layer-production.up.railway.app`
|
|
166
166
|
- set `PROVIDER_GATEWAY_URL=https://...` only when overriding the hosted default
|
|
167
167
|
- `PROVIDER_GATEWAY_BEARER_TOKEN` is optional and only needed when the gateway is protected
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
168
|
+
- `ethereum` and `base` mainnet are always routed through the provider gateway raw EVM RPC route
|
|
169
|
+
- `ethereum` and `base` mainnet are pinned to the gateway `provider=alchemy` path
|
|
170
|
+
- direct `WDK_EVM_ETHEREUM_RPC_URL` and `WDK_EVM_BASE_RPC_URL` values no longer override mainnet routing
|
|
171
|
+
- `WDK_EVM_SEPOLIA_RPC_URL` and `WDK_EVM_BASE_SEPOLIA_RPC_URL` remain direct per-network testnet overrides
|
|
171
172
|
|
|
172
173
|
Local security note:
|
|
173
174
|
|
|
@@ -10,6 +10,7 @@ const DEFAULTS = {
|
|
|
10
10
|
unlockTimeoutSeconds: 0,
|
|
11
11
|
};
|
|
12
12
|
const DEFAULT_PROVIDER_GATEWAY_URL = "https://agent-layer-production.up.railway.app";
|
|
13
|
+
const ENFORCED_GATEWAY_MAINNETS = new Set(["ethereum", "base"]);
|
|
13
14
|
|
|
14
15
|
const DEFAULT_NETWORK_PROFILES = {
|
|
15
16
|
ethereum: {
|
|
@@ -191,6 +192,20 @@ export function loadConfig(env = process.env) {
|
|
|
191
192
|
|
|
192
193
|
function resolveProviderUrl(networkKey, envValue, fallbackUrl) {
|
|
193
194
|
const direct = String(envValue ?? "").trim();
|
|
195
|
+
if (ENFORCED_GATEWAY_MAINNETS.has(networkKey)) {
|
|
196
|
+
const enforcedGatewayUrl = buildGatewayEvmRpcUrl(
|
|
197
|
+
providerGatewayUrl,
|
|
198
|
+
networkKey,
|
|
199
|
+
"alchemy",
|
|
200
|
+
providerGatewayToken
|
|
201
|
+
);
|
|
202
|
+
if (!enforcedGatewayUrl) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`PROVIDER_GATEWAY_URL is required for ${networkKey} mainnet RPC routing.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return enforcedGatewayUrl;
|
|
208
|
+
}
|
|
194
209
|
if (direct) {
|
|
195
210
|
return direct;
|
|
196
211
|
}
|
|
@@ -26,14 +26,6 @@ function assertPositiveInteger(value, fieldName) {
|
|
|
26
26
|
return parsed;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function assertNonNegativeInteger(value, fieldName) {
|
|
30
|
-
const parsed = Number(value);
|
|
31
|
-
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
32
|
-
throw new Error(`${fieldName} must be a non-negative integer.`);
|
|
33
|
-
}
|
|
34
|
-
return parsed;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
function sanitizeLabel(label) {
|
|
38
30
|
const normalized = String(label ?? "").trim();
|
|
39
31
|
return normalized || "EVM Wallet";
|
|
@@ -67,6 +59,7 @@ async function encryptSeedPhrase({ seedPhrase, password, walletId }) {
|
|
|
67
59
|
cipher.final(),
|
|
68
60
|
]);
|
|
69
61
|
const tag = cipher.getAuthTag();
|
|
62
|
+
key.fill(0);
|
|
70
63
|
return {
|
|
71
64
|
version: VAULT_VERSION,
|
|
72
65
|
kdf: {
|
|
@@ -95,7 +88,12 @@ async function decryptSeedPhrase({ encrypted, password, walletId }) {
|
|
|
95
88
|
decipher.setAAD(Buffer.from(`wdk-evm-wallet:${walletId}:v${VAULT_VERSION}`, "utf8"));
|
|
96
89
|
decipher.setAuthTag(tag);
|
|
97
90
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
98
|
-
|
|
91
|
+
key.fill(0);
|
|
92
|
+
// The returned string is an unavoidable transient (V8 strings are immutable);
|
|
93
|
+
// the derived key and plaintext buffers are zeroized so no zeroizable secret lingers.
|
|
94
|
+
const seedPhrase = plaintext.toString("utf8");
|
|
95
|
+
plaintext.fill(0);
|
|
96
|
+
return seedPhrase;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
async function decryptSeedPhraseWithPasswordCheck(args) {
|
|
@@ -117,7 +115,6 @@ async function decryptSeedPhraseWithPasswordCheck(args) {
|
|
|
117
115
|
export class LocalEvmVault {
|
|
118
116
|
constructor(config) {
|
|
119
117
|
this.config = config;
|
|
120
|
-
this._unlocked = new Map();
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
async createWallet({
|
|
@@ -139,10 +136,9 @@ export class LocalEvmVault {
|
|
|
139
136
|
source: "created",
|
|
140
137
|
network,
|
|
141
138
|
});
|
|
142
|
-
await this.unlockWallet({ walletId: wallet.walletId, password, timeoutSeconds: 0 });
|
|
143
139
|
return {
|
|
144
140
|
...wallet,
|
|
145
|
-
unlocked:
|
|
141
|
+
unlocked: false,
|
|
146
142
|
unlockExpiresAt: null,
|
|
147
143
|
...(revealSeedPhrase ? { seedPhrase } : {}),
|
|
148
144
|
};
|
|
@@ -160,39 +156,35 @@ export class LocalEvmVault {
|
|
|
160
156
|
source: "imported",
|
|
161
157
|
network,
|
|
162
158
|
});
|
|
163
|
-
await this.unlockWallet({ walletId: wallet.walletId, password, timeoutSeconds: 0 });
|
|
164
159
|
return {
|
|
165
160
|
...wallet,
|
|
166
|
-
unlocked:
|
|
161
|
+
unlocked: false,
|
|
167
162
|
unlockExpiresAt: null,
|
|
168
163
|
};
|
|
169
164
|
}
|
|
170
165
|
|
|
171
166
|
async listWallets() {
|
|
172
|
-
this.#sweepExpiredUnlocked();
|
|
173
167
|
const registry = await this.#loadRegistry();
|
|
174
|
-
return registry.wallets.map((wallet) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
unlockExpiresAt: unlocked ? unlocked.expiresAt : null,
|
|
180
|
-
};
|
|
181
|
-
});
|
|
168
|
+
return registry.wallets.map((wallet) => ({
|
|
169
|
+
...wallet,
|
|
170
|
+
unlocked: false,
|
|
171
|
+
unlockExpiresAt: null,
|
|
172
|
+
}));
|
|
182
173
|
}
|
|
183
174
|
|
|
184
175
|
async getWallet({ walletId }) {
|
|
185
|
-
this.#sweepExpiredUnlocked();
|
|
186
176
|
const wallet = await this.#getWalletMetadata(assertNonEmptyString(walletId, "walletId"));
|
|
187
|
-
const unlocked = this._unlocked.get(wallet.walletId);
|
|
188
177
|
return {
|
|
189
178
|
...wallet,
|
|
190
|
-
unlocked:
|
|
191
|
-
unlockExpiresAt:
|
|
179
|
+
unlocked: false,
|
|
180
|
+
unlockExpiresAt: null,
|
|
192
181
|
};
|
|
193
182
|
}
|
|
194
183
|
|
|
195
|
-
|
|
184
|
+
// Deprecated: the wallet now uses a decrypt-on-demand model and never holds a
|
|
185
|
+
// plaintext seed in memory between requests. This endpoint only verifies the
|
|
186
|
+
// password so callers get feedback; it does not persist any unlocked state.
|
|
187
|
+
async unlockWallet({ walletId, password }) {
|
|
196
188
|
const metadata = await this.#getWalletMetadata(assertNonEmptyString(walletId, "walletId"));
|
|
197
189
|
const encrypted = await this.#loadEncryptedWallet(walletId);
|
|
198
190
|
const secret = await decryptSeedPhraseWithPasswordCheck({
|
|
@@ -203,28 +195,21 @@ export class LocalEvmVault {
|
|
|
203
195
|
if (!WDK.isValidSeed(secret)) {
|
|
204
196
|
throw new Error("Decrypted wallet seed phrase is invalid.");
|
|
205
197
|
}
|
|
206
|
-
const ttl =
|
|
207
|
-
timeoutSeconds === undefined || timeoutSeconds === null
|
|
208
|
-
? this.config.unlockTimeoutSeconds
|
|
209
|
-
: assertNonNegativeInteger(timeoutSeconds, "timeoutSeconds");
|
|
210
|
-
const expiresAt = ttl === 0 ? null : new Date(Date.now() + ttl * 1000).toISOString();
|
|
211
|
-
this._unlocked.set(walletId, {
|
|
212
|
-
seedPhrase: secret,
|
|
213
|
-
expiresAt,
|
|
214
|
-
});
|
|
215
198
|
return {
|
|
216
199
|
walletId,
|
|
217
200
|
label: metadata.label,
|
|
218
|
-
unlocked:
|
|
219
|
-
unlockExpiresAt:
|
|
201
|
+
unlocked: false,
|
|
202
|
+
unlockExpiresAt: null,
|
|
203
|
+
deprecated: true,
|
|
220
204
|
};
|
|
221
205
|
}
|
|
222
206
|
|
|
207
|
+
// Deprecated no-op: the wallet is always sealed at rest in the decrypt-on-demand model.
|
|
223
208
|
async lockWallet({ walletId }) {
|
|
224
|
-
this._unlocked.delete(assertNonEmptyString(walletId, "walletId"));
|
|
225
209
|
return {
|
|
226
|
-
walletId,
|
|
210
|
+
walletId: assertNonEmptyString(walletId, "walletId"),
|
|
227
211
|
unlocked: false,
|
|
212
|
+
deprecated: true,
|
|
228
213
|
};
|
|
229
214
|
}
|
|
230
215
|
|
|
@@ -283,25 +268,19 @@ export class LocalEvmVault {
|
|
|
283
268
|
};
|
|
284
269
|
await this.#saveRegistry(registry);
|
|
285
270
|
|
|
286
|
-
const unlocked = this._unlocked.get(id);
|
|
287
|
-
if (unlocked) {
|
|
288
|
-
this._unlocked.set(id, {
|
|
289
|
-
seedPhrase,
|
|
290
|
-
expiresAt: unlocked.expiresAt,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
271
|
return {
|
|
295
272
|
walletId: id,
|
|
296
273
|
label: metadata.label,
|
|
297
274
|
passwordChanged: true,
|
|
298
275
|
updatedAt,
|
|
299
|
-
unlocked:
|
|
300
|
-
unlockExpiresAt:
|
|
276
|
+
unlocked: false,
|
|
277
|
+
unlockExpiresAt: null,
|
|
301
278
|
};
|
|
302
279
|
}
|
|
303
280
|
|
|
304
|
-
|
|
281
|
+
// Decrypt-on-demand: the seed is decrypted just-in-time for a single signing
|
|
282
|
+
// request from the supplied password, never persisted between requests.
|
|
283
|
+
async resolveSeedPhrase({ walletId, seedPhrase, password }) {
|
|
305
284
|
if (typeof seedPhrase === "string" && seedPhrase.trim()) {
|
|
306
285
|
if (!WDK.isValidSeed(seedPhrase.trim())) {
|
|
307
286
|
throw new Error("seedPhrase must be a valid BIP-39 seed phrase.");
|
|
@@ -313,16 +292,23 @@ export class LocalEvmVault {
|
|
|
313
292
|
};
|
|
314
293
|
}
|
|
315
294
|
const id = assertNonEmptyString(walletId, "walletId");
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
295
|
+
if (typeof password !== "string" || !password.trim()) {
|
|
296
|
+
throw new Error("Wallet is locked. Provide password or seedPhrase explicitly.");
|
|
297
|
+
}
|
|
298
|
+
await this.#getWalletMetadata(id);
|
|
299
|
+
const encrypted = await this.#loadEncryptedWallet(id);
|
|
300
|
+
const secret = await decryptSeedPhraseWithPasswordCheck({
|
|
301
|
+
encrypted,
|
|
302
|
+
password: password.trim(),
|
|
303
|
+
walletId: id,
|
|
304
|
+
});
|
|
305
|
+
if (!WDK.isValidSeed(secret)) {
|
|
306
|
+
throw new Error("Decrypted wallet seed phrase is invalid.");
|
|
320
307
|
}
|
|
321
308
|
return {
|
|
322
|
-
seedPhrase:
|
|
323
|
-
source: "local-vault",
|
|
309
|
+
seedPhrase: secret,
|
|
310
|
+
source: "local-vault-jit",
|
|
324
311
|
walletId: id,
|
|
325
|
-
unlockExpiresAt: unlocked.expiresAt,
|
|
326
312
|
};
|
|
327
313
|
}
|
|
328
314
|
|
|
@@ -418,13 +404,4 @@ export class LocalEvmVault {
|
|
|
418
404
|
#registryPath() {
|
|
419
405
|
return path.join(this.config.dataDir, REGISTRY_FILE);
|
|
420
406
|
}
|
|
421
|
-
|
|
422
|
-
#sweepExpiredUnlocked() {
|
|
423
|
-
const now = Date.now();
|
|
424
|
-
for (const [walletId, state] of this._unlocked.entries()) {
|
|
425
|
-
if (state.expiresAt && Date.parse(state.expiresAt) <= now) {
|
|
426
|
-
this._unlocked.delete(walletId);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
407
|
}
|