@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.
- package/.openclaw/extensions/agent-wallet/dist/index.js +82 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +11 -0
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/evm_user_wallets.py +42 -9
- package/agent-wallet/agent_wallet/providers/x402.py +145 -19
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +18 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +2 -2
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +2 -4
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
- package/wdk-evm-wallet/src/server.js +6 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +23 -0
|
@@ -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.
|
|
5
|
+
"version": "0.1.69",
|
|
6
6
|
"contracts": {
|
|
7
7
|
"tools": [
|
|
8
8
|
"agentlayer_autonomous_approve",
|
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.
|
|
1
|
+
0.1.69
|
|
@@ -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.
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
return False
|
|
432
|
+
scheme = _trim(requirement.get("scheme")).lower()
|
|
433
433
|
extra = _extract_requirement_extra(requirement)
|
|
434
|
-
|
|
435
|
-
|
|
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
|
|
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
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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"
|
package/package.json
CHANGED
|
@@ -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];
|