@agentlayer.tech/wallet 0.1.30 → 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.
Files changed (39) hide show
  1. package/.openclaw/extensions/agent-wallet/README.md +1 -2
  2. package/.openclaw/extensions/agent-wallet/dist/index.js +6 -340
  3. package/.openclaw/extensions/agent-wallet/index.ts +6 -340
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +0 -43
  5. package/.openclaw/extensions/agent-wallet/package.json +1 -1
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +1 -3
  7. package/CHANGELOG.md +35 -0
  8. package/README.md +0 -5
  9. package/agent-wallet/.env.example +0 -12
  10. package/agent-wallet/README.md +0 -35
  11. package/agent-wallet/agent_wallet/btc_user_wallets.py +32 -1
  12. package/agent-wallet/agent_wallet/config.py +11 -7
  13. package/agent-wallet/agent_wallet/evm_user_wallets.py +2 -0
  14. package/agent-wallet/agent_wallet/openclaw_adapter.py +1 -655
  15. package/agent-wallet/agent_wallet/openclaw_cli.py +0 -7
  16. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +18 -42
  17. package/agent-wallet/agent_wallet/providers/jupiter.py +1 -307
  18. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +31 -3
  19. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +37 -3
  20. package/agent-wallet/agent_wallet/transaction_policy.py +0 -262
  21. package/agent-wallet/agent_wallet/wallet_layer/base.py +0 -100
  22. package/agent-wallet/agent_wallet/wallet_layer/solana.py +1 -1118
  23. package/agent-wallet/openclaw.plugin.json +0 -4
  24. package/agent-wallet/pyproject.toml +1 -1
  25. package/agent-wallet/scripts/install_agent_wallet.py +113 -6
  26. package/agent-wallet/scripts/install_openclaw_local_config.py +7 -5
  27. package/agent-wallet/skills/wallet-operator/SKILL.md +1 -5
  28. package/bin/openclaw-agent-wallet.mjs +103 -36
  29. package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +7 -2
  30. package/codex/plugins/agent-wallet/server.py +2 -118
  31. package/hermes/plugins/agent_wallet/tools.py +1 -1
  32. package/package.json +1 -1
  33. package/wdk-btc-wallet/src/local_vault.js +45 -68
  34. package/wdk-btc-wallet/src/server.js +1 -0
  35. package/wdk-evm-wallet/README.md +4 -3
  36. package/wdk-evm-wallet/src/config.js +15 -0
  37. package/wdk-evm-wallet/src/local_vault.js +45 -68
  38. package/wdk-evm-wallet/src/server.js +1 -0
  39. package/agent-wallet/agent_wallet/providers/houdini.py +0 -539
@@ -101,14 +101,7 @@ def _apply_config_overrides(config: dict[str, Any]) -> None:
101
101
  config.get("jupiterPortfolioBaseUrl"),
102
102
  True,
103
103
  ),
104
- "jupiterLendBaseUrl": ("JUPITER_LEND_API_BASE_URL", config.get("jupiterLendBaseUrl"), True),
105
104
  "jupiterApiKey": ("JUPITER_API_KEY", config.get("jupiterApiKey"), True),
106
- "houdiniBaseUrl": ("HOUDINI_API_BASE_URL", config.get("houdiniBaseUrl"), True),
107
- "houdiniApiKey": ("HOUDINI_API_KEY", config.get("houdiniApiKey"), True),
108
- "houdiniApiSecret": ("HOUDINI_API_SECRET", config.get("houdiniApiSecret"), True),
109
- "houdiniUserIp": ("HOUDINI_USER_IP", config.get("houdiniUserIp"), True),
110
- "houdiniUserAgent": ("HOUDINI_USER_AGENT", config.get("houdiniUserAgent"), True),
111
- "houdiniUserTimezone": ("HOUDINI_USER_TIMEZONE", config.get("houdiniUserTimezone"), True),
112
105
  "kaminoBaseUrl": ("KAMINO_API_BASE_URL", config.get("kaminoBaseUrl"), True),
