@agentlayer.tech/wallet 0.1.10 → 0.1.12
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/CHANGELOG.md +30 -0
- package/README.md +50 -57
- package/agent-wallet/README.md +42 -0
- package/agent-wallet/agent_wallet/config.py +11 -1
- package/agent-wallet/agent_wallet/openclaw_adapter.py +74 -9
- package/agent-wallet/agent_wallet/providers/jupiter.py +171 -2
- package/agent-wallet/pyproject.toml +2 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +291 -0
- package/agent-wallet/scripts/install_agent_wallet.py +1 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +9 -0
- package/agent-wallet/scripts/manage_openclaw_evm_wallet.py +343 -0
- package/agent-wallet/scripts/setup_evm_wallet.sh +151 -0
- package/bin/openclaw-agent-wallet.mjs +229 -5
- package/hermes/plugins/agent_wallet/README.md +54 -0
- package/hermes/plugins/agent_wallet/__init__.py +55 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +9 -0
- package/hermes/plugins/agent_wallet/schemas.py +206 -0
- package/hermes/plugins/agent_wallet/tools.py +533 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Added an optional Hermes Agent bridge plugin under `hermes/plugins/agent_wallet`
|
|
6
|
+
that forwards into the existing Python wallet CLI instead of duplicating
|
|
7
|
+
OpenClaw wallet tools or policy.
|
|
8
|
+
- Added `wallet hermes install --yes` and `AGENT_WALLET_BOOT_KEY_FILE` support
|
|
9
|
+
for smoother Hermes onboarding without manual `.env` editing.
|
|
10
|
+
- Added Hermes EVM onboarding helpers:
|
|
11
|
+
`agent_wallet_evm_status` and `agent_wallet_evm_setup`.
|
|
12
|
+
- Added host-side EVM bootstrap scripts for packaged/runtime installs:
|
|
13
|
+
`manage_openclaw_evm_wallet.py`, `bootstrap_openclaw_evm.py`, and
|
|
14
|
+
`setup_evm_wallet.sh`.
|
|
15
|
+
- Kept Hermes EVM routing on the existing `wdk-evm-wallet` and
|
|
16
|
+
`provider-gateway` path for `ethereum` and `base`, without copying wallet
|
|
17
|
+
policy or duplicating tool implementations.
|
|
5
18
|
- Replaced the repo license with `PolyForm Small Business 1.0.0`.
|
|
6
19
|
- Clarified in `README.md` that individuals can audit, fork, run, and modify
|
|
7
20
|
the code for themselves, and that company use follows the PolyForm small
|
|
@@ -10,6 +23,23 @@
|
|
|
10
23
|
`wdk-evm-wallet`; cross-chain swaps now stay on LI.FI/Jupiter-backed paths
|
|
11
24
|
with Mayan denied as a LI.FI bridge.
|
|
12
25
|
|
|
26
|
+
## v0.1.12 - 2026-05-06
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Hermes EVM runtime helpers for packaged installs:
|
|
31
|
+
`agent_wallet_evm_status` and `agent_wallet_evm_setup`.
|
|
32
|
+
- Host-side EVM lifecycle scripts in the runtime bundle:
|
|
33
|
+
`manage_openclaw_evm_wallet.py`, `bootstrap_openclaw_evm.py`, and
|
|
34
|
+
`setup_evm_wallet.sh`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Hermes installs now support the same local EVM onboarding shape as BTC:
|
|
39
|
+
inspect runtime health, auto-start the local service, create or unlock the
|
|
40
|
+
vault wallet, and bind paired `ethereum/base` networks through the thin
|
|
41
|
+
bridge.
|
|
42
|
+
|
|
13
43
|
## v0.1.0-beta.2 - 2026-03-31
|
|
14
44
|
|
|
15
45
|
Second public beta release that expands the stack beyond the initial MCP, wallet, and AgentLayer bridge scope.
|
package/README.md
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|

