@agentlayer.tech/wallet 0.1.30 → 0.1.33
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 +1 -2
- package/.openclaw/extensions/agent-wallet/dist/index.js +6 -340
- package/.openclaw/extensions/agent-wallet/index.ts +6 -340
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +0 -43
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
- package/CHANGELOG.md +60 -0
- package/README.md +0 -5
- package/agent-wallet/.env.example +0 -12
- package/agent-wallet/README.md +0 -35
- package/agent-wallet/agent_wallet/btc_user_wallets.py +32 -1
- package/agent-wallet/agent_wallet/config.py +11 -7
- package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +1 -655
- package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
- 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/wdk_btc_local.py +31 -3
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
- package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
- package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -100
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1118
- package/agent-wallet/openclaw.plugin.json +0 -4
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/install_agent_wallet.py +113 -6
- package/agent-wallet/scripts/install_openclaw_local_config.py +7 -5
- package/agent-wallet/skills/wallet-operator/SKILL.md +1 -5
- package/bin/openclaw-agent-wallet.mjs +434 -68
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +21 -3
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +18 -0
- package/codex/plugins/agent-wallet/server.py +2 -118
- package/hermes/plugins/agent_wallet/tools.py +1 -1
- package/package.json +1 -1
- 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
|
@@ -46,27 +46,17 @@ HOST_DEFAULT_CONFIG_KEYS = {
|
|
|
46
46
|
"jupiterUltraBaseUrl",
|
|
47
47
|
"jupiterPriceBaseUrl",
|
|
48
48
|
"jupiterPortfolioBaseUrl",
|
|
49
|
-
"jupiterLendBaseUrl",
|
|
50
49
|
"jupiterApiKey",
|
|
51
|
-
"houdiniBaseUrl",
|
|
52
|
-
"houdiniApiKey",
|
|
53
|
-
"houdiniApiSecret",
|
|
54
|
-
"houdiniUserIp",
|
|
55
|
-
"houdiniUserAgent",
|
|
56
|
-
"houdiniUserTimezone",
|
|
57
50
|
"kaminoBaseUrl",
|
|
58
51
|
"kaminoProgramId",
|
|
59
52
|
}
|
|
60
53
|
BACKENDS = ("solana_local", "wdk_btc_local", "wdk_evm_local")
|
|
61
54
|
PREVIEW_CACHE_TTL_SECONDS = 15 * 60
|
|
62
|
-
PRIVATE_SWAP_CACHE_TTL_SECONDS = 35 * 60
|
|
63
55
|
PREVIEW_BOUND_SWAP_TOOLS = {
|
|
64
56
|
"swap_solana_tokens",
|
|
65
|
-
"swap_solana_privately",
|
|
66
57
|
"flash_trade_open_position",
|
|
67
58
|
"flash_trade_close_position",
|
|
68
59
|
}
|
|
69
|
-
PRIVATE_SWAP_APPROVAL_TOOL_NAME = "swap_solana_privately"
|
|
70
60
|
APPROVAL_PREVIEW_TOOL_ALIASES = {
|
|
71
61
|
"x402_pay_request": "x402_preview_request",
|
|
72
62
|
}
|
|
@@ -81,7 +71,6 @@ selected_solana_network: str | None = None
|
|
|
81
71
|
selected_evm_network: str | None = None
|
|
82
72
|
selected_btc_network: str | None = None
|
|
83
73
|
approval_preview_cache: dict[str, dict[str, Any]] = {}
|
|
84
|
-
private_swap_order_cache: dict[str, dict[str, Any]] = {}
|
|
85
74
|
|
|
86
75
|
|
|
87
76
|
class WalletCliError(RuntimeError):
|
|
@@ -245,17 +234,10 @@ def _cache_preview_for_approval(user_id: str, tool_name: str, payload: dict[str,
|
|
|
245
234
|
_prune_approval_preview_cache()
|
|
246
235
|
approval_preview_cache[_approval_cache_key(user_id, cache_tool_name)] = {
|
|
247
236
|
"digest": _preview_digest(data),
|
|
248
|
-
"expires_at": time.time()
|
|
249
|
-
+ (
|
|
250
|
-
PRIVATE_SWAP_CACHE_TTL_SECONDS
|
|
251
|
-
if cache_tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME
|
|
252
|
-
else PREVIEW_CACHE_TTL_SECONDS
|
|
253
|
-
),
|
|
237
|
+
"expires_at": time.time() + PREVIEW_CACHE_TTL_SECONDS,
|
|
254
238
|
"preview": data,
|
|
255
239
|
"summary": summary,
|
|
256
240
|
}
|
|
257
|
-
if cache_tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
258
|
-
private_swap_order_cache.pop(_approval_cache_key(user_id, cache_tool_name), None)
|
|
259
241
|
|
|
260
242
|
|
|
261
243
|
def _latest_cached_preview(user_id: str, tool_name: str) -> dict[str, Any] | None:
|
|
@@ -290,66 +272,6 @@ def _cached_preview_for_token(user_id: str, tool_name: str, token: str) -> dict[
|
|
|
290
272
|
return preview if isinstance(preview, dict) else None
|
|
291
273
|
|
|
292
274
|
|
|
293
|
-
def _cache_pending_private_swap_order(
|
|
294
|
-
user_id: str,
|
|
295
|
-
tool_name: str,
|
|
296
|
-
preview: dict[str, Any],
|
|
297
|
-
details: dict[str, Any],
|
|
298
|
-
) -> None:
|
|
299
|
-
if tool_name != PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
300
|
-
return
|
|
301
|
-
houdini_id = str(details.get("houdini_id") or "").strip()
|
|
302
|
-
deposit_address = str(details.get("deposit_address") or "").strip()
|
|
303
|
-
if not houdini_id or not deposit_address:
|
|
304
|
-
return
|
|
305
|
-
private_swap_order_cache[_approval_cache_key(user_id, tool_name)] = {
|
|
306
|
-
"digest": _preview_digest(preview),
|
|
307
|
-
"expires_at": time.time() + PRIVATE_SWAP_CACHE_TTL_SECONDS,
|
|
308
|
-
"order": {
|
|
309
|
-
"multi_id": str(details.get("multi_id") or "").strip() or None,
|
|
310
|
-
"houdini_id": houdini_id,
|
|
311
|
-
"deposit_address": deposit_address,
|
|
312
|
-
"order": details.get("order") if isinstance(details.get("order"), dict) else {},
|
|
313
|
-
},
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _latest_pending_private_swap_order(
|
|
318
|
-
user_id: str,
|
|
319
|
-
tool_name: str,
|
|
320
|
-
preview: dict[str, Any],
|
|
321
|
-
) -> dict[str, Any] | None:
|
|
322
|
-
if tool_name != PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
323
|
-
return None
|
|
324
|
-
cached = private_swap_order_cache.get(_approval_cache_key(user_id, tool_name))
|
|
325
|
-
if not cached:
|
|
326
|
-
return None
|
|
327
|
-
if float(cached.get("expires_at") or 0) <= time.time():
|
|
328
|
-
private_swap_order_cache.pop(_approval_cache_key(user_id, tool_name), None)
|
|
329
|
-
return None
|
|
330
|
-
if cached.get("digest") != _preview_digest(preview):
|
|
331
|
-
return None
|
|
332
|
-
order = cached.get("order")
|
|
333
|
-
return order if isinstance(order, dict) else None
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def _clear_pending_private_swap_order(user_id: str, tool_name: str) -> None:
|
|
337
|
-
if tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
338
|
-
private_swap_order_cache.pop(_approval_cache_key(user_id, tool_name), None)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _list_pending_private_swap_orders(user_id: str) -> list[dict[str, Any]]:
|
|
342
|
-
key = _approval_cache_key(user_id, PRIVATE_SWAP_APPROVAL_TOOL_NAME)
|
|
343
|
-
pending = private_swap_order_cache.get(key)
|
|
344
|
-
if not pending or float(pending.get("expires_at") or 0) <= time.time():
|
|
345
|
-
private_swap_order_cache.pop(key, None)
|
|
346
|
-
return []
|
|
347
|
-
order = pending.get("order")
|
|
348
|
-
if not isinstance(order, dict):
|
|
349
|
-
return []
|
|
350
|
-
return [{**order, "expires_at_ms": int(float(pending["expires_at"]) * 1000)}]
|
|
351
|
-
|
|
352
|
-
|
|
353
275
|
def _normalize_wallet_backend(value: Any) -> str:
|
|
354
276
|
normalized = str(value or "").strip().lower()
|
|
355
277
|
aliases = {
|
|
@@ -651,8 +573,6 @@ def _issue_approval_token(
|
|
|
651
573
|
]
|
|
652
574
|
if preview_payload.get("is_mainnet") is True:
|
|
653
575
|
extra_args.append("--mainnet-confirmed")
|
|
654
|
-
if tool_name == PRIVATE_SWAP_APPROVAL_TOOL_NAME:
|
|
655
|
-
extra_args.extend(["--ttl-seconds", "1800"])
|
|
656
576
|
payload = _call_wallet_cli("issue-approval", extra_args)
|
|
657
577
|
token = str(payload.get("approval_token") or "").strip()
|
|
658
578
|
if not token:
|
|
@@ -966,9 +886,6 @@ async def _handle_set_evm_network(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
966
886
|
|
|
967
887
|
|
|
968
888
|
async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
969
|
-
if tool_name == "list_pending_solana_private_swaps":
|
|
970
|
-
return {"orders": _list_pending_private_swap_orders(_user_id())}
|
|
971
|
-
|
|
972
889
|
config = _base_config(params, tool_name=tool_name)
|
|
973
890
|
backend = _normalize_wallet_backend(config.get("backend"))
|
|
974
891
|
if backend == "wdk_evm_local" and params.get("network") is None and selected_evm_network:
|
|
@@ -976,24 +893,7 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
|
|
|
976
893
|
config["network"] = selected_evm_network
|
|
977
894
|
|
|
978
895
|
effective_params = dict(params)
|
|
979
|
-
|
|
980
|
-
_attach_approval_for_execute(tool_name, config, effective_params)
|
|
981
|
-
else:
|
|
982
|
-
cached = _latest_cached_preview(_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME)
|
|
983
|
-
if cached and isinstance(cached.get("preview"), dict):
|
|
984
|
-
effective_params["_approved_preview"] = cached["preview"]
|
|
985
|
-
effective_params["approval_token"] = _issue_approval_token(
|
|
986
|
-
PRIVATE_SWAP_APPROVAL_TOOL_NAME,
|
|
987
|
-
config,
|
|
988
|
-
cached["preview"],
|
|
989
|
-
)
|
|
990
|
-
pending = _latest_pending_private_swap_order(
|
|
991
|
-
_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME, cached["preview"]
|
|
992
|
-
)
|
|
993
|
-
if pending and effective_params.get("_resume_private_swap_order") is None:
|
|
994
|
-
effective_params["_resume_private_swap_order"] = pending
|
|
995
|
-
elif not effective_params.get("approval_token"):
|
|
996
|
-
raise RuntimeError(APPROVAL_CONTEXT_MISSING_MESSAGE)
|
|
896
|
+
_attach_approval_for_execute(tool_name, config, effective_params)
|
|
997
897
|
|
|
998
898
|
try:
|
|
999
899
|
payload = _invoke_tool(tool_name, effective_params, config)
|
|
@@ -1001,22 +901,6 @@ async def _handle_wallet_tool(tool_name: str, params: dict[str, Any]) -> dict[st
|
|
|
1001
901
|
raise _normalize_approval_context_error(exc) from exc
|
|
1002
902
|
|
|
1003
903
|
_cache_preview_for_approval(_user_id(), tool_name, payload)
|
|
1004
|
-
if tool_name == "swap_solana_privately" and payload.get("ok") is True:
|
|
1005
|
-
data = payload.get("data")
|
|
1006
|
-
approved_preview = effective_params.get("_approved_preview")
|
|
1007
|
-
if (
|
|
1008
|
-
isinstance(data, dict)
|
|
1009
|
-
and data.get("execution_state") == "awaiting_deposit_funding"
|
|
1010
|
-
and isinstance(approved_preview, dict)
|
|
1011
|
-
):
|
|
1012
|
-
_cache_pending_private_swap_order(_user_id(), tool_name, approved_preview, data)
|
|
1013
|
-
elif isinstance(data, dict):
|
|
1014
|
-
_clear_pending_private_swap_order(_user_id(), tool_name)
|
|
1015
|
-
if tool_name == "continue_solana_private_swap" and payload.get("ok") is True:
|
|
1016
|
-
data = payload.get("data")
|
|
1017
|
-
if isinstance(data, dict) and data.get("execution_state") == "funding_submitted":
|
|
1018
|
-
_clear_pending_private_swap_order(_user_id(), PRIVATE_SWAP_APPROVAL_TOOL_NAME)
|
|
1019
|
-
|
|
1020
904
|
if payload.get("ok") is False:
|
|
1021
905
|
raise RuntimeError(str(payload.get("error") or f"{tool_name} failed"))
|
|
1022
906
|
return payload.get("data", {})
|
|
@@ -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:
|
package/package.json
CHANGED
|
@@ -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
|
}
|