113
106
  "kaminoProgramId": ("KAMINO_PROGRAM_ID", config.get("kaminoProgramId"), True),
114
107
  }
@@ -14,11 +14,6 @@ COINGECKO_API_URL = "https://api.coingecko.com/api/v3"
14
14
  PORTFOLIO_TOKEN_CACHE_TTL_SECONDS = 30.0
15
15
  PORTFOLIO_PRICE_CACHE_TTL_SECONDS = 60.0
16
16
 
17
- ALCHEMY_RPC_URLS = {
18
- "ethereum": "https://eth-mainnet.g.alchemy.com/v2",
19
- "base": "https://base-mainnet.g.alchemy.com/v2",
20
- }
21
-
22
17
  TOKEN_METADATA: dict[str, dict[str, dict[str, Any]]] = {
23
18
  "ethereum": {
24
19
  "0xdac17f958d2ee523a2206206994597c13d831ec7": {
@@ -204,51 +199,32 @@ def _to_decimal(value: Any) -> Decimal | None:
204
199
  return None
205
200
 
206
201
 
207
- async def _gateway_or_alchemy_rpc_call(network: str, method: str, params: list[Any]) -> dict[str, Any]:
202
+ async def _gateway_rpc_call(network: str, method: str, params: list[Any]) -> dict[str, Any]:
208
203
  client = get_client()
209
204
  payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
210
205
  gateway_url = str(settings.provider_gateway_url or "").strip()
211
206
  bearer = str(settings.provider_gateway_bearer_token or "").strip()
212
- gateway_error: Exception | None = None
213
-
214
- if gateway_url:
215
- try:
216
- response = await client.post(
217
- f"{gateway_url.rstrip('/')}/v1/evm/rpc/{network}?provider=alchemy",
218
- json=payload,
219
- headers={"Authorization": f"Bearer {bearer}"} if bearer else None,
220
- )
221
- if response.status_code == 200:
222
- data = response.json()
223
- if isinstance(data, dict) and "error" not in data:
224
- return data
225
- gateway_error = ProviderError("evm-portfolio", f"Gateway RPC error: {data}")
226
- else:
227
- gateway_error = ProviderError(
228
- "evm-portfolio",
229
- f"Gateway returned HTTP {response.status_code} for {method}",
230
- )
231
- except Exception as exc: # pragma: no cover - network path
232
- gateway_error = exc
233
-
234
- alchemy_key = str(settings.alchemy_api_key or "").strip()
235
- if not alchemy_key:
236
- if gateway_error is not None:
237
- raise ProviderError("evm-portfolio", f"No fallback Alchemy key available after gateway failure: {gateway_error}")
238
- raise ProviderError("evm-portfolio", "No gateway URL or Alchemy API key configured for EVM portfolio lookup.")
239
-
240
- base_url = ALCHEMY_RPC_URLS.get(network)
241
- if not base_url:
242
- raise ProviderError("evm-portfolio", f"No Alchemy RPC URL configured for {network}.")
207
+ if not gateway_url:
208
+ raise ProviderError(
209
+ "evm-portfolio",
210
+ "Provider gateway URL is required for EVM portfolio lookup on ethereum/base.",
211
+ )
243
212
  try:
244
- response = await client.post(f"{base_url}/{alchemy_key}", json=payload)
213
+ response = await client.post(
214
+ f"{gateway_url.rstrip('/')}/v1/evm/rpc/{network}?provider=alchemy",
215
+ json=payload,
216
+ headers={"Authorization": f"Bearer {bearer}"} if bearer else None,
217
+ )
245
218
  except Exception as exc: # pragma: no cover - network path
246
- raise ProviderError("evm-portfolio", f"Alchemy request failed: {exc}") from exc
219
+ raise ProviderError("evm-portfolio", f"Provider gateway request failed: {exc}") from exc
247
220
  if response.status_code != 200:
248
- raise ProviderError("evm-portfolio", f"Alchemy returned HTTP {response.status_code} for {method}.")
221
+ raise ProviderError(
222
+ "evm-portfolio",
223
+ f"Provider gateway returned HTTP {response.status_code} for {method}.",
224
+ )
249
225
  data = response.json()
250
226
  if "error" in data:
251
- raise ProviderError("evm-portfolio", f"Alchemy RPC error: {data['error']}")
227
+ raise ProviderError("evm-portfolio", f"Provider gateway RPC error: {data['error']}")
252
228
  return data
253
229
 
254
230
 
@@ -259,7 +235,7 @@ async def fetch_token_balances(address: str, network: str) -> list[dict[str, Any
259
235
  if cached is not None:
260
236
  return cached
261
237
 
262
- data = await _gateway_or_alchemy_rpc_call(
238
+ data = await _gateway_rpc_call(
263
239
  normalized_network,
264
240
  "alchemy_getTokenBalances",
265
241
  [address, "erc20"],
@@ -1,4 +1,4 @@
1
- """Jupiter providers for swap routing, prices, portfolio, and lend/earn flows."""
1
+ """Jupiter providers for swap routing, prices, and portfolio flows."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -20,14 +20,6 @@ def _headers() -> dict[str, str]:
20
20
  return headers
21
21
 
22
22
 
23
- def _require_api_key(provider_name: str) -> None:
24
- if not settings.jupiter_api_key.strip():
25
- raise ProviderError(
26
- provider_name,
27
- "Jupiter API key is required for this endpoint. Set JUPITER_API_KEY first.",
28
- )
29
-
30
-
31
23
  def _gateway_base_url() -> str:
32
24
  return os.getenv("PROVIDER_GATEWAY_URL", settings.provider_gateway_url).strip().rstrip("/")
33
25
 
@@ -47,16 +39,6 @@ def _gateway_enabled() -> bool:
47
39
  return bool(_gateway_base_url())
48
40
 
49
41
 
50
- def _gateway_route_missing(status_code: int, payload: Any) -> bool:
51
- if status_code == 404:
52
- return True
53
- if isinstance(payload, dict):
54
- message = str(payload.get("error") or "").lower()
55
- if "not found" in message:
56
- return True
57
- return False
58
-
59
-
60
42
  async def _gateway_get(path_suffix: str, *, params: dict[str, Any] | None = None) -> tuple[int, Any]:
61
43
  """Make a GET request through provider gateway."""
62
44
  client = get_client()
@@ -126,125 +108,6 @@ def _swap_v2_base_url() -> str:
126
108
  ).strip().rstrip("/")
127
109
 
128
110
 
129
- def _unwrap_gateway_payload(
130
- status_code: int,
131
- payload: Any,
132
- *,
133
- operation: str,
134
- ) -> Any:
135
- if isinstance(payload, dict) and payload.get("ok") is False:
136
- message = str(payload.get("error") or f"{operation} failed.")
137
- raise ProviderError("jupiter-lend", f"{operation} failed via provider gateway: {message}")
138
-
139
- if status_code != 200:
140
- message = payload
141
- if isinstance(payload, dict):
142
- message = payload.get("error") or payload
143
- raise ProviderError("jupiter-lend", f"{operation} failed via provider gateway: {message}")
144
-
145
- return payload
146
-
147
-
148
- async def _gateway_get_json(
149
- path: str,
150
- *,
151
- params: dict[str, Any] | None,
152
- operation: str,
153
- ) -> Any:
154
- client = get_client()
155
- response = await client.get(
156
- f"{_gateway_base_url()}{path}",
157
- params=params,
158
- headers=_gateway_headers(),
159
- )
160
- payload = response.json() if response.content else {}
161
- return _unwrap_gateway_payload(
162
- response.status_code,
163
- payload,
164
- operation=operation,
165
- )
166
-
167
-
168
- async def _gateway_post_json(
169
- path: str,
170
- *,
171
- body: dict[str, Any],
172
- operation: str,
173
- ) -> Any:
174
- client = get_client()
175
- response = await client.post(
176
- f"{_gateway_base_url()}{path}",
177
- json=body,
178
- headers={**_gateway_headers(), "Content-Type": "application/json"},
179
- )
180
- payload = response.json() if response.content else {}
181
- return _unwrap_gateway_payload(
182
- response.status_code,
183
- payload,
184
- operation=operation,
185
- )
186
-
187
-
188
- async def _earn_get_with_gateway_fallback(
189
- *,
190
- path: str,
191
- params: dict[str, Any] | None,
192
- operation: str,
193
- ) -> Any:
194
- if _gateway_enabled():
195
- client = get_client()
196
- response = await client.get(
197
- f"{_gateway_base_url()}{path}",
198
- params=params,
199
- headers=_gateway_headers(),
200
- )
201
- payload = response.json() if response.content else {}
202
- if _gateway_route_missing(response.status_code, payload) and _direct_jupiter_enabled():
203
- return None
204
- return _unwrap_gateway_payload(response.status_code, payload, operation=operation)
205
- return None
206
-
207
-
208
- async def _earn_post_with_gateway_fallback(
209
- *,
210
- path: str,
211
- body: dict[str, Any],
212
- operation: str,
213
- ) -> Any:
214
- if _gateway_enabled():
215
- client = get_client()
216
- response = await client.post(
217
- f"{_gateway_base_url()}{path}",
218
- json=body,
219
- headers={**_gateway_headers(), "Content-Type": "application/json"},
220
- )
221
- payload = response.json() if response.content else {}
222
- if _gateway_route_missing(response.status_code, payload) and _direct_jupiter_enabled():
223
- return None
224
- return _unwrap_gateway_payload(response.status_code, payload, operation=operation)
225
- return None
226
-
227
-
228
- def _normalize_named_list_response(
229
- data: Any,
230
- *,
231
- key: str,
232
- provider_name: str,
233
- ) -> dict[str, Any]:
234
- if isinstance(data, list):
235
- return {key: data}
236
- if isinstance(data, dict):
237
- items = data.get(key)
238
- if isinstance(items, list):
239
- return data
240
- fallback = data.get("data")
241
- if isinstance(fallback, list):
242
- normalized = dict(data)
243
- normalized[key] = fallback
244
- return normalized
245
- raise ProviderError(provider_name, f"Unexpected {key} response from Jupiter.")
246
-
247
-
248
111
  async def fetch_quote(
249
112
  *,
250
113
  input_mint: str,
@@ -678,172 +541,3 @@ async def fetch_staked_jup(*, address: str) -> dict[str, Any]:
678
541
  if not isinstance(data, dict):
679
542
  raise ProviderError("jupiter-portfolio", "Unexpected staked JUP response.")
680
543
  return data
681
-
682
-
683
- async def fetch_earn_tokens() -> dict[str, Any]:
684
- """Fetch supported Jupiter Earn vault tokens."""
685
- gateway_response = await _earn_get_with_gateway_fallback(
686
- path="/v1/jupiter/earn/tokens",
687
- params=None,
688
- operation="Jupiter Earn tokens",
689
- )
690
- if gateway_response is not None:
691
- return _normalize_named_list_response(
692
- gateway_response,
693
- key="tokens",
694
- provider_name="jupiter-lend",
695
- )
696
-
697
- _require_api_key("jupiter-lend")
698
- client = get_client()
699
- response = await client.get(
700
- f"{settings.jupiter_lend_api_base_url.rstrip('/')}/earn/tokens",
701
- headers=_headers(),
702
- )
703
- if response.status_code != 200:
704
- raise ProviderError("jupiter-lend", f"HTTP {response.status_code}: {response.text[:300]}")
705
- return _normalize_named_list_response(
706
- response.json(),
707
- key="tokens",
708
- provider_name="jupiter-lend",
709
- )
710
-
711
-
712
- async def fetch_earn_positions(*, users: list[str]) -> dict[str, Any]:
713
- """Fetch Jupiter Earn positions for one or more users."""
714
- gateway_response = await _earn_get_with_gateway_fallback(
715
- path="/v1/jupiter/earn/positions",
716
- params={"users": ",".join(users)},
717
- operation="Jupiter Earn positions",
718
- )
719
- if gateway_response is not None:
720
- return _normalize_named_list_response(
721
- gateway_response,
722
- key="positions",
723
- provider_name="jupiter-lend",
724
- )
725
-
726
- _require_api_key("jupiter-lend")
727
- client = get_client()
728
- response = await client.get(
729
- f"{settings.jupiter_lend_api_base_url.rstrip('/')}/earn/positions",
730
- params={"users": ",".join(users)},
731
- headers=_headers(),
732
- )
733
- if response.status_code != 200:
734
- raise ProviderError("jupiter-lend", f"HTTP {response.status_code}: {response.text[:300]}")
735
- return _normalize_named_list_response(
736
- response.json(),
737
- key="positions",
738
- provider_name="jupiter-lend",
739
- )
740
-
741
-
742
- async def fetch_earn_earnings(*, user: str, positions: list[str]) -> dict[str, Any]:
743
- """Fetch Jupiter Earn earnings for a user and position list."""
744
- gateway_response = await _earn_get_with_gateway_fallback(
745
- path="/v1/jupiter/earn/earnings",
746
- params={"user": user, "positions": ",".join(positions)},
747
- operation="Jupiter Earn earnings",
748
- )
749
- if gateway_response is not None:
750
- return _normalize_named_list_response(
751
- gateway_response,
752
- key="earnings",
753
- provider_name="jupiter-lend",
754
- )
755
-
756
- _require_api_key("jupiter-lend")
757
- client = get_client()
758
- response = await client.get(
759
- f"{settings.jupiter_lend_api_base_url.rstrip('/')}/earn/earnings",
760
- params={"user": user, "positions": ",".join(positions)},
761
- headers=_headers(),
762
- )
763
- if response.status_code != 200:
764
- raise ProviderError("jupiter-lend", f"HTTP {response.status_code}: {response.text[:300]}")
765
- return _normalize_named_list_response(
766
- response.json(),
767
- key="earnings",
768
- provider_name="jupiter-lend",
769
- )
770
-
771
-
772
- async def build_earn_deposit_transaction(
773
- *,
774
- asset: str,
775
- user_address: str,
776
- amount_raw: str,
777
- ) -> dict[str, Any]:
778
- """Build an unsigned Jupiter Earn deposit transaction."""
779
- gateway_response = await _earn_post_with_gateway_fallback(
780
- path="/v1/jupiter/earn/deposit",
781
- body={
782
- "asset": asset,
783
- "signer": user_address,
784
- "amount": amount_raw,
785
- },
786
- operation="Jupiter Earn deposit",
787
- )
788
- if gateway_response is not None:
789
- if not isinstance(gateway_response, dict) or "transaction" not in gateway_response:
790
- raise ProviderError("jupiter-lend", "Unexpected Earn deposit response.")
791
- return gateway_response
792
-
793
- _require_api_key("jupiter-lend")
794
- client = get_client()
795
- response = await client.post(
796
- f"{settings.jupiter_lend_api_base_url.rstrip('/')}/earn/deposit",
797
- json={
798
- "asset": asset,
799
- "signer": user_address,
800
- "amount": amount_raw,
801
- },
802
- headers=_headers(),
803
- )
804
- if response.status_code != 200:
805
- raise ProviderError("jupiter-lend", f"HTTP {response.status_code}: {response.text[:300]}")
806
- data = response.json()
807
- if not isinstance(data, dict) or "transaction" not in data:
808
- raise ProviderError("jupiter-lend", "Unexpected Earn deposit response.")
809
- return data
810
-
811
-
812
- async def build_earn_withdraw_transaction(
813
- *,
814
- asset: str,
815
- user_address: str,
816
- amount_raw: str,
817
- ) -> dict[str, Any]:
818
- """Build an unsigned Jupiter Earn withdraw transaction."""
819
- gateway_response = await _earn_post_with_gateway_fallback(
820
- path="/v1/jupiter/earn/withdraw",
821
- body={
822
- "asset": asset,
823
- "signer": user_address,
824
- "amount": amount_raw,
825
- },
826
- operation="Jupiter Earn withdraw",
827
- )
828
- if gateway_response is not None:
829
- if not isinstance(gateway_response, dict) or "transaction" not in gateway_response:
830
- raise ProviderError("jupiter-lend", "Unexpected Earn withdraw response.")
831
- return gateway_response
832
-
833
- _require_api_key("jupiter-lend")
834
- client = get_client()
835
- response = await client.post(
836
- f"{settings.jupiter_lend_api_base_url.rstrip('/')}/earn/withdraw",
837
- json={
838
- "asset": asset,
839
- "signer": user_address,
840
- "amount": amount_raw,
841
- },
842
- headers=_headers(),
843
- )
844
- if response.status_code != 200:
845
- raise ProviderError("jupiter-lend", f"HTTP {response.status_code}: {response.text[:300]}")
846
- data = response.json()
847
- if not isinstance(data, dict) or "transaction" not in data:
848
- raise ProviderError("jupiter-lend", "Unexpected Earn withdraw response.")
849
- return data
@@ -9,7 +9,7 @@ from urllib.parse import urlparse
9
9
 
10
10
  import httpx
11
11
 
12
- from agent_wallet.config import resolve_openclaw_home
12
+ from agent_wallet.config import resolve_btc_wallet_password, resolve_openclaw_home
13
13
  from agent_wallet.wallet_layer.base import WalletBackendError
14
14
 
15
15
  LOCAL_WDK_BTC_HOSTS = {"127.0.0.1", "localhost", "::1"}
@@ -72,6 +72,30 @@ class WdkBtcLocalClient:
72
72
  "Accept": "application/json",
73
73
  "Authorization": f"Bearer {_load_local_token()}",
74
74
  }
75
+ self._wallet_password: str | None = None
76
+
77
+ def _resolve_wallet_password(self) -> str:
78
+ """Resolve the sealed local BTC vault password once per client instance.
79
+
80
+ The decrypt-on-demand vault needs the password on every signing request;
81
+ resolving it once per client keeps the Argon2id unseal off the per-request
82
+ path. The agent already holds the boot key, so caching adds no extra exposure.
83
+ """
84
+ if self._wallet_password is None:
85
+ try:
86
+ self._wallet_password = resolve_btc_wallet_password() or ""
87
+ except Exception:
88
+ self._wallet_password = ""
89
+ return self._wallet_password
90
+
91
+ def _with_credentials(self, payload: dict[str, Any]) -> dict[str, Any]:
92
+ """Attach the sealed vault password unless the caller supplied one."""
93
+ if not isinstance(payload, dict) or payload.get("password"):
94
+ return payload
95
+ password = self._resolve_wallet_password()
96
+ if not password:
97
+ return payload
98
+ return {**payload, "password": password}
75
99
 
76
100
  async def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
77
101
  async with httpx.AsyncClient(
@@ -80,7 +104,9 @@ class WdkBtcLocalClient:
80
104
  follow_redirects=False,
81
105
  trust_env=False,
82
106
  ) as client:
83
- response = await client.post(f"{self.base_url}{path}", json=payload)
107
+ response = await client.post(
108
+ f"{self.base_url}{path}", json=self._with_credentials(payload)
109
+ )
84
110
  return _unwrap_payload(response)
85
111
 
86
112
  async def get(self, path: str) -> dict[str, Any]:
@@ -100,7 +126,9 @@ class WdkBtcLocalClient:
100
126
  follow_redirects=False,
101
127
  trust_env=False,
102
128
  ) as client:
103
- response = client.post(f"{self.base_url}{path}", json=payload)
129
+ response = client.post(
130
+ f"{self.base_url}{path}", json=self._with_credentials(payload)
131
+ )
104
132
  return _unwrap_payload(response)
105
133
 
106
134
  def get_sync(self, path: str) -> dict[str, Any]:
@@ -9,7 +9,7 @@ from urllib.parse import urlparse
9
9
 
10
10
  import httpx
11
11
 
12
- from agent_wallet.config import resolve_openclaw_home, settings
12
+ from agent_wallet.config import resolve_evm_wallet_password, resolve_openclaw_home, settings
13
13
  from agent_wallet.wallet_layer.base import WalletBackendError
14
14
 
15
15
  LOCAL_WDK_EVM_HOSTS = {"127.0.0.1", "localhost", "::1"}
@@ -140,6 +140,36 @@ class WdkEvmLocalClient:
140
140
  "Accept": "application/json",
141
141
  "Authorization": f"Bearer {_load_local_token()}",
142
142
  }
143
+ self._wallet_password: str | None = None
144
+
145
+ def _resolve_wallet_password(self) -> str:
146
+ """Resolve the sealed local EVM vault password once per client instance.
147
+
148
+ The decrypt-on-demand vault needs the password on every signing request.
149
+ Resolving (and unsealing) it once per client keeps the Argon2id unseal off
150
+ the per-request path. The agent already holds the boot key, so caching the
151
+ derived password here adds no exposure beyond what the boot key grants.
152
+ """
153
+ if self._wallet_password is None:
154
+ try:
155
+ self._wallet_password = resolve_evm_wallet_password() or ""
156
+ except Exception:
157
+ self._wallet_password = ""
158
+ return self._wallet_password
159
+
160
+ def _with_credentials(self, payload: dict[str, Any]) -> dict[str, Any]:
161
+ """Attach the sealed vault password unless the caller supplied one.
162
+
163
+ Read endpoints that resolve via a stored address ignore the field; only
164
+ seed-requiring (signing) endpoints consume it. Injecting it uniformly means
165
+ no signing path can accidentally be missed.
166
+ """
167
+ if not isinstance(payload, dict) or payload.get("password"):
168
+ return payload
169
+ password = self._resolve_wallet_password()
170
+ if not password:
171
+ return payload
172
+ return {**payload, "password": password}
143
173
 
144
174
  async def post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
145
175
  try:
@@ -149,7 +179,9 @@ class WdkEvmLocalClient:
149
179
  follow_redirects=False,
150
180
  trust_env=False,
151
181
  ) as client:
152
- response = await client.post(f"{self.base_url}{path}", json=payload)
182
+ response = await client.post(
183
+ f"{self.base_url}{path}", json=self._with_credentials(payload)
184
+ )
153
185
  except httpx.TimeoutException as exc:
154
186
  raise WalletBackendError(
155
187
  "wdk-evm-wallet request timed out.",
@@ -195,7 +227,9 @@ class WdkEvmLocalClient:
195
227
  follow_redirects=False,
196
228
  trust_env=False,
197
229
  ) as client:
198
- response = client.post(f"{self.base_url}{path}", json=payload)
230
+ response = client.post(
231
+ f"{self.base_url}{path}", json=self._with_credentials(payload)
232
+ )
199
233
  except httpx.TimeoutException as exc:
200
234
  raise WalletBackendError(
201
235
  "wdk-evm-wallet request timed out.",