|
|
2
|
-
|
|
3
2
|
# AgentLayer
|
|
4
3
|
|
|
4
|
+
```bash
|
|
5
|
+
npx @agentlayer.tech/wallet install --yes
|
|
6
|
+
```
|
|
7
|
+
For install to Hermes use
|
|
8
|
+
```bash
|
|
9
|
+
npx @agentlayer.tech/wallet install --yes && npx @agentlayer.tech/wallet hermes install --yes
|
|
10
|
+
```
|
|
11
|
+
|
|
5
12
|
AgentLayer is a beta local-first wallet and finance stack for agents.
|
|
6
13
|
|
|
7
14
|
The repository includes:
|
|
8
15
|
|
|
9
16
|
- `agent-wallet/` - the main wallet backend for AgentLayer
|
|
10
17
|
- `.openclaw/` - the local AgentLayer bridge layer
|
|
18
|
+
- `hermes/` - optional Hermes Agent plugin bridge for the same wallet backend
|
|
11
19
|
- `wdk-btc-wallet/` - the local Bitcoin wallet service
|
|
12
20
|
- `wdk-evm-wallet/` - the local EVM wallet service
|
|
13
21
|
- `provider-gateway/` - shared provider access for Solana RPC, Bags, and related finance reads
|
|
@@ -33,12 +41,6 @@ System prerequisites:
|
|
|
33
41
|
- `node`
|
|
34
42
|
- `npm`
|
|
35
43
|
|
|
36
|
-
Install from the latest GitHub release bundle:
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
curl -fsSL https://raw.githubusercontent.com/lopushok9/Agent-Layer/main/install-from-github.sh | sh
|
|
40
|
-
```
|
|
41
|
-
|
|
42
44
|
Install through npm:
|
|
43
45
|
|
|
44
46
|
```bash
|
|
@@ -66,6 +68,7 @@ Useful npm CLI commands:
|
|
|
66
68
|
```bash
|
|
67
69
|
wallet status
|
|
68
70
|
wallet doctor
|
|
71
|
+
wallet hermes install --yes
|
|
69
72
|
wallet update --yes
|
|
70
73
|
wallet rollback
|
|
71
74
|
```
|
|
@@ -94,28 +97,10 @@ sh agent-wallet/scripts/setup_btc_wallet.sh
|
|
|
94
97
|
EVM:
|
|
95
98
|
|
|
96
99
|
```bash
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Create a local EVM wallet binding for an OpenClaw user:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
printf '%s\n' 'your-local-evm-password' | \
|
|
104
|
-
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
|
|
105
|
-
--user-id your-user-id \
|
|
106
|
-
--password-stdin \
|
|
107
|
-
--config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
|
|
100
|
+
sh agent-wallet/scripts/setup_evm_wallet.sh
|
|
108
101
|
```
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
printf '%s\n' 'your-local-evm-password' | \
|
|
114
|
-
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-unlock \
|
|
115
|
-
--user-id your-user-id \
|
|
116
|
-
--password-stdin \
|
|
117
|
-
--config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
|
|
118
|
-
```
|
|
103
|
+
That host-side bootstrap can auto-start the local `wdk-evm-wallet` service, create or unlock the vault wallet, bind both `base` and `ethereum` for the same local user, and patch OpenClaw config to `backend=wdk_evm_local`.
|
|
119
104
|
|
|
120
105
|
That generates three fresh local secrets in the current shell session. If you prefer Python instead of `openssl`:
|
|
121
106
|
|
|
@@ -143,9 +128,33 @@ Without those secrets, the installer still lays down the runtime and installs de
|
|
|
143
128
|
}
|
|
144
129
|
```
|
|
145
130
|
|
|
131
|
+
## Connect Hermes Agent
|
|
132
|
+
|
|
133
|
+
OpenClaw remains the primary local environment, but the repo also ships an optional Hermes Agent bridge at:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
hermes/plugins/agent_wallet
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
It exposes a thin bridge, not a port of wallet logic:
|
|
140
|
+
|
|
141
|
+
- `agent_wallet_tools`
|
|
142
|
+
- `agent_wallet_invoke`
|
|
143
|
+
- `agent_wallet_approve`
|
|
144
|
+
- `agent_wallet_evm_status`
|
|
145
|
+
- `agent_wallet_evm_setup`
|
|
146
|
+
|
|
147
|
+
Install it by symlinking the plugin directory into Hermes:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx @agentlayer.tech/wallet hermes install --yes
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That command installs the Hermes plugin, runs `hermes plugins enable agent-wallet`, writes non-secret runtime paths into `~/.hermes/.env`, and points Hermes at a local boot-key file. Secrets stay in the existing protected OpenClaw runtime paths, especially `~/.openclaw/sealed_keys.json`; do not put wallet secrets into Hermes tool config.
|
|
154
|
+
|
|
146
155
|
## What you get after install
|
|
147
156
|
|
|
148
|
-
If you install
|
|
157
|
+
If you install through npm, the runtime is extracted under:
|
|
149
158
|
|
|
150
159
|
```bash
|
|
151
160
|
~/.openclaw/agent-wallet-runtime/current
|
|
@@ -159,6 +168,7 @@ The installer then does the following:
|
|
|
159
168
|
- creates a minimal `~/.openclaw/openclaw.json` if one does not exist
|
|
160
169
|
- if the required secrets are already present, writes or updates `~/.openclaw/sealed_keys.json`
|
|
161
170
|
- if the required secrets are already present, patches `~/.openclaw/openclaw.json` to load the `agent-wallet` extension and point it at the installed runtime
|
|
171
|
+
- `wallet hermes install --yes` additionally connects Hermes Agent to the same runtime without copying wallet tools or policy
|
|
162
172
|
|
|
163
173
|
When the installer reaches the final config step, the default plugin config is:
|
|
164
174
|
|
|
@@ -214,49 +224,32 @@ sh agent-wallet/scripts/reveal_btc_seed.sh
|
|
|
214
224
|
|
|
215
225
|
## EVM setup
|
|
216
226
|
|
|
217
|
-
The EVM runtime is installed by `setup.sh`,
|
|
218
|
-
|
|
219
|
-
Start the local EVM service:
|
|
227
|
+
The EVM runtime is installed by `setup.sh`, and the host-side onboarding now has the same one-command shape as BTC:
|
|
220
228
|
|
|
221
229
|
```bash
|
|
222
|
-
|
|
230
|
+
sh agent-wallet/scripts/setup_evm_wallet.sh
|
|
223
231
|
```
|
|
224
232
|
|
|
225
|
-
|
|
233
|
+
That flow:
|
|
226
234
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
- prompts for `user-id`
|
|
236
|
+
- prompts for `ethereum`, `base`, `sepolia`, or `base-sepolia`
|
|
237
|
+
- defaults to `http://127.0.0.1:8081`
|
|
238
|
+
- can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
|
|
239
|
+
- creates or unlocks the local EVM wallet binding
|
|
240
|
+
- also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
|
|
241
|
+
- patches OpenClaw config to `backend=wdk_evm_local`
|
|
234
242
|
|
|
235
|
-
|
|
243
|
+
You can still use the lower-level CLI if needed:
|
|
236
244
|
|
|
237
245
|
```bash
|
|
238
246
|
printf '%s\n' 'your-local-evm-password' | \
|
|
239
|
-
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-
|
|
247
|
+
agent-wallet/.venv/bin/python -m agent_wallet.openclaw_cli evm-wallet-create \
|
|
240
248
|
--user-id your-user-id \
|
|
241
249
|
--password-stdin \
|
|
242
250
|
--config-json '{"backend":"wdk_evm_local","network":"base","wdkEvmServiceUrl":"http://127.0.0.1:8081"}'
|
|
243
251
|
```
|
|
244
252
|
|
|
245
|
-
Then switch the OpenClaw plugin config to the EVM backend:
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
AGENT_WALLET_BOOT_KEY='...' \
|
|
249
|
-
agent-wallet/.venv/bin/python agent-wallet/scripts/install_openclaw_local_config.py \
|
|
250
|
-
--backend wdk_evm_local \
|
|
251
|
-
--network base \
|
|
252
|
-
--user-id your-user-id \
|
|
253
|
-
--package-root agent-wallet \
|
|
254
|
-
--extension-path .openclaw/extensions/agent-wallet \
|
|
255
|
-
--python-bin agent-wallet/.venv/bin/python
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
That final config step assumes `~/.openclaw/sealed_keys.json` already exists. The normal path is to let the main installer create it by running install with `AGENT_WALLET_BOOT_KEY`, `AGENT_WALLET_MASTER_KEY`, and `AGENT_WALLET_APPROVAL_SECRET` available.
|
|
259
|
-
|
|
260
253
|
Important EVM notes:
|
|
261
254
|
|
|
262
255
|
- only localhost service URLs are supported for the OpenClaw EVM flow
|
package/agent-wallet/README.md
CHANGED
|
@@ -18,6 +18,7 @@ The package now includes a thin adapter for agent runtimes:
|
|
|
18
18
|
- `agent_wallet.plugin_bundle.build_openclaw_plugin_bundle`
|
|
19
19
|
- `agent_wallet.openclaw_runtime.onboard_openclaw_user_wallet`
|
|
20
20
|
- `agent_wallet.openclaw_cli` for the official OpenClaw TypeScript plugin bridge
|
|
21
|
+
- `hermes/plugins/agent_wallet` as an optional Hermes Agent bridge to the same CLI
|
|
21
22
|
|
|
22
23
|
It provides:
|
|
23
24
|
|
|
@@ -27,6 +28,32 @@ It provides:
|
|
|
27
28
|
- OpenClaw-style plugin manifest and skill bundle
|
|
28
29
|
- explicit network-aware results so the host and agent can see `devnet` vs `mainnet`
|
|
29
30
|
|
|
31
|
+
## Hermes integration
|
|
32
|
+
|
|
33
|
+
The optional Hermes plugin is intentionally a bridge, not a port of the OpenClaw plugin. It registers:
|
|
34
|
+
|
|
35
|
+
- `agent_wallet_tools` - read-only discovery for the underlying Python adapter tool specs.
|
|
36
|
+
- `agent_wallet_invoke` - a dispatcher that calls `python -m agent_wallet.openclaw_cli invoke`.
|
|
37
|
+
- `agent_wallet_approve` - a host approval-token issuer for exact execute operations.
|
|
38
|
+
- `agent_wallet_evm_status` - a read-only EVM runtime and binding inspector.
|
|
39
|
+
- `agent_wallet_evm_setup` - a host-side EVM bootstrap helper for Hermes.
|
|
40
|
+
|
|
41
|
+
Install it with:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @agentlayer.tech/wallet hermes install --yes
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That command symlinks `hermes/plugins/agent_wallet` into `~/.hermes/plugins/agent_wallet`, enables the plugin with `hermes plugins enable agent-wallet`, and writes `AGENT_WALLET_PACKAGE_ROOT`, `AGENT_WALLET_PYTHON`, and `AGENT_WALLET_BOOT_KEY_FILE` into `~/.hermes/.env`. OpenClaw remains the canonical host integration and wallet safety policy remains in Python.
|
|
48
|
+
|
|
49
|
+
Hermes tool config must not contain wallet secrets. Use the existing sealed runtime path and host-issued approval tokens for execute flows. `AGENT_WALLET_BOOT_KEY_FILE` lets OpenClaw and Hermes reference one local boot-key file instead of duplicating the boot key across multiple env files.
|
|
50
|
+
|
|
51
|
+
For EVM on Hermes, the intended host flow is:
|
|
52
|
+
|
|
53
|
+
- call `agent_wallet_evm_status` to inspect `wdk-evm-wallet` health and current bindings
|
|
54
|
+
- call `agent_wallet_evm_setup` once to auto-start the local service when needed, create or unlock the local EVM wallet, and patch local OpenClaw config to `backend=wdk_evm_local`
|
|
55
|
+
- then use ordinary `agent_wallet_invoke` calls for EVM reads, transfers, swaps, and Aave flows
|
|
56
|
+
|
|
30
57
|
Current safe tools:
|
|
31
58
|
|
|
32
59
|
- `get_wallet_capabilities`
|
|
@@ -357,6 +384,21 @@ For the local EVM backend (`backend=wdk_evm_local`), the lifecycle mirrors the B
|
|
|
357
384
|
- `evm-wallet-unlock`
|
|
358
385
|
- `evm-wallet-lock`
|
|
359
386
|
|
|
387
|
+
For a simpler host-side bootstrap, use:
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
sh agent-wallet/scripts/setup_evm_wallet.sh
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
That wrapper:
|
|
394
|
+
|
|
395
|
+
- prompts for `user-id` and EVM network when run interactively
|
|
396
|
+
- defaults to `http://127.0.0.1:8081`
|
|
397
|
+
- can auto-start `wdk-evm-wallet/run-local.sh` if the local service is not already healthy
|
|
398
|
+
- creates or unlocks the local EVM wallet binding
|
|
399
|
+
- also binds the paired EVM network by default: `ethereum <-> base`, `sepolia <-> base-sepolia`
|
|
400
|
+
- patches OpenClaw config to `backend=wdk_evm_local`
|
|
401
|
+
|
|
360
402
|
Example host-side EVM wallet creation:
|
|
361
403
|
|
|
362
404
|
```bash
|
|
@@ -13,6 +13,7 @@ class Settings(BaseSettings):
|
|
|
13
13
|
agent_wallet_backend: str = "none"
|
|
14
14
|
agent_wallet_sign_only: bool = False
|
|
15
15
|
agent_wallet_boot_key: str = ""
|
|
16
|
+
agent_wallet_boot_key_file: str = ""
|
|
16
17
|
agent_wallet_approval_ttl_seconds: int = 600
|
|
17
18
|
agent_wallet_per_user_key_derivation: bool = True
|
|
18
19
|
agent_wallet_encrypt_user_wallets: bool = True
|
|
@@ -297,7 +298,16 @@ def _env_bool(name: str, default: bool) -> bool:
|
|
|
297
298
|
|
|
298
299
|
def resolve_boot_key() -> str:
|
|
299
300
|
"""Resolve the boot key used to unlock sealed secrets from disk."""
|
|
300
|
-
|
|
301
|
+
direct = os.getenv("AGENT_WALLET_BOOT_KEY", settings.agent_wallet_boot_key).strip()
|
|
302
|
+
if direct:
|
|
303
|
+
return direct
|
|
304
|
+
key_file = os.getenv("AGENT_WALLET_BOOT_KEY_FILE", settings.agent_wallet_boot_key_file).strip()
|
|
305
|
+
if not key_file:
|
|
306
|
+
return ""
|
|
307
|
+
try:
|
|
308
|
+
return Path(key_file).expanduser().read_text(encoding="utf-8").strip()
|
|
309
|
+
except OSError:
|
|
310
|
+
return ""
|
|
301
311
|
|
|
302
312
|
|
|
303
313
|
def _reject_legacy_runtime_secret_env(var_name: str) -> None:
|
|
@@ -11,6 +11,14 @@ from agent_wallet.models import AgentToolResult, AgentToolSpec
|
|
|
11
11
|
from agent_wallet.wallet_layer.base import AgentWalletBackend, WalletBackendError
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _canonical_json_text(payload: dict[str, Any]) -> str:
|
|
15
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def preview_payload_digest(preview: dict[str, Any]) -> str:
|
|
19
|
+
return hashlib.sha256(_canonical_json_text(preview).encode("utf-8")).hexdigest()
|
|
20
|
+
|
|
21
|
+
|
|
14
22
|
WALLET_RUNTIME_INSTRUCTIONS = """
|
|
15
23
|
Use wallet tools only when the user explicitly asks for wallet-related actions.
|
|
16
24
|
Treat any signing request as sensitive.
|
|
@@ -4398,19 +4406,76 @@ class OpenClawWalletAdapter:
|
|
|
4398
4406
|
),
|
|
4399
4407
|
)
|
|
4400
4408
|
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4409
|
+
approval_payload = inspect_approval_token(
|
|
4410
|
+
approval_token,
|
|
4411
|
+
tool_name=tool_name,
|
|
4412
|
+
network=str(getattr(self.backend, "network", "unknown")),
|
|
4413
|
+
require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
|
|
4406
4414
|
)
|
|
4415
|
+
approval_summary = approval_payload.get("binding", {}).get("summary")
|
|
4416
|
+
if not isinstance(approval_summary, dict):
|
|
4417
|
+
raise WalletBackendError(
|
|
4418
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
4419
|
+
)
|
|
4420
|
+
expected_summary = {
|
|
4421
|
+
"operation": "Swap",
|
|
4422
|
+
"network": str(getattr(self.backend, "network", "unknown")),
|
|
4423
|
+
"input_mint": input_mint.strip(),
|
|
4424
|
+
"output_mint": output_mint.strip(),
|
|
4425
|
+
"slippage_bps": slippage_bps,
|
|
4426
|
+
}
|
|
4427
|
+
for key, expected_value in expected_summary.items():
|
|
4428
|
+
if approval_summary.get(key) != expected_value:
|
|
4429
|
+
raise WalletBackendError(
|
|
4430
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
4431
|
+
)
|
|
4432
|
+
try:
|
|
4433
|
+
approved_amount = float(approval_summary.get("input_amount_ui"))
|
|
4434
|
+
except (TypeError, ValueError):
|
|
4435
|
+
raise WalletBackendError(
|
|
4436
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
4437
|
+
)
|
|
4438
|
+
if approved_amount != float(amount):
|
|
4439
|
+
raise WalletBackendError(
|
|
4440
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
4441
|
+
)
|
|
4442
|
+
|
|
4443
|
+
approval_summary_copy = dict(approval_summary)
|
|
4444
|
+
approved_preview = args.get("_approved_preview")
|
|
4445
|
+
if isinstance(approval_summary_copy.get("_preview_digest"), str):
|
|
4446
|
+
if not isinstance(approved_preview, dict):
|
|
4447
|
+
raise WalletBackendError(
|
|
4448
|
+
"Approved swap preview payload is required for execute mode. Generate a new preview and approval before execute."
|
|
4449
|
+
)
|
|
4450
|
+
if preview_payload_digest(approved_preview) != approval_summary_copy["_preview_digest"]:
|
|
4451
|
+
raise WalletBackendError(
|
|
4452
|
+
"approved preview payload does not match the approval token. Generate a new preview and approval before execute."
|
|
4453
|
+
)
|
|
4454
|
+
preview_summary = self._build_confirmation_summary(
|
|
4455
|
+
action_label="Swap",
|
|
4456
|
+
payload=approved_preview,
|
|
4457
|
+
)
|
|
4458
|
+
summary_without_digest = {
|
|
4459
|
+
key: value
|
|
4460
|
+
for key, value in approval_summary_copy.items()
|
|
4461
|
+
if key != "_preview_digest"
|
|
4462
|
+
}
|
|
4463
|
+
if preview_summary != summary_without_digest:
|
|
4464
|
+
raise WalletBackendError(
|
|
4465
|
+
"approved preview payload does not match the approval token. Generate a new preview and approval before execute."
|
|
4466
|
+
)
|
|
4467
|
+
execute_preview = dict(approved_preview)
|
|
4468
|
+
else:
|
|
4469
|
+
execute_preview = await self.backend.preview_swap(
|
|
4470
|
+
input_mint=input_mint.strip(),
|
|
4471
|
+
output_mint=output_mint.strip(),
|
|
4472
|
+
amount_ui=float(amount),
|
|
4473
|
+
slippage_bps=slippage_bps,
|
|
4474
|
+
)
|
|
4407
4475
|
self._require_execute_approval(
|
|
4408
4476
|
approval_token=approval_token,
|
|
4409
4477
|
tool_name=tool_name,
|
|
4410
|
-
summary=
|
|
4411
|
-
action_label="Swap",
|
|
4412
|
-
payload=execute_preview,
|
|
4413
|
-
),
|
|
4478
|
+
summary=approval_summary_copy,
|
|
4414
4479
|
action_label="Swap",
|
|
4415
4480
|
)
|
|
4416
4481
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -54,6 +55,38 @@ def _gateway_route_missing(status_code: int, payload: Any) -> bool:
|
|
|
54
55
|
return False
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
async def _gateway_get(path_suffix: str, *, params: dict[str, Any] | None = None) -> tuple[int, Any]:
|
|
59
|
+
"""Make a GET request through provider gateway."""
|
|
60
|
+
client = get_client()
|
|
61
|
+
response = await client.get(
|
|
62
|
+
f"{_gateway_base_url()}/v1/jupiter/swap/{path_suffix}",
|
|
63
|
+
params=params,
|
|
64
|
+
headers=_gateway_headers(),
|
|
65
|
+
)
|
|
66
|
+
if not response.content:
|
|
67
|
+
return response.status_code, {}
|
|
68
|
+
try:
|
|
69
|
+
return response.status_code, response.json()
|
|
70
|
+
except ValueError:
|
|
71
|
+
return response.status_code, response.text[:500]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _gateway_post(path_suffix: str, *, body: dict[str, Any]) -> tuple[int, Any]:
|
|
75
|
+
"""Make a POST request through provider gateway."""
|
|
76
|
+
client = get_client()
|
|
77
|
+
response = await client.post(
|
|
78
|
+
f"{_gateway_base_url()}/v1/jupiter/swap/{path_suffix}",
|
|
79
|
+
json=body,
|
|
80
|
+
headers={**_gateway_headers(), "Content-Type": "application/json"},
|
|
81
|
+
)
|
|
82
|
+
if not response.content:
|
|
83
|
+
return response.status_code, {}
|
|
84
|
+
try:
|
|
85
|
+
return response.status_code, response.json()
|
|
86
|
+
except ValueError:
|
|
87
|
+
return response.status_code, response.text[:500]
|
|
88
|
+
|
|
89
|
+
|
|
57
90
|
def _direct_jupiter_enabled() -> bool:
|
|
58
91
|
return bool(settings.jupiter_api_key.strip())
|
|
59
92
|
|
|
@@ -187,7 +220,58 @@ async def fetch_quote(
|
|
|
187
220
|
only_direct_routes: bool = False,
|
|
188
221
|
swap_mode: str = "ExactIn",
|
|
189
222
|
) -> dict[str, Any]:
|
|
190
|
-
"""Fetch a Jupiter quote for an exact-in swap.
|
|
223
|
+
"""Fetch a Jupiter quote for an exact-in swap.
|
|
224
|
+
|
|
225
|
+
Tries direct Jupiter API first. On free-tier errors (TOKEN_NOT_TRADABLE,
|
|
226
|
+
NOT_SUPPORTED) falls back to provider gateway when configured.
|
|
227
|
+
"""
|
|
228
|
+
# Try direct first
|
|
229
|
+
try:
|
|
230
|
+
return await _fetch_quote_direct(
|
|
231
|
+
input_mint=input_mint,
|
|
232
|
+
output_mint=output_mint,
|
|
233
|
+
amount_raw=amount_raw,
|
|
234
|
+
slippage_bps=slippage_bps,
|
|
235
|
+
restrict_intermediate_tokens=restrict_intermediate_tokens,
|
|
236
|
+
only_direct_routes=only_direct_routes,
|
|
237
|
+
swap_mode=swap_mode,
|
|
238
|
+
)
|
|
239
|
+
except ProviderError as exc:
|
|
240
|
+
error_msg = str(exc).lower()
|
|
241
|
+
# Only fall back for known free-tier limitations
|
|
242
|
+
gateway_fallback_errors = (
|
|
243
|
+
"not tradable",
|
|
244
|
+
"token_not_tradable",
|
|
245
|
+
"not supported",
|
|
246
|
+
"restrict_intermediate_tokens",
|
|
247
|
+
)
|
|
248
|
+
if not any(phrase in error_msg for phrase in gateway_fallback_errors):
|
|
249
|
+
raise
|
|
250
|
+
if not _gateway_enabled():
|
|
251
|
+
raise
|
|
252
|
+
# Retry via gateway with relaxed restrictions
|
|
253
|
+
return await _fetch_quote_via_gateway(
|
|
254
|
+
input_mint=input_mint,
|
|
255
|
+
output_mint=output_mint,
|
|
256
|
+
amount_raw=amount_raw,
|
|
257
|
+
slippage_bps=slippage_bps,
|
|
258
|
+
restrict_intermediate_tokens=False,
|
|
259
|
+
only_direct_routes=only_direct_routes,
|
|
260
|
+
swap_mode=swap_mode,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def _fetch_quote_direct(
|
|
265
|
+
*,
|
|
266
|
+
input_mint: str,
|
|
267
|
+
output_mint: str,
|
|
268
|
+
amount_raw: int,
|
|
269
|
+
slippage_bps: int = 50,
|
|
270
|
+
restrict_intermediate_tokens: bool = True,
|
|
271
|
+
only_direct_routes: bool = False,
|
|
272
|
+
swap_mode: str = "ExactIn",
|
|
273
|
+
) -> dict[str, Any]:
|
|
274
|
+
"""Fetch a Jupiter quote directly from Jupiter API."""
|
|
191
275
|
client = get_client()
|
|
192
276
|
params = {
|
|
193
277
|
"inputMint": input_mint,
|
|
@@ -211,6 +295,38 @@ async def fetch_quote(
|
|
|
211
295
|
return data
|
|
212
296
|
|
|
213
297
|
|
|
298
|
+
async def _fetch_quote_via_gateway(
|
|
299
|
+
*,
|
|
300
|
+
input_mint: str,
|
|
301
|
+
output_mint: str,
|
|
302
|
+
amount_raw: int,
|
|
303
|
+
slippage_bps: int = 50,
|
|
304
|
+
restrict_intermediate_tokens: bool = False,
|
|
305
|
+
only_direct_routes: bool = False,
|
|
306
|
+
swap_mode: str = "ExactIn",
|
|
307
|
+
) -> dict[str, Any]:
|
|
308
|
+
"""Fetch a Jupiter quote via provider gateway (uses API key)."""
|
|
309
|
+
params: dict[str, Any] = {
|
|
310
|
+
"inputMint": input_mint,
|
|
311
|
+
"outputMint": output_mint,
|
|
312
|
+
"amount": str(amount_raw),
|
|
313
|
+
"slippageBps": str(slippage_bps),
|
|
314
|
+
"swapMode": swap_mode,
|
|
315
|
+
}
|
|
316
|
+
if only_direct_routes:
|
|
317
|
+
params["onlyDirectRoutes"] = "true"
|
|
318
|
+
|
|
319
|
+
status_code, payload = await _gateway_get("quote", params=params)
|
|
320
|
+
if status_code != 200:
|
|
321
|
+
error_msg = payload if isinstance(payload, str) else json.dumps(payload)
|
|
322
|
+
raise ProviderError("jupiter-gateway", f"HTTP {status_code}: {error_msg}")
|
|
323
|
+
if isinstance(payload, dict) and payload.get("errorCode"):
|
|
324
|
+
raise ProviderError("jupiter-gateway", str(payload.get("error") or payload.get("errorCode")))
|
|
325
|
+
if not isinstance(payload, dict) or "outAmount" not in payload:
|
|
326
|
+
raise ProviderError("jupiter-gateway", "Unexpected quote response from gateway.")
|
|
327
|
+
return payload
|
|
328
|
+
|
|
329
|
+
|
|
214
330
|
async def fetch_ultra_order(
|
|
215
331
|
*,
|
|
216
332
|
input_mint: str,
|
|
@@ -257,7 +373,35 @@ async def build_swap_transaction(
|
|
|
257
373
|
quote_response: dict[str, Any],
|
|
258
374
|
wrap_and_unwrap_sol: bool = True,
|
|
259
375
|
) -> dict[str, Any]:
|
|
260
|
-
"""Build a serialized swap transaction from a Jupiter quote.
|
|
376
|
+
"""Build a serialized swap transaction from a Jupiter quote.
|
|
377
|
+
|
|
378
|
+
Tries direct Jupiter API first. Falls back to provider gateway on error.
|
|
379
|
+
"""
|
|
380
|
+
# Try direct first
|
|
381
|
+
try:
|
|
382
|
+
return await _build_swap_direct(
|
|
383
|
+
user_public_key=user_public_key,
|
|
384
|
+
quote_response=quote_response,
|
|
385
|
+
wrap_and_unwrap_sol=wrap_and_unwrap_sol,
|
|
386
|
+
)
|
|
387
|
+
except ProviderError as exc:
|
|
388
|
+
if not _gateway_enabled():
|
|
389
|
+
raise
|
|
390
|
+
# Fall back to gateway
|
|
391
|
+
return await _build_swap_via_gateway(
|
|
392
|
+
user_public_key=user_public_key,
|
|
393
|
+
quote_response=quote_response,
|
|
394
|
+
wrap_and_unwrap_sol=wrap_and_unwrap_sol,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
async def _build_swap_direct(
|
|
399
|
+
*,
|
|
400
|
+
user_public_key: str,
|
|
401
|
+
quote_response: dict[str, Any],
|
|
402
|
+
wrap_and_unwrap_sol: bool = True,
|
|
403
|
+
) -> dict[str, Any]:
|
|
404
|
+
"""Build a swap transaction directly via Jupiter API."""
|
|
261
405
|
client = get_client()
|
|
262
406
|
body = {
|
|
263
407
|
"userPublicKey": user_public_key,
|
|
@@ -279,6 +423,31 @@ async def build_swap_transaction(
|
|
|
279
423
|
return data
|
|
280
424
|
|
|
281
425
|
|
|
426
|
+
async def _build_swap_via_gateway(
|
|
427
|
+
*,
|
|
428
|
+
user_public_key: str,
|
|
429
|
+
quote_response: dict[str, Any],
|
|
430
|
+
wrap_and_unwrap_sol: bool = True,
|
|
431
|
+
) -> dict[str, Any]:
|
|
432
|
+
"""Build a swap transaction via provider gateway (uses API key)."""
|
|
433
|
+
body = {
|
|
434
|
+
"userPublicKey": user_public_key,
|
|
435
|
+
"quoteResponse": quote_response,
|
|
436
|
+
"wrapAndUnwrapSol": wrap_and_unwrap_sol,
|
|
437
|
+
"dynamicComputeUnitLimit": True,
|
|
438
|
+
"prioritizationFeeLamports": "auto",
|
|
439
|
+
}
|
|
440
|
+
status_code, payload = await _gateway_post("swap", body=body)
|
|
441
|
+
if status_code != 200:
|
|
442
|
+
error_msg = payload if isinstance(payload, str) else json.dumps(payload)
|
|
443
|
+
raise ProviderError("jupiter-gateway", f"HTTP {status_code}: {error_msg}")
|
|
444
|
+
if isinstance(payload, dict) and payload.get("errorCode"):
|
|
445
|
+
raise ProviderError("jupiter-gateway", str(payload.get("error") or payload.get("errorCode")))
|
|
446
|
+
if not isinstance(payload, dict) or "swapTransaction" not in payload:
|
|
447
|
+
raise ProviderError("jupiter-gateway", "Unexpected swap response from gateway.")
|
|
448
|
+
return payload
|
|
449
|
+
|
|
450
|
+
|
|
282
451
|
async def execute_ultra_order(
|
|
283
452
|
*,
|
|
284
453
|
signed_transaction_base64: str,
|
|
@@ -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.12"
|
|
8
8
|
description = "Plugin-friendly wallet backend for OpenClaw agents"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
@@ -14,6 +14,7 @@ dependencies = [
|
|
|
14
14
|
"pynacl>=1.5.0",
|
|
15
15
|
"python-dotenv>=1.0.0",
|
|
16
16
|
"solana>=0.36.0",
|
|
17
|
+
"solders>=0.27.0",
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
[project.optional-dependencies]
|