@agentlayer.tech/wallet 0.1.66 → 0.1.68
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/index.ts +82 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +7 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/README.md +6 -0
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +481 -4
- package/agent-wallet/agent_wallet/providers/kamino.py +169 -3
- package/agent-wallet/agent_wallet/providers/x402.py +145 -19
- package/agent-wallet/agent_wallet/transaction_policy.py +60 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +124 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +563 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +34 -0
- package/agent-wallet/openclaw.plugin.json +2 -2
- package/agent-wallet/pyproject.toml +2 -2
- package/agent-wallet/scripts/install_openclaw_local_config.py +6 -0
- package/agent-wallet/skills/wallet-operator/SKILL.md +5 -3
- 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 +149 -19
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
5
7
|
from typing import Any
|
|
6
8
|
|
|
7
9
|
from agent_wallet.config import normalize_solana_network, settings
|
|
@@ -9,6 +11,41 @@ from agent_wallet.exceptions import ProviderError
|
|
|
9
11
|
from agent_wallet.http_client import get_client
|
|
10
12
|
|
|
11
13
|
KAMINO_BUILD_TIMEOUT_SECONDS = 20.0
|
|
14
|
+
# Discovery data only (market/vault catalogs and rate metrics) — never
|
|
15
|
+
# user-specific data such as obligations, positions, rewards, or portfolios.
|
|
16
|
+
# Catalogs change when Kamino ships a market/vault (days); rate metrics drift
|
|
17
|
+
# slower than the API's own aggregation window over these TTLs. The cache is
|
|
18
|
+
# long-lived only inside the resident read worker; cold CLI runs start empty.
|
|
19
|
+
MARKETS_CACHE_TTL_SECONDS = 600.0
|
|
20
|
+
RESERVES_CACHE_TTL_SECONDS = 180.0
|
|
21
|
+
EARN_VAULTS_CACHE_TTL_SECONDS = 600.0
|
|
22
|
+
EARN_VAULT_METRICS_CACHE_TTL_SECONDS = 180.0
|
|
23
|
+
_DISCOVERY_CACHE_MAX_ENTRIES = 128
|
|
24
|
+
_discovery_cache: dict[str, tuple[float, str]] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _discovery_cache_get(cache_key: str) -> Any | None:
|
|
28
|
+
entry = _discovery_cache.get(cache_key)
|
|
29
|
+
if entry is None:
|
|
30
|
+
return None
|
|
31
|
+
expires_at, payload_text = entry
|
|
32
|
+
if expires_at <= time.monotonic():
|
|
33
|
+
_discovery_cache.pop(cache_key, None)
|
|
34
|
+
return None
|
|
35
|
+
return json.loads(payload_text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _discovery_cache_set(cache_key: str, payload: Any, ttl_seconds: float) -> None:
|
|
39
|
+
try:
|
|
40
|
+
payload_text = json.dumps(payload)
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return
|
|
43
|
+
if len(_discovery_cache) >= _DISCOVERY_CACHE_MAX_ENTRIES:
|
|
44
|
+
for stale_key in sorted(_discovery_cache, key=lambda key: _discovery_cache[key][0])[
|
|
45
|
+
: len(_discovery_cache) - _DISCOVERY_CACHE_MAX_ENTRIES + 1
|
|
46
|
+
]:
|
|
47
|
+
_discovery_cache.pop(stale_key, None)
|
|
48
|
+
_discovery_cache[cache_key] = (time.monotonic() + ttl_seconds, payload_text)
|
|
12
49
|
|
|
13
50
|
|
|
14
51
|
def _normalized_api_base() -> str:
|
|
@@ -48,6 +85,10 @@ def _env_name(network: str) -> str:
|
|
|
48
85
|
|
|
49
86
|
async def fetch_lend_markets() -> dict[str, Any]:
|
|
50
87
|
"""Fetch Kamino lending markets for the configured program id."""
|
|
88
|
+
cache_key = f"markets:{settings.kamino_program_id}"
|
|
89
|
+
cached = _discovery_cache_get(cache_key)
|
|
90
|
+
if cached is not None:
|
|
91
|
+
return cached
|
|
51
92
|
client = get_client()
|
|
52
93
|
response = await client.get(
|
|
53
94
|
f"{_normalized_api_base()}/v2/kamino-market",
|
|
@@ -55,11 +96,85 @@ async def fetch_lend_markets() -> dict[str, Any]:
|
|
|
55
96
|
)
|
|
56
97
|
if response.status_code != 200:
|
|
57
98
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
58
|
-
|
|
99
|
+
result = _normalize_named_list_response(
|
|
59
100
|
response.json(),
|
|
60
101
|
key="markets",
|
|
61
102
|
provider_name="kamino",
|
|
62
103
|
)
|
|
104
|
+
_discovery_cache_set(cache_key, result, MARKETS_CACHE_TTL_SECONDS)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def fetch_portfolio(*, user: str) -> dict[str, Any]:
|
|
109
|
+
"""Fetch the unified Kamino product portfolio for one wallet."""
|
|
110
|
+
client = get_client()
|
|
111
|
+
response = await client.get(f"{_normalized_api_base()}/portfolio/{user}")
|
|
112
|
+
if response.status_code != 200:
|
|
113
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
114
|
+
data = response.json()
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
raise ProviderError("kamino", "Unexpected portfolio response from Kamino.")
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def fetch_earn_vaults() -> dict[str, Any]:
|
|
121
|
+
"""Fetch the list of Kamino Earn vaults."""
|
|
122
|
+
cache_key = "earn_vaults"
|
|
123
|
+
cached = _discovery_cache_get(cache_key)
|
|
124
|
+
if cached is not None:
|
|
125
|
+
return cached
|
|
126
|
+
client = get_client()
|
|
127
|
+
response = await client.get(f"{_normalized_api_base()}/kvaults/vaults")
|
|
128
|
+
if response.status_code != 200:
|
|
129
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
130
|
+
result = _normalize_named_list_response(
|
|
131
|
+
response.json(),
|
|
132
|
+
key="vaults",
|
|
133
|
+
provider_name="kamino",
|
|
134
|
+
)
|
|
135
|
+
_discovery_cache_set(cache_key, result, EARN_VAULTS_CACHE_TTL_SECONDS)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def fetch_earn_vault_metrics(*, vault: str, network: str) -> dict[str, Any]:
|
|
140
|
+
"""Fetch APY/TVL metrics for one Kamino Earn vault."""
|
|
141
|
+
env = _env_name(network)
|
|
142
|
+
cache_key = f"earn_vault_metrics:{vault}:{env}"
|
|
143
|
+
cached = _discovery_cache_get(cache_key)
|
|
144
|
+
if cached is not None:
|
|
145
|
+
return cached
|
|
146
|
+
client = get_client()
|
|
147
|
+
response = await client.get(
|
|
148
|
+
f"{_normalized_api_base()}/kvaults/{vault}/metrics",
|
|
149
|
+
params={"env": env},
|
|
150
|
+
)
|
|
151
|
+
if response.status_code != 200:
|
|
152
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
153
|
+
data = response.json()
|
|
154
|
+
if not isinstance(data, dict):
|
|
155
|
+
raise ProviderError("kamino", "Unexpected vault metrics response from Kamino.")
|
|
156
|
+
_discovery_cache_set(cache_key, data, EARN_VAULT_METRICS_CACHE_TTL_SECONDS)
|
|
157
|
+
return data
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def fetch_earn_user_positions(
|
|
161
|
+
*,
|
|
162
|
+
user: str,
|
|
163
|
+
network: str,
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
"""Fetch all Kamino Earn vault positions for a wallet."""
|
|
166
|
+
client = get_client()
|
|
167
|
+
response = await client.get(
|
|
168
|
+
f"{_normalized_api_base()}/kvaults/users/{user}/positions",
|
|
169
|
+
params={"env": _env_name(network)},
|
|
170
|
+
)
|
|
171
|
+
if response.status_code != 200:
|
|
172
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
173
|
+
return _normalize_named_list_response(
|
|
174
|
+
response.json(),
|
|
175
|
+
key="positions",
|
|
176
|
+
provider_name="kamino",
|
|
177
|
+
)
|
|
63
178
|
|
|
64
179
|
|
|
65
180
|
async def fetch_lend_market_reserves(
|
|
@@ -68,18 +183,25 @@ async def fetch_lend_market_reserves(
|
|
|
68
183
|
network: str,
|
|
69
184
|
) -> dict[str, Any]:
|
|
70
185
|
"""Fetch reserve metrics for one Kamino lending market."""
|
|
186
|
+
env = _env_name(network)
|
|
187
|
+
cache_key = f"reserves:{market}:{env}"
|
|
188
|
+
cached = _discovery_cache_get(cache_key)
|
|
189
|
+
if cached is not None:
|
|
190
|
+
return cached
|
|
71
191
|
client = get_client()
|
|
72
192
|
response = await client.get(
|
|
73
193
|
f"{_normalized_api_base()}/kamino-market/{market}/reserves/metrics",
|
|
74
|
-
params={"env":
|
|
194
|
+
params={"env": env},
|
|
75
195
|
)
|
|
76
196
|
if response.status_code != 200:
|
|
77
197
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
78
|
-
|
|
198
|
+
result = _normalize_named_list_response(
|
|
79
199
|
response.json(),
|
|
80
200
|
key="reserves",
|
|
81
201
|
provider_name="kamino",
|
|
82
202
|
)
|
|
203
|
+
_discovery_cache_set(cache_key, result, RESERVES_CACHE_TTL_SECONDS)
|
|
204
|
+
return result
|
|
83
205
|
|
|
84
206
|
|
|
85
207
|
async def fetch_lend_user_obligations(
|
|
@@ -236,3 +358,47 @@ async def build_lend_repay_transaction(
|
|
|
236
358
|
if response.status_code != 200:
|
|
237
359
|
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
238
360
|
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
async def build_earn_deposit_transaction(
|
|
364
|
+
*,
|
|
365
|
+
wallet: str,
|
|
366
|
+
kvault: str,
|
|
367
|
+
amount_ui: str,
|
|
368
|
+
) -> dict[str, Any]:
|
|
369
|
+
"""Build an unsigned Kamino Earn deposit transaction."""
|
|
370
|
+
client = get_client()
|
|
371
|
+
response = await client.post(
|
|
372
|
+
f"{_normalized_api_base()}/ktx/kvault/deposit",
|
|
373
|
+
json={
|
|
374
|
+
"wallet": wallet,
|
|
375
|
+
"kvault": kvault,
|
|
376
|
+
"amount": amount_ui,
|
|
377
|
+
},
|
|
378
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
379
|
+
)
|
|
380
|
+
if response.status_code != 200:
|
|
381
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
382
|
+
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async def build_earn_withdraw_transaction(
|
|
386
|
+
*,
|
|
387
|
+
wallet: str,
|
|
388
|
+
kvault: str,
|
|
389
|
+
amount_ui: str,
|
|
390
|
+
) -> dict[str, Any]:
|
|
391
|
+
"""Build an unsigned Kamino Earn withdraw transaction."""
|
|
392
|
+
client = get_client()
|
|
393
|
+
response = await client.post(
|
|
394
|
+
f"{_normalized_api_base()}/ktx/kvault/withdraw",
|
|
395
|
+
json={
|
|
396
|
+
"wallet": wallet,
|
|
397
|
+
"kvault": kvault,
|
|
398
|
+
"amount": amount_ui,
|
|
399
|
+
},
|
|
400
|
+
timeout=KAMINO_BUILD_TIMEOUT_SECONDS,
|
|
401
|
+
)
|
|
402
|
+
if response.status_code != 200:
|
|
403
|
+
raise ProviderError("kamino", f"HTTP {response.status_code}: {response.text[:300]}")
|
|
404
|
+
return _normalized_tx_response(response.json(), provider_name="kamino")
|
|
@@ -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",
|
|
@@ -18,6 +18,7 @@ JUPITER_V6_PROGRAM_ID = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5Nt7NQYjN"
|
|
|
18
18
|
JUPITER_ULTRA_EXACT_OUT_PROGRAM_ID = "j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X"
|
|
19
19
|
JUPITER_DCA_PROGRAM_ID = "DCA265Vj8a7wYymQG8LqM3m7A4QeV9hiC7VYh4S6Jsa"
|
|
20
20
|
KAMINO_LEND_PROGRAM_ID = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD"
|
|
21
|
+
KAMINO_EARN_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd"
|
|
21
22
|
NATIVE_SOL_MINT = "So11111111111111111111111111111111111111112"
|
|
22
23
|
DEFAULT_NATIVE_SOL_EXTRA_SPEND_ALLOWANCE_LAMPORTS = 10_000_000
|
|
23
24
|
|
|
@@ -45,6 +46,7 @@ FORBIDDEN_PROGRAMS = {
|
|
|
45
46
|
}
|
|
46
47
|
KAMINO_ALLOWED_PROGRAMS = CORE_PROGRAM_IDS | {
|
|
47
48
|
KAMINO_LEND_PROGRAM_ID,
|
|
49
|
+
KAMINO_EARN_PROGRAM_ID,
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
|
|
@@ -513,6 +515,64 @@ def verify_provider_kamino_lend_transaction(
|
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
|
|
518
|
+
def verify_provider_kamino_earn_transaction(
|
|
519
|
+
message: Any,
|
|
520
|
+
*,
|
|
521
|
+
wallet_address: str,
|
|
522
|
+
vault_address: str,
|
|
523
|
+
action: str,
|
|
524
|
+
vault_token_mint: str | None = None,
|
|
525
|
+
loaded_addresses: list[str] | None = None,
|
|
526
|
+
) -> dict[str, Any]:
|
|
527
|
+
binding = _assert_basic_wallet_binding(
|
|
528
|
+
message,
|
|
529
|
+
wallet_address=wallet_address,
|
|
530
|
+
loaded_addresses=loaded_addresses,
|
|
531
|
+
)
|
|
532
|
+
keys = binding["account_keys"]
|
|
533
|
+
if vault_address not in keys:
|
|
534
|
+
raise WalletBackendError(
|
|
535
|
+
f"{action} transaction does not reference the expected Kamino Earn vault."
|
|
536
|
+
)
|
|
537
|
+
if vault_token_mint and vault_token_mint not in keys:
|
|
538
|
+
raise WalletBackendError(
|
|
539
|
+
f"{action} transaction does not reference the expected Kamino Earn vault token mint."
|
|
540
|
+
)
|
|
541
|
+
program_ids = _program_ids(message, loaded_addresses)
|
|
542
|
+
unknown_program_ids = _assert_program_allowlist(
|
|
543
|
+
program_ids,
|
|
544
|
+
allowed_programs=KAMINO_ALLOWED_PROGRAMS,
|
|
545
|
+
label=action,
|
|
546
|
+
reject_unknown=False,
|
|
547
|
+
)
|
|
548
|
+
recognized_kamino_program_ids = [
|
|
549
|
+
pid for pid in program_ids if pid == KAMINO_EARN_PROGRAM_ID
|
|
550
|
+
]
|
|
551
|
+
if not recognized_kamino_program_ids:
|
|
552
|
+
raise WalletBackendError(
|
|
553
|
+
f"{action} transaction does not include the expected Kamino Earn program."
|
|
554
|
+
)
|
|
555
|
+
return {
|
|
556
|
+
"wallet_address": wallet_address,
|
|
557
|
+
"fee_payer": binding["fee_payer"],
|
|
558
|
+
"required_signer_keys": binding["required_signer_keys"],
|
|
559
|
+
"required_signature_count": binding["required_signature_count"],
|
|
560
|
+
"wallet_signer_index": binding["wallet_signer_index"],
|
|
561
|
+
"sponsored_fee_payer": binding["sponsored_fee_payer"],
|
|
562
|
+
"program_ids": program_ids,
|
|
563
|
+
"unknown_program_ids": unknown_program_ids,
|
|
564
|
+
"recognized_kamino_program_ids": recognized_kamino_program_ids,
|
|
565
|
+
"has_recognized_kamino_program": True,
|
|
566
|
+
"non_core_program_ids": [pid for pid in program_ids if pid not in CORE_PROGRAM_IDS],
|
|
567
|
+
"account_key_count": len(keys),
|
|
568
|
+
"instruction_count": len(_compiled_instructions(message)),
|
|
569
|
+
"vault_address": vault_address,
|
|
570
|
+
"vault_token_mint": vault_token_mint,
|
|
571
|
+
"action": action,
|
|
572
|
+
"verified": True,
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
|
|
516
576
|
def verify_provider_flash_transaction(
|
|
517
577
|
message: Any,
|
|
518
578
|
*,
|