@agentlayer.tech/wallet 0.1.0
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/AGENTS.md +98 -0
- package/.openclaw/extensions/agent-wallet/README.md +127 -0
- package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
- package/.openclaw/extensions/agent-wallet/package.json +11 -0
- package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +104 -0
- package/README.md +332 -0
- package/RELEASING.md +204 -0
- package/agent-wallet/.env.example +62 -0
- package/agent-wallet/AGENTS.md +129 -0
- package/agent-wallet/README.md +527 -0
- package/agent-wallet/agent_wallet/__init__.py +11 -0
- package/agent-wallet/agent_wallet/approval.py +161 -0
- package/agent-wallet/agent_wallet/bootstrap.py +178 -0
- package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
- package/agent-wallet/agent_wallet/config.py +382 -0
- package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
- package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
- package/agent-wallet/agent_wallet/exceptions.py +9 -0
- package/agent-wallet/agent_wallet/file_ops.py +34 -0
- package/agent-wallet/agent_wallet/http_client.py +25 -0
- package/agent-wallet/agent_wallet/models.py +66 -0
- package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
- package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
- package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
- package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
- package/agent-wallet/agent_wallet/providers/bags.py +259 -0
- package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
- package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
- package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
- package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
- package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
- package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
- package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
- package/agent-wallet/agent_wallet/solana_stake.py +103 -0
- package/agent-wallet/agent_wallet/solana_tx.py +93 -0
- package/agent-wallet/agent_wallet/spending_limits.py +101 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
- package/agent-wallet/agent_wallet/user_wallets.py +355 -0
- package/agent-wallet/agent_wallet/validation.py +31 -0
- package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
- package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
- package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
- package/agent-wallet/examples/bootstrap_wallet.py +21 -0
- package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
- package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
- package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
- package/agent-wallet/openclaw.plugin.json +138 -0
- package/agent-wallet/pyproject.toml +31 -0
- package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
- package/agent-wallet/scripts/build_release_bundle.py +188 -0
- package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
- package/agent-wallet/scripts/install_agent_wallet.py +505 -0
- package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
- package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
- package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
- package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
- package/agent-wallet/scripts/security_utils.py +37 -0
- package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
- package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
- package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
- package/bin/openclaw-agent-wallet.mjs +487 -0
- package/install-from-github.sh +134 -0
- package/package.json +61 -0
- package/setup.sh +40 -0
- package/wdk-btc-wallet/README.md +325 -0
- package/wdk-btc-wallet/bootstrap.sh +22 -0
- package/wdk-btc-wallet/package-lock.json +1839 -0
- package/wdk-btc-wallet/package.json +18 -0
- package/wdk-btc-wallet/run-local.sh +21 -0
- package/wdk-btc-wallet/src/config.js +160 -0
- package/wdk-btc-wallet/src/json.js +35 -0
- package/wdk-btc-wallet/src/local_vault.js +432 -0
- package/wdk-btc-wallet/src/network_state.js +84 -0
- package/wdk-btc-wallet/src/server.js +257 -0
- package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
- package/wdk-evm-wallet/README.md +183 -0
- package/wdk-evm-wallet/bootstrap.sh +8 -0
- package/wdk-evm-wallet/package-lock.json +2340 -0
- package/wdk-evm-wallet/package.json +23 -0
- package/wdk-evm-wallet/run-local.sh +12 -0
- package/wdk-evm-wallet/src/config.js +274 -0
- package/wdk-evm-wallet/src/json.js +35 -0
- package/wdk-evm-wallet/src/local_vault.js +430 -0
- package/wdk-evm-wallet/src/network_state.js +92 -0
- package/wdk-evm-wallet/src/server.js +575 -0
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +4981 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Host-issued approval tokens for sensitive wallet actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from agent_wallet.config import resolve_approval_secret, settings
|
|
13
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
14
|
+
|
|
15
|
+
APPROVAL_TOKEN_VERSION = 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _canonical_json(payload: dict[str, Any]) -> bytes:
|
|
19
|
+
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _urlsafe_b64(data: bytes) -> str:
|
|
23
|
+
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _urlsafe_b64_decode(data: str) -> bytes:
|
|
27
|
+
padding = "=" * (-len(data) % 4)
|
|
28
|
+
return base64.urlsafe_b64decode(data + padding)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _sign_payload(payload: dict[str, Any], secret: str) -> str:
|
|
32
|
+
digest = hmac.new(secret.encode("utf-8"), _canonical_json(payload), hashlib.sha256).digest()
|
|
33
|
+
return _urlsafe_b64(digest)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _approval_secret() -> str:
|
|
37
|
+
secret = resolve_approval_secret().strip()
|
|
38
|
+
if not secret:
|
|
39
|
+
raise WalletBackendError(
|
|
40
|
+
"Execute mode requires AGENT_WALLET_BOOT_KEY and a sealed approval_secret. "
|
|
41
|
+
"The host must issue approval tokens after explicit user confirmation."
|
|
42
|
+
)
|
|
43
|
+
return secret
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_operation_binding(*, tool_name: str, network: str, summary: dict[str, Any]) -> dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"tool": tool_name,
|
|
49
|
+
"network": str(network).strip().lower(),
|
|
50
|
+
"summary": summary,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def issue_approval_token(
|
|
55
|
+
*,
|
|
56
|
+
tool_name: str,
|
|
57
|
+
network: str,
|
|
58
|
+
summary: dict[str, Any],
|
|
59
|
+
mainnet_confirmed: bool = False,
|
|
60
|
+
ttl_seconds: int | None = None,
|
|
61
|
+
issued_by: str = "host",
|
|
62
|
+
) -> str:
|
|
63
|
+
secret = _approval_secret()
|
|
64
|
+
now = int(time.time())
|
|
65
|
+
ttl = ttl_seconds if ttl_seconds is not None else int(settings.agent_wallet_approval_ttl_seconds)
|
|
66
|
+
if ttl <= 0:
|
|
67
|
+
raise WalletBackendError("Approval token ttl must be greater than zero.")
|
|
68
|
+
|
|
69
|
+
payload = {
|
|
70
|
+
"v": APPROVAL_TOKEN_VERSION,
|
|
71
|
+
"iat": now,
|
|
72
|
+
"exp": now + ttl,
|
|
73
|
+
"issued_by": issued_by,
|
|
74
|
+
"binding": build_operation_binding(tool_name=tool_name, network=network, summary=summary),
|
|
75
|
+
"mainnet_confirmed": bool(mainnet_confirmed),
|
|
76
|
+
}
|
|
77
|
+
signature = _sign_payload(payload, secret)
|
|
78
|
+
return f"{_urlsafe_b64(_canonical_json(payload))}.{signature}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def verify_approval_token(
|
|
82
|
+
token: str,
|
|
83
|
+
*,
|
|
84
|
+
tool_name: str,
|
|
85
|
+
network: str,
|
|
86
|
+
summary: dict[str, Any],
|
|
87
|
+
require_mainnet_confirmation: bool,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
secret = _approval_secret()
|
|
90
|
+
if not isinstance(token, str) or "." not in token:
|
|
91
|
+
raise WalletBackendError("A valid approval_token is required for execute mode.")
|
|
92
|
+
|
|
93
|
+
encoded_payload, encoded_sig = token.split(".", 1)
|
|
94
|
+
try:
|
|
95
|
+
payload = json.loads(_urlsafe_b64_decode(encoded_payload).decode("utf-8"))
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
raise WalletBackendError("approval_token could not be parsed.") from exc
|
|
98
|
+
|
|
99
|
+
if not isinstance(payload, dict) or int(payload.get("v") or 0) != APPROVAL_TOKEN_VERSION:
|
|
100
|
+
raise WalletBackendError("approval_token version is invalid.")
|
|
101
|
+
|
|
102
|
+
expected_sig = _sign_payload(payload, secret)
|
|
103
|
+
if not hmac.compare_digest(encoded_sig, expected_sig):
|
|
104
|
+
raise WalletBackendError("approval_token signature is invalid.")
|
|
105
|
+
|
|
106
|
+
now = int(time.time())
|
|
107
|
+
if int(payload.get("exp") or 0) < now:
|
|
108
|
+
raise WalletBackendError("approval_token has expired.")
|
|
109
|
+
|
|
110
|
+
expected_binding = build_operation_binding(tool_name=tool_name, network=network, summary=summary)
|
|
111
|
+
if payload.get("binding") != expected_binding:
|
|
112
|
+
raise WalletBackendError(
|
|
113
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
114
|
+
)
|
|
115
|
+
if require_mainnet_confirmation and payload.get("mainnet_confirmed") is not True:
|
|
116
|
+
raise WalletBackendError("approval_token is missing explicit mainnet confirmation.")
|
|
117
|
+
return payload
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def inspect_approval_token(
|
|
121
|
+
token: str,
|
|
122
|
+
*,
|
|
123
|
+
tool_name: str,
|
|
124
|
+
network: str,
|
|
125
|
+
require_mainnet_confirmation: bool,
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
secret = _approval_secret()
|
|
128
|
+
if not isinstance(token, str) or "." not in token:
|
|
129
|
+
raise WalletBackendError("A valid approval_token is required for execute mode.")
|
|
130
|
+
|
|
131
|
+
encoded_payload, encoded_sig = token.split(".", 1)
|
|
132
|
+
try:
|
|
133
|
+
payload = json.loads(_urlsafe_b64_decode(encoded_payload).decode("utf-8"))
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
raise WalletBackendError("approval_token could not be parsed.") from exc
|
|
136
|
+
|
|
137
|
+
if not isinstance(payload, dict) or int(payload.get("v") or 0) != APPROVAL_TOKEN_VERSION:
|
|
138
|
+
raise WalletBackendError("approval_token version is invalid.")
|
|
139
|
+
|
|
140
|
+
expected_sig = _sign_payload(payload, secret)
|
|
141
|
+
if not hmac.compare_digest(encoded_sig, expected_sig):
|
|
142
|
+
raise WalletBackendError("approval_token signature is invalid.")
|
|
143
|
+
|
|
144
|
+
now = int(time.time())
|
|
145
|
+
if int(payload.get("exp") or 0) < now:
|
|
146
|
+
raise WalletBackendError("approval_token has expired.")
|
|
147
|
+
|
|
148
|
+
binding = payload.get("binding")
|
|
149
|
+
if not isinstance(binding, dict):
|
|
150
|
+
raise WalletBackendError("approval_token binding is invalid.")
|
|
151
|
+
if str(binding.get("tool") or "") != tool_name:
|
|
152
|
+
raise WalletBackendError(
|
|
153
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
154
|
+
)
|
|
155
|
+
if str(binding.get("network") or "").strip().lower() != str(network).strip().lower():
|
|
156
|
+
raise WalletBackendError(
|
|
157
|
+
"approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
|
|
158
|
+
)
|
|
159
|
+
if require_mainnet_confirmation and payload.get("mainnet_confirmed") is not True:
|
|
160
|
+
raise WalletBackendError("approval_token is missing explicit mainnet confirmation.")
|
|
161
|
+
return payload
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Bootstrap helpers for provisioning agent wallets on first use."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agent_wallet.config import refuse_mainnet_wallet_recreation
|
|
9
|
+
from agent_wallet.file_ops import atomic_write_text
|
|
10
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
11
|
+
from agent_wallet.wallet_layer.base58 import b58encode
|
|
12
|
+
|
|
13
|
+
WALLET_ADDRESS_PIN_KIND = "openclaw-agent-wallet-address-pin"
|
|
14
|
+
WALLET_ADDRESS_PIN_VERSION = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _keypair_bytes_for_file(secret_key: bytes, public_key: bytes) -> list[int]:
|
|
18
|
+
return list(secret_key + public_key)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_solana_wallet_material() -> dict[str, str]:
|
|
22
|
+
"""Generate new Solana secret material without writing it to disk."""
|
|
23
|
+
try:
|
|
24
|
+
from nacl.signing import SigningKey
|
|
25
|
+
except ImportError as exc:
|
|
26
|
+
raise WalletBackendError(
|
|
27
|
+
"PyNaCl is required to auto-create a local Solana wallet."
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
signing_key = SigningKey.generate()
|
|
31
|
+
secret_key = signing_key.encode()
|
|
32
|
+
public_key = bytes(signing_key.verify_key)
|
|
33
|
+
return {
|
|
34
|
+
"address": b58encode(public_key),
|
|
35
|
+
"secret_material": json.dumps(_keypair_bytes_for_file(secret_key, public_key), indent=2),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_solana_wallet_file(path: Path) -> dict[str, str]:
|
|
40
|
+
"""Create a new local Solana wallet file in Solana CLI JSON format."""
|
|
41
|
+
material = generate_solana_wallet_material()
|
|
42
|
+
atomic_write_text(path, material["secret_material"], mode=0o600)
|
|
43
|
+
return {
|
|
44
|
+
"address": material["address"],
|
|
45
|
+
"path": str(path),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_wallet_pin_path(path: Path) -> Path:
|
|
50
|
+
"""Return the sidecar file used to pin an expected wallet address."""
|
|
51
|
+
return path.with_suffix(f"{path.suffix}.pin.json")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_wallet_pin(path: Path) -> dict[str, str] | None:
|
|
55
|
+
"""Load wallet pin metadata if present and valid."""
|
|
56
|
+
pin_path = resolve_wallet_pin_path(path)
|
|
57
|
+
if not pin_path.exists():
|
|
58
|
+
return None
|
|
59
|
+
try:
|
|
60
|
+
payload = json.loads(pin_path.read_text(encoding="utf-8"))
|
|
61
|
+
except json.JSONDecodeError as exc:
|
|
62
|
+
raise WalletBackendError(f"Wallet pin file is malformed: {pin_path}") from exc
|
|
63
|
+
if not isinstance(payload, dict):
|
|
64
|
+
raise WalletBackendError(f"Wallet pin file is malformed: {pin_path}")
|
|
65
|
+
if payload.get("kind") != WALLET_ADDRESS_PIN_KIND:
|
|
66
|
+
raise WalletBackendError(f"Wallet pin file kind is invalid: {pin_path}")
|
|
67
|
+
if int(payload.get("version") or 0) != WALLET_ADDRESS_PIN_VERSION:
|
|
68
|
+
raise WalletBackendError(f"Wallet pin file version is invalid: {pin_path}")
|
|
69
|
+
address = str(payload.get("address") or "").strip()
|
|
70
|
+
network = str(payload.get("network") or "").strip().lower()
|
|
71
|
+
if not address or not network:
|
|
72
|
+
raise WalletBackendError(f"Wallet pin file is incomplete: {pin_path}")
|
|
73
|
+
return {
|
|
74
|
+
"address": address,
|
|
75
|
+
"network": network,
|
|
76
|
+
"path": str(pin_path),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str]:
|
|
81
|
+
"""Persist the expected wallet address for later mismatch checks."""
|
|
82
|
+
payload = {
|
|
83
|
+
"kind": WALLET_ADDRESS_PIN_KIND,
|
|
84
|
+
"version": WALLET_ADDRESS_PIN_VERSION,
|
|
85
|
+
"address": address,
|
|
86
|
+
"network": network.strip().lower(),
|
|
87
|
+
"wallet_file": path.name,
|
|
88
|
+
}
|
|
89
|
+
pin_path = resolve_wallet_pin_path(path)
|
|
90
|
+
atomic_write_text(pin_path, json.dumps(payload, indent=2), mode=0o600)
|
|
91
|
+
return {
|
|
92
|
+
"address": address,
|
|
93
|
+
"network": payload["network"],
|
|
94
|
+
"path": str(pin_path),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ensure_wallet_pin(path: Path, *, address: str, network: str) -> dict[str, str]:
|
|
99
|
+
"""Ensure the wallet pin exists and matches the expected address."""
|
|
100
|
+
expected_network = network.strip().lower()
|
|
101
|
+
existing = load_wallet_pin(path)
|
|
102
|
+
if existing is None:
|
|
103
|
+
return write_wallet_pin(path, address=address, network=expected_network)
|
|
104
|
+
if existing["network"] != expected_network:
|
|
105
|
+
raise WalletBackendError(
|
|
106
|
+
f"Wallet pin network mismatch for {path}: expected {expected_network}, found {existing['network']}."
|
|
107
|
+
)
|
|
108
|
+
if existing["address"] != address:
|
|
109
|
+
raise WalletBackendError(
|
|
110
|
+
f"Wallet address mismatch for {path}: pinned {existing['address']}, derived {address}."
|
|
111
|
+
)
|
|
112
|
+
return existing
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def refuse_recreation_if_pinned(path: Path, *, network: str) -> None:
|
|
116
|
+
"""Refuse to recreate a wallet when a mainnet address is already pinned."""
|
|
117
|
+
expected_network = network.strip().lower()
|
|
118
|
+
if expected_network != "mainnet" or not refuse_mainnet_wallet_recreation():
|
|
119
|
+
return
|
|
120
|
+
existing = load_wallet_pin(path)
|
|
121
|
+
if existing is None:
|
|
122
|
+
return
|
|
123
|
+
raise WalletBackendError(
|
|
124
|
+
"Refusing to create a new mainnet wallet because a pinned wallet address already exists "
|
|
125
|
+
f"for {path}. Restore the original wallet file for {existing['address']} instead of creating a new one."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ensure_solana_wallet_ready() -> dict[str, str] | None:
|
|
130
|
+
"""Ensure that a Solana wallet exists when auto-create is enabled."""
|
|
131
|
+
from agent_wallet.config import default_solana_wallet_path, resolve_solana_private_key, settings
|
|
132
|
+
|
|
133
|
+
if settings.agent_wallet_backend.strip().lower() not in {"solana", "solana_local", "solana-local"}:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
if resolve_solana_private_key():
|
|
137
|
+
return {"address": "", "path": ""}
|
|
138
|
+
|
|
139
|
+
configured_path = settings.solana_agent_keypair_path.strip()
|
|
140
|
+
path = Path(configured_path).expanduser() if configured_path else default_solana_wallet_path(settings.solana_network)
|
|
141
|
+
|
|
142
|
+
if path.exists():
|
|
143
|
+
return {"address": "", "path": str(path)}
|
|
144
|
+
|
|
145
|
+
if not settings.solana_auto_create_wallet:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
refuse_recreation_if_pinned(path, network=settings.solana_network)
|
|
149
|
+
created = create_solana_wallet_file(path)
|
|
150
|
+
write_wallet_pin(path, address=created["address"], network=settings.solana_network)
|
|
151
|
+
return created
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def describe_bootstrap() -> dict[str, str | bool]:
|
|
155
|
+
"""Return the effective bootstrap configuration for installer/runtime usage."""
|
|
156
|
+
from agent_wallet.config import (
|
|
157
|
+
default_solana_wallet_path,
|
|
158
|
+
resolve_solana_rpc_url,
|
|
159
|
+
resolve_runtime_solana_rpc_urls,
|
|
160
|
+
settings,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
configured_path = settings.solana_agent_keypair_path.strip()
|
|
164
|
+
path = Path(configured_path).expanduser() if configured_path else default_solana_wallet_path(settings.solana_network)
|
|
165
|
+
rpc_urls = resolve_runtime_solana_rpc_urls(
|
|
166
|
+
settings.solana_network,
|
|
167
|
+
settings.solana_rpc_url,
|
|
168
|
+
settings.solana_rpc_urls,
|
|
169
|
+
)
|
|
170
|
+
return {
|
|
171
|
+
"backend": settings.agent_wallet_backend,
|
|
172
|
+
"network": settings.solana_network,
|
|
173
|
+
"rpc_url": rpc_urls[0] if rpc_urls else resolve_solana_rpc_url(settings.solana_network, ""),
|
|
174
|
+
"rpc_urls": rpc_urls,
|
|
175
|
+
"auto_create_wallet": settings.solana_auto_create_wallet,
|
|
176
|
+
"keypair_path": str(path),
|
|
177
|
+
"sign_only": settings.agent_wallet_sign_only,
|
|
178
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Host-side helpers for binding local BTC wallets to OpenClaw users."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agent_wallet.config import resolve_openclaw_home, settings
|
|
10
|
+
from agent_wallet.providers.wdk_btc_local import WdkBtcLocalClient
|
|
11
|
+
from agent_wallet.user_wallets import normalize_user_id
|
|
12
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize_btc_network(value: str | None) -> str:
|
|
16
|
+
network = str(value or "").strip().lower()
|
|
17
|
+
aliases = {"mainnet": "bitcoin"}
|
|
18
|
+
network = aliases.get(network, network)
|
|
19
|
+
if network not in {"bitcoin", "testnet", "regtest"}:
|
|
20
|
+
return "bitcoin"
|
|
21
|
+
return network
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_service_url(service_url: str | None = None) -> str:
|
|
25
|
+
effective = (service_url or settings.wdk_btc_service_url).strip()
|
|
26
|
+
if not effective:
|
|
27
|
+
raise WalletBackendError("wdk_btc_service_url is required for BTC wallet host operations.")
|
|
28
|
+
return effective
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_user_btc_wallet_path(user_id: str, network: str | None = None) -> Path:
|
|
32
|
+
effective_network = _normalize_btc_network(network or settings.solana_network)
|
|
33
|
+
user_dir = resolve_openclaw_home() / "users" / normalize_user_id(user_id) / "wallets"
|
|
34
|
+
return user_dir / f"btc-{effective_network}-agent.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _write_wallet_binding(path: Path, payload: dict[str, Any]) -> None:
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_user_btc_wallet_binding(user_id: str, network: str | None = None) -> dict[str, Any]:
|
|
43
|
+
path = resolve_user_btc_wallet_path(user_id, network=network)
|
|
44
|
+
if not path.exists():
|
|
45
|
+
raise WalletBackendError(f"BTC wallet binding does not exist yet: {path}")
|
|
46
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
47
|
+
if not isinstance(payload, dict) or not str(payload.get("wallet_id") or "").strip():
|
|
48
|
+
raise WalletBackendError(f"BTC wallet binding is invalid: {path}")
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_user_btc_wallet(
|
|
53
|
+
user_id: str,
|
|
54
|
+
*,
|
|
55
|
+
password: str,
|
|
56
|
+
label: str | None = None,
|
|
57
|
+
network: str | None = None,
|
|
58
|
+
service_url: str | None = None,
|
|
59
|
+
reveal_seed_phrase: bool = False,
|
|
60
|
+
account_index: int | None = None,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
effective_network = _normalize_btc_network(network or settings.solana_network)
|
|
63
|
+
effective_account_index = settings.wdk_btc_account_index if account_index is None else int(account_index)
|
|
64
|
+
client = WdkBtcLocalClient(_resolve_service_url(service_url))
|
|
65
|
+
created = client.post_sync(
|
|
66
|
+
"/v1/btc/wallets/create",
|
|
67
|
+
{
|
|
68
|
+
"label": (label or "").strip() or "Agent BTC Wallet",
|
|
69
|
+
"password": password,
|
|
70
|
+
"network": effective_network,
|
|
71
|
+
"revealSeedPhrase": bool(reveal_seed_phrase),
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
address = client.post_sync(
|
|
75
|
+
"/v1/btc/address/resolve",
|
|
76
|
+
{
|
|
77
|
+
"walletId": created["walletId"],
|
|
78
|
+
"accountIndex": effective_account_index,
|
|
79
|
+
"network": effective_network,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
binding = {
|
|
83
|
+
"user_id": user_id,
|
|
84
|
+
"wallet_id": str(created["walletId"]),
|
|
85
|
+
"label": str(created.get("label") or "Agent BTC Wallet"),
|
|
86
|
+
"network": effective_network,
|
|
87
|
+
"account_index": effective_account_index,
|
|
88
|
+
"address": str(address.get("address") or ""),
|
|
89
|
+
"storage_format": "local_vault",
|
|
90
|
+
"service_kind": "wdk-btc-wallet",
|
|
91
|
+
"created_at": created.get("createdAt"),
|
|
92
|
+
"updated_at": created.get("updatedAt"),
|
|
93
|
+
}
|
|
94
|
+
_write_wallet_binding(resolve_user_btc_wallet_path(user_id, effective_network), binding)
|
|
95
|
+
return {
|
|
96
|
+
**binding,
|
|
97
|
+
"unlocked": bool(created.get("unlocked", True)),
|
|
98
|
+
"unlock_expires_at": created.get("unlockExpiresAt"),
|
|
99
|
+
**({"seed_phrase": created["seedPhrase"]} if created.get("seedPhrase") else {}),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def import_user_btc_wallet(
|
|
104
|
+
user_id: str,
|
|
105
|
+
*,
|
|
106
|
+
password: str,
|
|
107
|
+
seed_phrase: str,
|
|
108
|
+
label: str | None = None,
|
|
109
|
+
network: str | None = None,
|
|
110
|
+
service_url: str | None = None,
|
|
111
|
+
account_index: int | None = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
effective_network = _normalize_btc_network(network or settings.solana_network)
|
|
114
|
+
effective_account_index = settings.wdk_btc_account_index if account_index is None else int(account_index)
|
|
115
|
+
client = WdkBtcLocalClient(_resolve_service_url(service_url))
|
|
116
|
+
created = client.post_sync(
|
|
117
|
+
"/v1/btc/wallets/import",
|
|
118
|
+
{
|
|
119
|
+
"label": (label or "").strip() or "Agent BTC Wallet",
|
|
120
|
+
"password": password,
|
|
121
|
+
"seedPhrase": seed_phrase,
|
|
122
|
+
"network": effective_network,
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
address = client.post_sync(
|
|
126
|
+
"/v1/btc/address/resolve",
|
|
127
|
+
{
|
|
128
|
+
"walletId": created["walletId"],
|
|
129
|
+
"accountIndex": effective_account_index,
|
|
130
|
+
"network": effective_network,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
binding = {
|
|
134
|
+
"user_id": user_id,
|
|
135
|
+
"wallet_id": str(created["walletId"]),
|
|
136
|
+
"label": str(created.get("label") or "Agent BTC Wallet"),
|
|
137
|
+
"network": effective_network,
|
|
138
|
+
"account_index": effective_account_index,
|
|
139
|
+
"address": str(address.get("address") or ""),
|
|
140
|
+
"storage_format": "local_vault",
|
|
141
|
+
"service_kind": "wdk-btc-wallet",
|
|
142
|
+
"created_at": created.get("createdAt"),
|
|
143
|
+
"updated_at": created.get("updatedAt"),
|
|
144
|
+
}
|
|
145
|
+
_write_wallet_binding(resolve_user_btc_wallet_path(user_id, effective_network), binding)
|
|
146
|
+
return {
|
|
147
|
+
**binding,
|
|
148
|
+
"unlocked": bool(created.get("unlocked", True)),
|
|
149
|
+
"unlock_expires_at": created.get("unlockExpiresAt"),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def unlock_user_btc_wallet(
|
|
154
|
+
user_id: str,
|
|
155
|
+
*,
|
|
156
|
+
password: str,
|
|
157
|
+
network: str | None = None,
|
|
158
|
+
service_url: str | None = None,
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
binding = get_user_btc_wallet_binding(user_id, network=network)
|
|
161
|
+
client = WdkBtcLocalClient(_resolve_service_url(service_url))
|
|
162
|
+
payload = client.post_sync(
|
|
163
|
+
"/v1/btc/wallets/unlock",
|
|
164
|
+
{
|
|
165
|
+
"walletId": binding["wallet_id"],
|
|
166
|
+
"password": password,
|
|
167
|
+
"timeoutSeconds": 0,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
return {
|
|
171
|
+
**binding,
|
|
172
|
+
"unlocked": bool(payload.get("unlocked", True)),
|
|
173
|
+
"unlock_expires_at": payload.get("unlockExpiresAt"),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def reveal_user_btc_wallet_seed_phrase(
|
|
178
|
+
user_id: str,
|
|
179
|
+
*,
|
|
180
|
+
password: str,
|
|
181
|
+
network: str | None = None,
|
|
182
|
+
service_url: str | None = None,
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
binding = get_user_btc_wallet_binding(user_id, network=network)
|
|
185
|
+
client = WdkBtcLocalClient(_resolve_service_url(service_url))
|
|
186
|
+
payload = client.post_sync(
|
|
187
|
+
"/v1/btc/wallets/reveal-seed",
|
|
188
|
+
{
|
|
189
|
+
"walletId": binding["wallet_id"],
|
|
190
|
+
"password": password,
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
return {
|
|
194
|
+
**binding,
|
|
195
|
+
"seed_phrase": str(payload.get("seedPhrase") or ""),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def lock_user_btc_wallet(
|
|
200
|
+
user_id: str,
|
|
201
|
+
*,
|
|
202
|
+
network: str | None = None,
|
|
203
|
+
service_url: str | None = None,
|
|
204
|
+
) -> dict[str, Any]:
|
|
205
|
+
binding = get_user_btc_wallet_binding(user_id, network=network)
|
|
206
|
+
client = WdkBtcLocalClient(_resolve_service_url(service_url))
|
|
207
|
+
payload = client.post_sync(
|
|
208
|
+
"/v1/btc/wallets/lock",
|
|
209
|
+
{
|
|
210
|
+
"walletId": binding["wallet_id"],
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
return {
|
|
214
|
+
**binding,
|
|
215
|
+
"unlocked": bool(payload.get("unlocked", False)),
|
|
216
|
+
"unlock_expires_at": None,
|
|
217
|
+
}
|