@agentlayer.tech/wallet 0.1.67 → 0.1.69

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.
@@ -984,6 +984,54 @@ const solanaToolDefinitions = [
984
984
  additionalProperties: false,
985
985
  },
986
986
  },
987
+ {
988
+ name: "get_kamino_portfolio",
989
+ description:
990
+ "Get the unified Kamino portfolio view for a Solana wallet on mainnet across lending, multiply, leverage, liquidity, earn, and staking.",
991
+ parameters: {
992
+ type: "object",
993
+ properties: {
994
+ user: {
995
+ type: "string",
996
+ description: "Optional Solana wallet address override.",
997
+ },
998
+ },
999
+ additionalProperties: false,
1000
+ },
1001
+ },
1002
+ {
1003
+ name: "get_kamino_vaults",
1004
+ description: "List Kamino Earn vaults currently available on Solana mainnet.",
1005
+ parameters: { type: "object", properties: {}, additionalProperties: false },
1006
+ },
1007
+ {
1008
+ name: "get_kamino_earn_positions",
1009
+ description: "Get Kamino Earn vault positions for a Solana wallet on mainnet.",
1010
+ parameters: {
1011
+ type: "object",
1012
+ properties: {
1013
+ user: {
1014
+ type: "string",
1015
+ description: "Optional Solana wallet address override.",
1016
+ },
1017
+ },
1018
+ additionalProperties: false,
1019
+ },
1020
+ },
1021
+ {
1022
+ name: "get_kamino_liquidity_positions",
1023
+ description: "Get Kamino Liquidity strategy positions for a Solana wallet on mainnet.",
1024
+ parameters: {
1025
+ type: "object",
1026
+ properties: {
1027
+ user: {
1028
+ type: "string",
1029
+ description: "Optional Solana wallet address override.",
1030
+ },
1031
+ },
1032
+ additionalProperties: false,
1033
+ },
1034
+ },
987
1035
  {
988
1036
  name: "get_kamino_lend_markets",
989
1037
  description: "List Kamino lending markets currently available on Solana mainnet.",
@@ -1277,6 +1325,40 @@ const solanaToolDefinitions = [
1277
1325
  additionalProperties: false,
1278
1326
  },
1279
1327
  },
