@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.
Files changed (96) hide show
  1. package/.openclaw/AGENTS.md +98 -0
  2. package/.openclaw/extensions/agent-wallet/README.md +127 -0
  3. package/.openclaw/extensions/agent-wallet/index.ts +1520 -0
  4. package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +184 -0
  5. package/.openclaw/extensions/agent-wallet/package.json +11 -0
  6. package/.openclaw/extensions/agent-wallet/skills/wallet-operator/SKILL.md +20 -0
  7. package/CHANGELOG.md +42 -0
  8. package/LICENSE +104 -0
  9. package/README.md +332 -0
  10. package/RELEASING.md +204 -0
  11. package/agent-wallet/.env.example +62 -0
  12. package/agent-wallet/AGENTS.md +129 -0
  13. package/agent-wallet/README.md +527 -0
  14. package/agent-wallet/agent_wallet/__init__.py +11 -0
  15. package/agent-wallet/agent_wallet/approval.py +161 -0
  16. package/agent-wallet/agent_wallet/bootstrap.py +178 -0
  17. package/agent-wallet/agent_wallet/btc_user_wallets.py +217 -0
  18. package/agent-wallet/agent_wallet/config.py +382 -0
  19. package/agent-wallet/agent_wallet/encrypted_storage.py +161 -0
  20. package/agent-wallet/agent_wallet/evm_user_wallets.py +370 -0
  21. package/agent-wallet/agent_wallet/exceptions.py +9 -0
  22. package/agent-wallet/agent_wallet/file_ops.py +34 -0
  23. package/agent-wallet/agent_wallet/http_client.py +25 -0
  24. package/agent-wallet/agent_wallet/models.py +66 -0
  25. package/agent-wallet/agent_wallet/nonce_registry.py +59 -0
  26. package/agent-wallet/agent_wallet/openclaw_adapter.py +5128 -0
  27. package/agent-wallet/agent_wallet/openclaw_cli.py +626 -0
  28. package/agent-wallet/agent_wallet/openclaw_runtime.py +272 -0
  29. package/agent-wallet/agent_wallet/plugin_bundle.py +42 -0
  30. package/agent-wallet/agent_wallet/providers/__init__.py +1 -0
  31. package/agent-wallet/agent_wallet/providers/bags.py +259 -0
  32. package/agent-wallet/agent_wallet/providers/evm_portfolio.py +470 -0
  33. package/agent-wallet/agent_wallet/providers/jupiter.py +567 -0
  34. package/agent-wallet/agent_wallet/providers/kamino.py +215 -0
  35. package/agent-wallet/agent_wallet/providers/lifi.py +277 -0
  36. package/agent-wallet/agent_wallet/providers/solana_rpc.py +470 -0
  37. package/agent-wallet/agent_wallet/providers/wdk_btc_local.py +114 -0
  38. package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +205 -0
  39. package/agent-wallet/agent_wallet/sealed_keys.py +61 -0
  40. package/agent-wallet/agent_wallet/solana_stake.py +103 -0
  41. package/agent-wallet/agent_wallet/solana_tx.py +93 -0
  42. package/agent-wallet/agent_wallet/spending_limits.py +101 -0
  43. package/agent-wallet/agent_wallet/transaction_policy.py +518 -0
  44. package/agent-wallet/agent_wallet/user_wallets.py +355 -0
  45. package/agent-wallet/agent_wallet/validation.py +31 -0
  46. package/agent-wallet/agent_wallet/wallet_layer/__init__.py +1 -0
  47. package/agent-wallet/agent_wallet/wallet_layer/base.py +808 -0
  48. package/agent-wallet/agent_wallet/wallet_layer/base58.py +44 -0
  49. package/agent-wallet/agent_wallet/wallet_layer/factory.py +102 -0
  50. package/agent-wallet/agent_wallet/wallet_layer/solana.py +4252 -0
  51. package/agent-wallet/agent_wallet/wallet_layer/wdk_btc.py +272 -0
  52. package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +1628 -0
  53. package/agent-wallet/examples/bootstrap_wallet.py +21 -0
  54. package/agent-wallet/examples/openclaw_runtime_onboarding.py +28 -0
  55. package/agent-wallet/examples/openclaw_user_wallet_example.py +31 -0
  56. package/agent-wallet/examples/openclaw_wallet_adapter_example.py +33 -0
  57. package/agent-wallet/openclaw.plugin.json +138 -0
  58. package/agent-wallet/pyproject.toml +31 -0
  59. package/agent-wallet/scripts/bootstrap_openclaw_btc.py +278 -0
  60. package/agent-wallet/scripts/build_release_bundle.py +188 -0
  61. package/agent-wallet/scripts/finalize_openclaw_local_wallet_config.py +121 -0
  62. package/agent-wallet/scripts/install_agent_wallet.py +505 -0
  63. package/agent-wallet/scripts/install_openclaw_local_config.py +226 -0
  64. package/agent-wallet/scripts/install_openclaw_sealed_keys.py +105 -0
  65. package/agent-wallet/scripts/manage_openclaw_btc_wallet.py +244 -0
  66. package/agent-wallet/scripts/reveal_btc_seed.sh +130 -0
  67. package/agent-wallet/scripts/security_utils.py +37 -0
  68. package/agent-wallet/scripts/setup_btc_wallet.sh +146 -0
  69. package/agent-wallet/scripts/switch_openclaw_wallet_network.py +106 -0
  70. package/agent-wallet/skills/wallet-operator/SKILL.md +128 -0
  71. package/bin/openclaw-agent-wallet.mjs +487 -0
  72. package/install-from-github.sh +134 -0
  73. package/package.json +61 -0
  74. package/setup.sh +40 -0
  75. package/wdk-btc-wallet/README.md +325 -0
  76. package/wdk-btc-wallet/bootstrap.sh +22 -0
  77. package/wdk-btc-wallet/package-lock.json +1839 -0
  78. package/wdk-btc-wallet/package.json +18 -0
  79. package/wdk-btc-wallet/run-local.sh +21 -0
  80. package/wdk-btc-wallet/src/config.js +160 -0
  81. package/wdk-btc-wallet/src/json.js +35 -0
  82. package/wdk-btc-wallet/src/local_vault.js +432 -0
  83. package/wdk-btc-wallet/src/network_state.js +84 -0
  84. package/wdk-btc-wallet/src/server.js +257 -0
  85. package/wdk-btc-wallet/src/wdk_btc_wallet.js +332 -0
  86. package/wdk-evm-wallet/README.md +183 -0
  87. package/wdk-evm-wallet/bootstrap.sh +8 -0
  88. package/wdk-evm-wallet/package-lock.json +2340 -0
  89. package/wdk-evm-wallet/package.json +23 -0
  90. package/wdk-evm-wallet/run-local.sh +12 -0
  91. package/wdk-evm-wallet/src/config.js +274 -0
  92. package/wdk-evm-wallet/src/json.js +35 -0
  93. package/wdk-evm-wallet/src/local_vault.js +430 -0
  94. package/wdk-evm-wallet/src/network_state.js +92 -0
  95. package/wdk-evm-wallet/src/server.js +575 -0
  96. 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
+ }