1328
+ {
1329
+ name: "kamino_earn_deposit",
1330
+ description: "Preview, prepare, or execute a Kamino Earn vault deposit using a decimal token amount. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
1331
+ optional: true,
1332
+ parameters: {
1333
+ type: "object",
1334
+ properties: {
1335
+ kvault: { type: "string" },
1336
+ amount_ui: { type: "string" },
1337
+ mode: { type: "string", enum: ["preview", "prepare", "execute"] },
1338
+ purpose: { type: "string" },
1339
+ user_intent: { type: "boolean" },
1340
+ },
1341
+ required: ["kvault", "amount_ui", "mode", "purpose"],
1342
+ additionalProperties: false,
1343
+ },
1344
+ },
1345
+ {
1346
+ name: "kamino_earn_withdraw",
1347
+ description: "Preview, prepare, or execute a Kamino Earn vault withdraw using a decimal token amount. Preview or prepare first. After the user explicitly confirms the shown summary in chat, call execute; the OpenClaw plugin handles the internal execution authorization automatically.",
1348
+ optional: true,
1349
+ parameters: {
1350
+ type: "object",
1351
+ properties: {
1352
+ kvault: { type: "string" },
1353
+ amount_ui: { type: "string" },
1354
+ mode: { type: "string", enum: ["preview", "prepare", "execute"] },
1355
+ purpose: { type: "string" },
1356
+ user_intent: { type: "boolean" },
1357
+ },
1358
+ required: ["kvault", "amount_ui", "mode", "purpose"],
1359
+ additionalProperties: false,
1360
+ },
1361
+ },
1280
1362
  {
1281
1363
  name: "flash_trade_open_position",
1282
1364
  description: "Preview, prepare, or execute a Flash Trade perpetual open on Solana mainnet using a supported Flash collateral.",
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Official OpenClaw plugin bridge for the agent-wallet backends, including Solana, local BTC, and local EVM.",
5
- "version": "0.1.67",
5
+ "version": "0.1.69",
6
6
  "contracts": {
7
7
  "tools": [
8
8
  "agentlayer_autonomous_approve",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayertech/agent-wallet-plugin",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "OpenClaw plugin bridge for the AgentLayer wallet runtime.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN ../../../LICENSE",
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Fixed local EVM autostart incorrectly trusting any healthy same-version
6
+ `wdk-evm-wallet` already listening on the shared localhost port. If a temp or
7
+ alternate `OPENCLAW_HOME` had left a daemon running, host runtimes could hit
8
+ `Unauthorized` because the daemon served a different `dataDir` and bearer
9
+ token than the current install expected. Local EVM startup now also validates
10
+ the reported `dataDir` from `/health` and restarts mismatched daemons before
11
+ issuing authenticated requests.
12
+ - `agent-wallet/agent_wallet/evm_user_wallets.py`
13
+ - `agent-wallet/scripts/bootstrap_openclaw_evm.py`
14
+ - `agent-wallet/tests/smoke_openclaw_evm_runtime_restart_wrong_home.py`
15
+
5
16
  ## v0.1.62 - 2026-07-04
6
17
 
7
18
  - Made default boot-key storage prompt-free on macOS. The automatic keystore
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.67
1
+ 0.1.69
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Keep in sync with package.json, pyproject.toml, and the npm installer version.
4
4
  # scripts/check_release_version.mjs enforces this on release.
5
- __version__ = "0.1.67"
5
+ __version__ = "0.1.69"
6
6
 
7
7
  __all__ = [
8
8
  "config",
@@ -86,6 +86,43 @@ def _read_on_disk_service_version(wallet_root: Path) -> str | None:
86
86
  return version or None
87
87
 
88
88
 
89
+ def _expected_local_service_data_dir() -> Path:
90
+ configured = os.getenv("WDK_EVM_DATA_DIR", "").strip()
91
+ if configured:
92
+ return Path(configured).expanduser().resolve()
93
+ return (resolve_openclaw_home() / "wdk-evm-wallet").resolve()
94
+
95
+
96
+ def _same_path(left: str | Path | None, right: str | Path | None) -> bool:
97
+ if left is None or right is None:
98
+ return False
99
+ try:
100
+ left_path = Path(str(left)).expanduser().resolve()
101
+ right_path = Path(str(right)).expanduser().resolve()
102
+ except OSError:
103
+ return False
104
+ return left_path == right_path
105
+
106
+
107
+ def _should_restart_local_service(
108
+ health: dict[str, Any] | None,
109
+ *,
110
+ wallet_root: Path | None,
111
+ ) -> bool:
112
+ if health is None:
113
+ return False
114
+ expected_version = _read_on_disk_service_version(wallet_root) if wallet_root is not None else None
115
+ running_version = str(health.get("version") or "").strip()
116
+ if expected_version and running_version and running_version != expected_version:
117
+ return True
118
+
119
+ reported_data_dir = str(health.get("dataDir") or "").strip()
120
+ if reported_data_dir and not _same_path(reported_data_dir, _expected_local_service_data_dir()):
121
+ return True
122
+
123
+ return False
124
+
125
+
89
126
  def _listening_pids(port: int) -> list[int]:
90
127
  """PIDs LISTENing on a local TCP port (via lsof), excluding our own."""
91
128
  lsof = shutil.which("lsof")
@@ -177,18 +214,14 @@ def _auto_start_local_service(service_url: str, network: str) -> None:
177
214
  health = _service_health(service_url)
178
215
  if health is not None:
179
216
  # Already running. The daemon loads code once at boot (no hot-reload), so a
180
- # long-running process keeps serving stale code after a release. Restart it
181
- # only when its reported version differs from the on-disk launcher version
182
- # comparing against the version the restarted daemon will itself report keeps
183
- # this idempotent (steady state = no-op) and free of restart loops. Remote
217
+ # long-running process keeps serving stale code after a release. It can also
218
+ # keep serving the wrong local vault after a temp/smoke install left another
219
+ # daemon on the shared localhost port. Restart only when the local daemon no
220
+ # longer matches the expected launcher version or expected dataDir. Remote
184
221
  # (non-local) healthy services we don't manage are left untouched.
185
222
  if not _is_local_service_url(service_url):
186
223
  return
187
- expected_version = (
188
- _read_on_disk_service_version(wallet_root) if wallet_root is not None else None
189
- )
190
- running_version = str(health.get("version") or "").strip()
191
- if expected_version is None or running_version == expected_version:
224
+ if not _should_restart_local_service(health, wallet_root=wallet_root):
192
225
  return
193
226
  _stop_local_service(service_url)
194
227
  if not _is_local_service_url(service_url):
@@ -7,6 +7,7 @@ import hashlib
7
7
  import json
8
8
  import logging
9
9
  import time
10
+ from types import SimpleNamespace
10
11
  from typing import Any
11
12
  from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
12
13
 
@@ -428,11 +429,16 @@ def _evm_exact_execution_supported(backend: AgentWalletBackend) -> bool:
428
429
 
429
430
 
430
431
  def _evm_payment_requirement_supported(requirement: dict[str, Any]) -> bool:
431
- if _trim(requirement.get("scheme")).lower() != "exact":
432
- return False
432
+ scheme = _trim(requirement.get("scheme")).lower()
433
433
  extra = _extract_requirement_extra(requirement)
434
- transfer_method = _trim(extra.get("assetTransferMethod")).lower()
435
- return transfer_method in {"", "eip3009", "transferwithauthorization"}
434
+ if scheme == "exact":
435
+ transfer_method = _trim(extra.get("assetTransferMethod")).lower()
436
+ return transfer_method in {"", "eip3009", "transferwithauthorization"}
437
+ if scheme == "upto":
438
+ # The upto Permit2 payload needs the facilitator's address up front;
439
+ # without it the SDK signer raises instead of falling back cleanly.
440
+ return bool(_trim(extra.get("facilitatorAddress")))
441
+ return False
436
442
 
437
443
 
438
444
  def _wallet_x402_support_summary(backend: AgentWalletBackend) -> dict[str, Any]:
@@ -478,7 +484,7 @@ def _requirement_compatibility(requirement: dict[str, Any], backend: AgentWallet
478
484
  )
479
485
  elif chain == "evm":
480
486
  planned_execution_supported = (
481
- scheme == "exact"
487
+ scheme in {"exact", "upto"}
482
488
  and network in set(wallet_summary["planned_execution_networks"])
483
489
  and _evm_payment_requirement_supported(requirement)
484
490
  )
@@ -491,10 +497,12 @@ def _requirement_compatibility(requirement: dict[str, Any], backend: AgentWallet
491
497
  reason = (
492
498
  "Executable now through the local Solana exact buyer flow."
493
499
  if chain == "solana"
494
- else "Executable now through the local EVM exact buyer flow."
500
+ else f"Executable now through the local EVM {scheme or 'exact'} buyer flow."
495
501
  )
496
502
  elif chain == "evm" and scheme == "exact" and not _evm_payment_requirement_supported(requirement):
497
503
  reason = "This EVM exact payment requires a transfer method that is not enabled in the current wallet runtime."
504
+ elif chain == "evm" and scheme == "upto" and not _evm_payment_requirement_supported(requirement):
505
+ reason = "This EVM upto payment is missing a facilitatorAddress in its extra data, so it cannot be signed."
498
506
  elif planned_execution_supported and wallet_network_matches:
499
507
  reason = "Wallet network matches, but this backend does not yet expose a supported x402 signer path."
500
508
  elif planned_execution_supported:
@@ -527,11 +535,16 @@ def _select_preferred_requirement(
527
535
  if not candidates:
528
536
  return None
529
537
 
530
- def sort_key(item: dict[str, Any]) -> tuple[int, int]:
538
+ def sort_key(item: dict[str, Any]) -> tuple[int, int, int]:
539
+ # Prefer upto over exact: upto settles for actual usage against a
540
+ # signed cap, which fits metered/usage-priced endpoints better than
541
+ # exact's fixed charge. Ties within a scheme still favor the cheapest
542
+ # declared amount.
543
+ scheme_rank = 0 if _trim(item.get("scheme")).lower() == "upto" else 1
531
544
  amount = _trim(item.get("amount"))
532
545
  if amount.isdigit():
533
- return (0, int(amount))
534
- return (1, 0)
546
+ return (scheme_rank, 0, int(amount))
547
+ return (scheme_rank, 1, 0)
535
548
 
536
549
  return sorted(candidates, key=sort_key)[0]
537
550
 
@@ -728,10 +741,10 @@ def _validate_payment_requirement(
728
741
  )
729
742
 
730
743
  scheme = _trim(selected.get("scheme")).lower()
731
- if scheme != "exact":
744
+ if scheme not in {"exact", "upto"}:
732
745
  raise ProviderError(
733
746
  "x402-validate",
734
- f"Unsupported x402 payment scheme '{scheme or 'unknown'}'. Only 'exact' is supported.",
747
+ f"Unsupported x402 payment scheme '{scheme or 'unknown'}'. Only 'exact' and 'upto' are supported.",
735
748
  details={"request_url": request_url, "selected_payment": selected},
736
749
  )
737
750
 
@@ -846,6 +859,24 @@ def _select_sdk_payment_requirement(
846
859
  )
847
860
 
848
861
 
862
+ # The `upto` Permit2 signature authorizes the (merchant + facilitator) pair to
863
+ # settle any amount in [0, cap] until `max_timeout_seconds` elapses, with no
864
+ # on-chain check beyond the deadline itself. A merchant-declared timeout with
865
+ # no upper bound would extend that facilitator-discretion window indefinitely,
866
+ # so cap it defensively before signing. `exact` amounts are fixed in the
867
+ # signature, so this cap only applies to `upto`.
868
+ MAX_UPTO_DEADLINE_SECONDS = 7 * 24 * 60 * 60 # 7 days
869
+
870
+
871
+ def _cap_upto_deadline(requirement: Any) -> Any:
872
+ if _trim(getattr(requirement, "scheme", "")).lower() != "upto":
873
+ return requirement
874
+ timeout = getattr(requirement, "max_timeout_seconds", None)
875
+ if not isinstance(timeout, int) or timeout <= MAX_UPTO_DEADLINE_SECONDS:
876
+ return requirement
877
+ return requirement.model_copy(update={"max_timeout_seconds": MAX_UPTO_DEADLINE_SECONDS})
878
+
879
+
849
880
  def _build_selected_payment_required_payload(
850
881
  payment_required: Any,
851
882
  *,
@@ -855,6 +886,7 @@ def _build_selected_payment_required_payload(
855
886
  payment_required,
856
887
  selected_payment=selected_payment,
857
888
  )
889
+ selected_requirement = _cap_upto_deadline(selected_requirement)
858
890
  return payment_required.model_copy(update={"accepts": [selected_requirement]})
859
891
 
860
892
 
@@ -904,9 +936,18 @@ def _load_x402_evm_sdk() -> dict[str, Any]:
904
936
  "x402 EVM execution requires EVM support.",
905
937
  details={"hint": 'Install dependencies so `x402[httpx,evm]` is available in the wallet runtime.'},
906
938
  ) from exc
939
+ try:
940
+ from x402.mechanisms.evm.upto import UptoEvmScheme
941
+ except ImportError as exc:
942
+ raise ProviderError(
943
+ "x402-sdk",
944
+ "x402 EVM upto-scheme execution requires the upto mechanism module.",
945
+ details={"hint": 'Install dependencies so `x402[httpx,evm]` is available in the wallet runtime.'},
946
+ ) from exc
907
947
  sdk.update(
908
948
  {
909
949
  "register_exact_evm_client": register_exact_evm_client,
950
+ "UptoEvmScheme": UptoEvmScheme,
910
951
  }
911
952
  )
912
953
  return sdk
@@ -916,6 +957,67 @@ def _load_x402_sdk() -> dict[str, Any]:
916
957
  return _load_x402_common_sdk()
917
958
 
918
959
 
960
+ def _load_x402_siwx_sdk() -> dict[str, Any]:
961
+ try:
962
+ from x402.extensions.sign_in_with_x import (
963
+ SIGN_IN_WITH_X,
964
+ CreateSIWxClientExtensionOptions,
965
+ create_siwx_client_extension,
966
+ )
967
+ except ImportError as exc:
968
+ raise ProviderError(
969
+ "x402-sdk",
970
+ "x402 sign-in-with-x execution requires the SIWx extension module.",
971
+ details={"hint": 'Install dependencies so `x402[httpx,evm,svm]` is available in the wallet runtime.'},
972
+ ) from exc
973
+ return {
974
+ "SIGN_IN_WITH_X": SIGN_IN_WITH_X,
975
+ "CreateSIWxClientExtensionOptions": CreateSIWxClientExtensionOptions,
976
+ "create_siwx_client_extension": create_siwx_client_extension,
977
+ }
978
+
979
+
980
+ async def _maybe_attach_siwx_header(
981
+ *,
982
+ payment_required: Any,
983
+ signer: Any,
984
+ headers: dict[str, str],
985
+ ) -> dict[str, str]:
986
+ """Attach a Sign-In-With-X header when the merchant declares that extension.
987
+
988
+ Some x402 gateways require proving wallet ownership via SIWx alongside the
989
+ payment signature; without it those requests are rejected even with a
990
+ valid payment. Best-effort: any failure to build the SIWx credential falls
991
+ back to paying without it, matching the upstream SDK's own
992
+ on_payment_required hook contract (which swallows exceptions the same way).
993
+ """
994
+ extensions = getattr(payment_required, "extensions", None) or {}
995
+ if not extensions:
996
+ return headers
997
+ try:
998
+ sdk = _load_x402_siwx_sdk()
999
+ except ProviderError:
1000
+ return headers
1001
+ if sdk["SIGN_IN_WITH_X"] not in extensions:
1002
+ return headers
1003
+ extension = sdk["create_siwx_client_extension"](
1004
+ sdk["CreateSIWxClientExtensionOptions"](signers=[signer])
1005
+ )
1006
+ context = SimpleNamespace(payment_required=payment_required)
1007
+ try:
1008
+ result = await extension.transport_hooks.http.on_payment_required(None, context)
1009
+ except Exception as exc:
1010
+ log.warning(
1011
+ "x402 SIWx header build failed; continuing without it",
1012
+ extra={"error_type": type(exc).__name__, "error": str(exc) or None},
1013
+ )
1014
+ return headers
1015
+ siwx_headers = getattr(result, "headers", None) if result is not None else None
1016
+ if not siwx_headers:
1017
+ return headers
1018
+ return {**headers, **siwx_headers}
1019
+
1020
+
919
1021
  def _build_solana_sdk_signer(backend: AgentWalletBackend) -> Any:
920
1022
  signer = getattr(backend, "signer", None)
921
1023
  if signer is None or not hasattr(signer, "export_keypair_bytes"):
@@ -959,15 +1061,30 @@ def _build_evm_sdk_signer(backend: AgentWalletBackend, address: str) -> Any:
959
1061
  "The active EVM backend does not expose an x402 exact typed-data signer.",
960
1062
  )
961
1063
 
1064
+ class _NoEthAccountSentinel:
1065
+ """Truthy placeholder with neither `.sign_message` nor `.address`.
1066
+
1067
+ x402's SIWx sign_evm_message() treats a signer with `.account` unset
1068
+ but both `.sign_message` and `.address` present as an eth_account-style
1069
+ wallet, and calls `.sign_message(SignableMessage)` positionally instead
1070
+ of our keyword-based `sign_message(message=..., account=...)`. Exposing
1071
+ a benign `.account` here short-circuits that fallback so the SDK
1072
+ reaches our intended call path.
1073
+ """
1074
+
962
1075
  class _OpenClawEvmX402Signer:
963
1076
  def __init__(self, wallet_backend: AgentWalletBackend, wallet_address: str):
964
1077
  self._wallet_backend = wallet_backend
965
1078
  self._address = wallet_address
1079
+ self.account = _NoEthAccountSentinel()
966
1080
 
967
1081
  @property
968
1082
  def address(self) -> str:
969
1083
  return self._address
970
1084
 
1085
+ async def sign_message(self, *, message: str, account: Any = None) -> str:
1086
+ return await self._wallet_backend.sign_message(message)
1087
+
971
1088
  def sign_typed_data(
972
1089
  self,
973
1090
  domain: Any,
@@ -1022,9 +1139,10 @@ async def _create_payment_headers(
1022
1139
  "No direct Solana RPC URL is available for the x402 SDK signer path.",
1023
1140
  details={"network": _backend_network(backend)},
1024
1141
  )
1142
+ solana_signer = _build_solana_sdk_signer(backend)
1025
1143
  sdk["register_exact_svm_client"](
1026
1144
  client,
1027
- _build_solana_sdk_signer(backend),
1145
+ solana_signer,
1028
1146
  networks=str(selected_payment["network"]),
1029
1147
  rpc_url=sdk_rpc_url,
1030
1148
  )
@@ -1041,7 +1159,10 @@ async def _create_payment_headers(
1041
1159
  "error": str(exc) or None,
1042
1160
  },
1043
1161
  ) from exc
1044
- return sdk["x402HTTPClientBase"]().encode_payment_signature_header(payment_payload)
1162
+ headers = sdk["x402HTTPClientBase"]().encode_payment_signature_header(payment_payload)
1163
+ return await _maybe_attach_siwx_header(
1164
+ payment_required=payment_required, signer=solana_signer, headers=headers
1165
+ )
1045
1166
 
1046
1167
  if chain == "evm":
1047
1168
  sdk = _load_x402_evm_sdk()
@@ -1054,13 +1175,18 @@ async def _create_payment_headers(
1054
1175
  address = await backend.get_address()
1055
1176
  if not isinstance(address, str) or not address.strip():
1056
1177
  raise ProviderError("x402-evm", "The active EVM backend did not resolve a payer address.")
1057
- sdk["register_exact_evm_client"](
1058
- client,
1059
- _build_evm_sdk_signer(backend, address.strip()),
1060
- networks=str(selected_payment["network"]),
1061
- )
1178
+ evm_signer = _build_evm_sdk_signer(backend, address.strip())
1179
+ network = str(selected_payment["network"])
1180
+ sdk["register_exact_evm_client"](client, evm_signer, networks=network)
1181
+ # `upto` reuses the same typed-data signer as `exact`; registering both
1182
+ # lets the SDK settle whichever scheme the merchant's accepted
1183
+ # requirement (already narrowed to one entry above) turns out to use.
1184
+ client.register(network, sdk["UptoEvmScheme"](evm_signer))
1062
1185
  payment_payload = await client.create_payment_payload(selected_payload)
1063
- return sdk["x402HTTPClientBase"]().encode_payment_signature_header(payment_payload)
1186
+ headers = sdk["x402HTTPClientBase"]().encode_payment_signature_header(payment_payload)
1187
+ return await _maybe_attach_siwx_header(
1188
+ payment_required=payment_required, signer=evm_signer, headers=headers
1189
+ )
1064
1190
 
1065
1191
  raise ProviderError(
1066
1192
  "x402-http",
@@ -568,6 +568,24 @@ class WdkEvmLocalWalletBackend(AgentWalletBackend):
568
568
  except ValueError as exc:
569
569
  raise WalletBackendError("wdk-evm-wallet returned an invalid x402 EVM signature.") from exc
570
570
 
571
+ async def sign_message(self, message: bytes | str) -> str:
572
+ text = message.decode("utf-8") if isinstance(message, bytes) else str(message)
573
+ if not text:
574
+ raise WalletBackendError("message must be a non-empty string.")
575
+ data = await self.client.post(
576
+ "/v1/evm/message/sign",
577
+ {
578
+ "walletId": self.wallet_id,
579
+ "accountIndex": self.account_index,
580
+ "network": self.network,
581
+ "message": text,
582
+ },
583
+ )
584
+ signature = str(data.get("signature") or "").strip()
585
+ if not signature:
586
+ raise WalletBackendError("wdk-evm-wallet did not return a message signature.")
587
+ return signature
588
+
571
589
  async def get_balance(self, address: str | None = None) -> dict[str, Any]:
572
590
  resolved_address = await self.get_address()
573
591
  if address is not None and address.strip() and address.strip() != resolved_address:
@@ -2,7 +2,7 @@
2
2
  "id": "agent-wallet",
3
3
  "name": "Agent Wallet",
4
4
  "description": "Plugin-friendly wallet backend for OpenClaw agents with safe wallet tools and runtime instructions across Solana, local BTC, and local EVM.",
5
- "version": "0.1.67",
5
+ "version": "0.1.69",
6
6
  "skills": ["skills/wallet-operator"],
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.67"
7
+ version = "0.1.69"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "python-dotenv>=1.0.0",
17
17
  "solana>=0.36.0",
18
18
  "solders>=0.27.0",
19
- "x402[httpx,svm,evm]>=2.10.0",
19
+ "x402[httpx,svm,evm,extensions]>=2.10.0",
20
20
  ]
21
21
 
22
22
  [project.optional-dependencies]
@@ -142,17 +142,15 @@ def _auto_start_local_service(
142
142
  # steady state is a no-op. See agent_wallet.evm_user_wallets for the shared
143
143
  # health/version/stop helpers.
144
144
  from agent_wallet.evm_user_wallets import (
145
- _read_on_disk_service_version,
146
145
  _service_health,
146
+ _should_restart_local_service,
147
147
  _stop_local_service,
148
148
  )
149
149
 
150
150
  restarted = False
151
151
  health = _service_health(service_url)
152
152
  if health is not None:
153
- expected_version = _read_on_disk_service_version(wdk_wallet_root)
154
- running_version = str(health.get("version") or "").strip()
155
- if expected_version is None or running_version == expected_version:
153
+ if not _should_restart_local_service(health, wallet_root=wdk_wallet_root):
156
154
  return {"started": False, "already_healthy": True}
157
155
  _stop_local_service(service_url)
158
156
  restarted = True
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
3
  "displayName": "Agent Wallet",
4
- "version": "0.1.67",
4
+ "version": "0.1.69",
5
5
  "description": "Claude Code bridge for the existing AgentLayer wallet runtime. Connects to Solana, Bitcoin, and EVM wallets without creating a new one.",
6
6
  "author": {
7
7
  "name": "AgentLayer"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-wallet",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "Codex plugin bridge for the AgentLayer wallet runtime.",
5
5
  "author": {
6
6
  "name": "AgentLayer"
@@ -1,5 +1,5 @@
1
1
  name: agent-wallet
2
- version: 0.1.67
2
+ version: 0.1.69
3
3
  description: Thin Hermes Agent bridge to the existing AgentLayer/OpenClaw wallet backend
4
4
  provides_tools:
5
5
  - agent_wallet_tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-btc-wallet",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate BTC-only wallet service built on Tether WDK.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdk-evm-wallet",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Separate EVM wallet service built on Tether WDK.",
@@ -649,6 +649,12 @@ async function handleRequest(request, response) {
649
649
  return sendJson(response, 200, { ok: true, data });
650
650
  }
651
651
 
652
+ if (method === "POST" && url.pathname === "/v1/evm/message/sign") {
653
+ const body = await withResolvedNetwork(await withResolvedSeed(await readJsonBody(request)));
654
+ const data = await service.signPersonalMessage(body);
655
+ return sendJson(response, 200, { ok: true, data });
656
+ }
657
+
652
658
  return notFound(response);
653
659
  } catch (error) {
654
660
  const shaped = toErrorResponse(error, new URL(request.url || "/", "http://localhost").pathname, 400);
@@ -4068,6 +4068,29 @@ export class WdkEvmWalletService {
4068
4068
  });
4069
4069
  }
4070
4070
 
4071
+ async signPersonalMessage({
4072
+ seedPhrase,
4073
+ accountIndex = 0,
4074
+ network,
4075
+ message,
4076
+ }) {
4077
+ return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
4078
+ if (typeof message !== "string" || message.length === 0) {
4079
+ throw new Error("message must be a non-empty string.");
4080
+ }
4081
+ const signerAddress = normalizeAddress(await account.getAddress(), "accountAddress");
4082
+ const signature = await account.sign(message);
4083
+ return {
4084
+ network: runtimeConfig.network,
4085
+ chainId: runtimeConfig.chainId,
4086
+ accountIndex,
4087
+ address: signerAddress,
4088
+ signature,
4089
+ source: "wdk-wallet-evm",
4090
+ };
4091
+ });
4092
+ }
4093
+
4071
4094
  #resolveRuntimeConfig(networkOverride) {
4072
4095
  const network = assertValidNetwork(networkOverride) || this.config.network;
4073
4096
  const profile = this.config.networkProfiles?.[network];