@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,226 @@
1
+ """Patch an OpenClaw config file for the agent-wallet plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
13
+
14
+ from agent_wallet.file_ops import atomic_write_text, chmod_if_exists
15
+ from agent_wallet.sealed_keys import resolve_sealed_keys_path, seal_keys, unseal_keys
16
+ from security_utils import write_redacted_backup
17
+
18
+ OPTIONAL_TOOLS = [
19
+ "sign_wallet_message",
20
+ "transfer_sol",
21
+ "transfer_btc",
22
+ "transfer_spl_token",
23
+ "swap_solana_tokens",
24
+ "close_empty_token_accounts",
25
+ "request_devnet_airdrop",
26
+ ]
27
+
28
+
29
+ def _default_config_path() -> Path:
30
+ return Path(os.path.expanduser("~/.openclaw/openclaw.json"))
31
+
32
+
33
+ def _repo_root() -> Path:
34
+ return Path(__file__).resolve().parents[2]
35
+
36
+
37
+ def _default_extension_path() -> Path:
38
+ return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
39
+
40
+
41
+ def _default_package_root() -> Path:
42
+ return Path(__file__).resolve().parents[1]
43
+
44
+
45
+ def _default_python_bin() -> str:
46
+ return os.getenv("OPENCLAW_AGENT_WALLET_PYTHON", sys.executable)
47
+
48
+
49
+ def _default_user_id() -> str:
50
+ return f"{os.getenv('USER', 'openclaw-user')}-local"
51
+
52
+
53
+ def _normalize_network(backend: str, network: str) -> str:
54
+ backend_name = backend.strip().lower()
55
+ normalized = network.strip().lower()
56
+ if backend_name in {"wdk_btc_local", "wdk-btc-local", "btc_local", "btc-local"}:
57
+ if normalized == "mainnet":
58
+ return "bitcoin"
59
+ return normalized or "bitcoin"
60
+ return normalized or "devnet"
61
+
62
+
63
+ def build_parser() -> argparse.ArgumentParser:
64
+ parser = argparse.ArgumentParser(description=__doc__)
65
+ parser.add_argument("--config-path", default=str(_default_config_path()))
66
+ parser.add_argument("--plugin-id", default="agent-wallet")
67
+ parser.add_argument("--user-id", default=_default_user_id())
68
+ parser.add_argument("--backend", default="solana_local")
69
+ parser.add_argument("--network", default="devnet")
70
+ parser.add_argument("--rpc-url", default="")
71
+ parser.add_argument("--rpc-urls", default="")
72
+ parser.add_argument("--wdk-btc-service-url", default="")
73
+ parser.add_argument("--wdk-btc-wallet-id", default="")
74
+ parser.add_argument("--wdk-btc-account-index", type=int, default=0)
75
+ parser.add_argument("--sign-only", action=argparse.BooleanOptionalAction, default=False)
76
+ parser.add_argument("--encrypt-user-wallets", action=argparse.BooleanOptionalAction, default=True)
77
+ parser.add_argument(
78
+ "--migrate-plaintext-user-wallets",
79
+ action=argparse.BooleanOptionalAction,
80
+ default=True,
81
+ )
82
+ parser.add_argument("--extension-path", default=str(_default_extension_path()))
83
+ parser.add_argument("--package-root", default=str(_default_package_root()))
84
+ parser.add_argument("--python-bin", default=_default_python_bin())
85
+ parser.add_argument("--write-master-key", action=argparse.BooleanOptionalAction, default=False)
86
+ return parser
87
+
88
+
89
+ def _collect_sealed_secret_updates() -> dict[str, str]:
90
+ updates: dict[str, str] = {}
91
+ master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
92
+ approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
93
+ private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
94
+ if master_key:
95
+ updates["master_key"] = master_key
96
+ if approval_secret:
97
+ updates["approval_secret"] = approval_secret
98
+ if private_key:
99
+ updates["private_key"] = private_key
100
+ return updates
101
+
102
+
103
+ def _maybe_install_sealed_keys() -> str | None:
104
+ boot_key = os.getenv("AGENT_WALLET_BOOT_KEY", "").strip()
105
+ if not boot_key:
106
+ return None
107
+ updates = _collect_sealed_secret_updates()
108
+ if not updates:
109
+ return None
110
+ sealed_path = resolve_sealed_keys_path()
111
+ existing = unseal_keys(boot_key) if sealed_path.exists() else {}
112
+ return str(seal_keys(boot_key, {**existing, **updates}))
113
+
114
+
115
+ def _require_hardened_runtime_secrets(backend: str) -> str | None:
116
+ if backend.strip().lower() in {"", "none"}:
117
+ return None
118
+
119
+ boot_key = os.getenv("AGENT_WALLET_BOOT_KEY", "").strip()
120
+ if not boot_key:
121
+ raise SystemExit(
122
+ "AGENT_WALLET_BOOT_KEY is required. Runtime secrets must be loaded from sealed_keys.json."
123
+ )
124
+
125
+ sealed_path = resolve_sealed_keys_path()
126
+ if not sealed_path.exists():
127
+ raise SystemExit(
128
+ "sealed_keys.json was not created. Provide AGENT_WALLET_MASTER_KEY and "
129
+ "AGENT_WALLET_APPROVAL_SECRET during install so the installer can seal them."
130
+ )
131
+
132
+ secrets = unseal_keys(boot_key)
133
+ missing = [name for name in ("master_key", "approval_secret") if not str(secrets.get(name) or "").strip()]
134
+ if missing:
135
+ raise SystemExit(
136
+ "sealed_keys.json is missing required runtime secrets: "
137
+ + ", ".join(missing)
138
+ + "."
139
+ )
140
+ return str(sealed_path)
141
+
142
+
143
+ def main() -> None:
144
+ args = build_parser().parse_args()
145
+ config_path = Path(args.config_path).expanduser()
146
+ data = json.loads(config_path.read_text(encoding="utf-8"))
147
+
148
+ backup_path = config_path.with_name(
149
+ f"{config_path.name}.bak.agent-wallet.{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}"
150
+ )
151
+ write_redacted_backup(backup_path, data)
152
+
153
+ plugins = data.setdefault("plugins", {})
154
+ plugins["enabled"] = True
155
+
156
+ load = plugins.setdefault("load", {})
157
+ paths = load.setdefault("paths", [])
158
+ extension_path_text = str(Path(args.extension_path).expanduser().resolve())
159
+ if extension_path_text not in paths:
160
+ paths.append(extension_path_text)
161
+
162
+ entries = plugins.setdefault("entries", {})
163
+ effective_network = _normalize_network(args.backend, args.network)
164
+ plugin_config = {
165
+ "userId": args.user_id,
166
+ "backend": args.backend,
167
+ "network": effective_network,
168
+ "signOnly": args.sign_only,
169
+ "encryptUserWallets": args.encrypt_user_wallets,
170
+ "migratePlaintextUserWallets": args.migrate_plaintext_user_wallets,
171
+ "packageRoot": str(Path(args.package_root).expanduser().resolve()),
172
+ "pythonBin": args.python_bin,
173
+ }
174
+ if args.rpc_url.strip():
175
+ plugin_config["rpcUrl"] = args.rpc_url.strip()
176
+ if args.rpc_urls.strip():
177
+ plugin_config["rpcUrls"] = [
178
+ item.strip() for item in args.rpc_urls.split(",") if item.strip()
179
+ ]
180
+ if args.wdk_btc_service_url.strip():
181
+ plugin_config["wdkBtcServiceUrl"] = args.wdk_btc_service_url.strip()
182
+ if args.wdk_btc_wallet_id.strip():
183
+ plugin_config["wdkBtcWalletId"] = args.wdk_btc_wallet_id.strip()
184
+ if args.wdk_btc_account_index is not None:
185
+ plugin_config["wdkBtcAccountIndex"] = int(args.wdk_btc_account_index)
186
+ if args.write_master_key:
187
+ raise SystemExit(
188
+ "Refusing to write masterKey into config. Runtime secrets must live in sealed_keys.json."
189
+ )
190
+
191
+ entries[args.plugin_id] = {
192
+ "enabled": True,
193
+ "config": plugin_config,
194
+ }
195
+
196
+ tools = data.setdefault("tools", {})
197
+ also_allow = tools.setdefault("alsoAllow", [])
198
+ for tool_name in OPTIONAL_TOOLS:
199
+ if tool_name not in also_allow:
200
+ also_allow.append(tool_name)
201
+
202
+ atomic_write_text(config_path, json.dumps(data, indent=2) + "\n", mode=0o600)
203
+ chmod_if_exists(config_path, 0o600)
204
+ _maybe_install_sealed_keys()
205
+ sealed_keys_path = _require_hardened_runtime_secrets(args.backend)
206
+
207
+ print(
208
+ json.dumps(
209
+ {
210
+ "ok": True,
211
+ "config_path": str(config_path),
212
+ "backup_path": str(backup_path),
213
+ "extension_path": extension_path_text,
214
+ "python_bin": args.python_bin,
215
+ "package_root": plugin_config["packageRoot"],
216
+ "plugin_id": args.plugin_id,
217
+ "user_id": args.user_id,
218
+ "sealed_keys_path": sealed_keys_path,
219
+ },
220
+ indent=2,
221
+ )
222
+ )
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -0,0 +1,105 @@
1
+ """Create or update the encrypted sealed_keys.json secret bundle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
+
13
+ from agent_wallet.sealed_keys import resolve_sealed_keys_path, seal_keys, unseal_keys
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(description=__doc__)
18
+ parser.add_argument(
19
+ "--replace",
20
+ action=argparse.BooleanOptionalAction,
21
+ default=False,
22
+ help="Replace the sealed bundle instead of merging with existing entries.",
23
+ )
24
+ parser.add_argument(
25
+ "--boot-key",
26
+ default="",
27
+ help="Deprecated insecure path. Use AGENT_WALLET_BOOT_KEY env instead.",
28
+ )
29
+ parser.add_argument(
30
+ "--master-key",
31
+ default="",
32
+ help="Deprecated insecure path. Use AGENT_WALLET_MASTER_KEY env instead.",
33
+ )
34
+ parser.add_argument(
35
+ "--approval-secret",
36
+ default="",
37
+ help="Deprecated insecure path. Use AGENT_WALLET_APPROVAL_SECRET env instead.",
38
+ )
39
+ parser.add_argument(
40
+ "--private-key",
41
+ default="",
42
+ help="Deprecated insecure path. Use SOLANA_AGENT_PRIVATE_KEY env instead.",
43
+ )
44
+ return parser
45
+
46
+
47
+ def _collect_secret_updates() -> dict[str, str]:
48
+ updates: dict[str, str] = {}
49
+ master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
50
+ approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
51
+ private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
52
+ if master_key:
53
+ updates["master_key"] = master_key
54
+ if approval_secret:
55
+ updates["approval_secret"] = approval_secret
56
+ if private_key:
57
+ updates["private_key"] = private_key
58
+ return updates
59
+
60
+
61
+ def main() -> None:
62
+ args = build_parser().parse_args()
63
+ if (
64
+ args.boot_key.strip()
65
+ or args.master_key.strip()
66
+ or args.approval_secret.strip()
67
+ or args.private_key.strip()
68
+ ):
69
+ raise SystemExit(
70
+ "Passing secrets via command-line arguments is insecure. "
71
+ "Use AGENT_WALLET_BOOT_KEY / AGENT_WALLET_MASTER_KEY / "
72
+ "AGENT_WALLET_APPROVAL_SECRET / SOLANA_AGENT_PRIVATE_KEY environment variables instead."
73
+ )
74
+
75
+ boot_key = os.getenv("AGENT_WALLET_BOOT_KEY", "").strip()
76
+ if not boot_key:
77
+ raise SystemExit("AGENT_WALLET_BOOT_KEY is required.")
78
+
79
+ updates = _collect_secret_updates()
80
+ sealed_path = resolve_sealed_keys_path()
81
+ existing = unseal_keys(boot_key) if sealed_path.exists() and not args.replace else {}
82
+ secrets = {**existing, **updates}
83
+ if not secrets:
84
+ raise SystemExit(
85
+ "No secrets provided. Set AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET, "
86
+ "and/or SOLANA_AGENT_PRIVATE_KEY in the environment."
87
+ )
88
+
89
+ path = seal_keys(boot_key, secrets)
90
+ print(
91
+ json.dumps(
92
+ {
93
+ "ok": True,
94
+ "path": str(path),
95
+ "stored_keys": sorted(secrets.keys()),
96
+ "updated_keys": sorted(updates.keys()),
97
+ "replaced": bool(args.replace),
98
+ },
99
+ indent=2,
100
+ )
101
+ )
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env python3
2
+ """Host-side helper for managing a local OpenClaw BTC wallet binding."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from getpass import getpass
10
+ from pathlib import Path
11
+
12
+ PACKAGE_ROOT = Path(__file__).resolve().parents[1]
13
+ if str(PACKAGE_ROOT) not in sys.path:
14
+ sys.path.insert(0, str(PACKAGE_ROOT))
15
+
16
+ from agent_wallet.btc_user_wallets import ( # noqa: E402
17
+ create_user_btc_wallet,
18
+ get_user_btc_wallet_binding,
19
+ import_user_btc_wallet,
20
+ lock_user_btc_wallet,
21
+ reveal_user_btc_wallet_seed_phrase,
22
+ unlock_user_btc_wallet,
23
+ )
24
+
25
+
26
+ def _normalize_network(value: str) -> str:
27
+ network = str(value or "").strip().lower()
28
+ if network == "mainnet":
29
+ return "bitcoin"
30
+ return network or "bitcoin"
31
+
32
+
33
+ def _read_secret(
34
+ *,
35
+ prompt: str,
36
+ confirm_prompt: str | None = None,
37
+ stdin_mode: bool = False,
38
+ ) -> str:
39
+ if stdin_mode:
40
+ value = sys.stdin.read().strip()
41
+ if not value:
42
+ raise SystemExit(f"{prompt.rstrip(':')} is required on stdin.")
43
+ return value
44
+ value = getpass(prompt)
45
+ if confirm_prompt is not None:
46
+ confirmed = getpass(confirm_prompt)
47
+ if value != confirmed:
48
+ raise SystemExit("Secrets did not match.")
49
+ if not value.strip():
50
+ raise SystemExit(f"{prompt.rstrip(':')} is required.")
51
+ return value.strip()
52
+
53
+
54
+ def _read_password_and_seed_from_stdin() -> tuple[str, str]:
55
+ raw = sys.stdin.read().strip()
56
+ if not raw:
57
+ raise SystemExit("Password and seed phrase payload is required on stdin.")
58
+ lines = raw.splitlines()
59
+ if len(lines) < 2:
60
+ raise SystemExit(
61
+ "For import via stdin, provide password on the first line and the seed phrase on the remaining lines."
62
+ )
63
+ password = lines[0].strip()
64
+ seed_phrase = " ".join(line.strip() for line in lines[1:] if line.strip())
65
+ if not password or not seed_phrase:
66
+ raise SystemExit("Both password and seed phrase are required.")
67
+ return password, seed_phrase
68
+
69
+
70
+ def main() -> int:
71
+ parser = argparse.ArgumentParser(description="Manage a local OpenClaw BTC wallet binding")
72
+ subparsers = parser.add_subparsers(dest="command", required=True)
73
+
74
+ common_parent = argparse.ArgumentParser(add_help=False)
75
+ common_parent.add_argument("--user-id", required=True)
76
+ common_parent.add_argument("--network", default="bitcoin")
77
+ common_parent.add_argument("--service-url")
78
+
79
+ get_parser = subparsers.add_parser("get", parents=[common_parent])
80
+
81
+ setup_parser = subparsers.add_parser("setup", parents=[common_parent])
82
+ setup_parser.add_argument("--label")
83
+ setup_parser.add_argument("--account-index", type=int)
84
+ setup_parser.add_argument("--reveal-seed", action="store_true")
85
+ setup_parser.add_argument("--password-stdin", action="store_true")
86
+
87
+ create_parser = subparsers.add_parser("create", parents=[common_parent])
88
+ create_parser.add_argument("--label")
89
+ create_parser.add_argument("--account-index", type=int)
90
+ create_parser.add_argument("--reveal-seed", action="store_true")
91
+ create_parser.add_argument("--password-stdin", action="store_true")
92
+
93
+ import_parser = subparsers.add_parser("import", parents=[common_parent])
94
+ import_parser.add_argument("--label")
95
+ import_parser.add_argument("--account-index", type=int)
96
+ import_parser.add_argument("--password-stdin", action="store_true")
97
+ import_parser.add_argument("--seed-stdin", action="store_true")
98
+
99
+ unlock_parser = subparsers.add_parser("unlock", parents=[common_parent])
100
+ unlock_parser.add_argument("--password-stdin", action="store_true")
101
+
102
+ reveal_parser = subparsers.add_parser("reveal-seed", parents=[common_parent])
103
+ reveal_parser.add_argument("--password-stdin", action="store_true")
104
+
105
+ lock_parser = subparsers.add_parser("lock", parents=[common_parent])
106
+
107
+ args = parser.parse_args()
108
+ effective_network = _normalize_network(args.network)
109
+
110
+ def _config_hint(wallet: dict[str, object]) -> dict[str, object]:
111
+ return {
112
+ "backend": "wdk_btc_local",
113
+ "network": effective_network,
114
+ "wdkBtcServiceUrl": args.service_url,
115
+ "wdkBtcWalletId": wallet.get("wallet_id"),
116
+ "wdkBtcAccountIndex": wallet.get("account_index"),
117
+ }
118
+
119
+ if args.command == "get":
120
+ payload = {"ok": True, "wallet": get_user_btc_wallet_binding(args.user_id, network=effective_network)}
121
+ elif args.command == "setup":
122
+ password = _read_secret(
123
+ prompt="BTC wallet password: ",
124
+ confirm_prompt=None,
125
+ stdin_mode=bool(args.password_stdin),
126
+ )
127
+ try:
128
+ existing = get_user_btc_wallet_binding(args.user_id, network=effective_network)
129
+ except Exception:
130
+ existing = None
131
+
132
+ if existing is None:
133
+ wallet = create_user_btc_wallet(
134
+ args.user_id,
135
+ password=password,
136
+ label=args.label,
137
+ network=effective_network,
138
+ service_url=args.service_url,
139
+ reveal_seed_phrase=bool(args.reveal_seed),
140
+ account_index=args.account_index,
141
+ )
142
+ payload = {
143
+ "ok": True,
144
+ "action": "created",
145
+ "wallet": wallet,
146
+ "openclaw_config_hint": _config_hint(wallet),
147
+ }
148
+ else:
149
+ wallet = unlock_user_btc_wallet(
150
+ args.user_id,
151
+ password=password,
152
+ network=effective_network,
153
+ service_url=args.service_url,
154
+ )
155
+ payload = {
156
+ "ok": True,
157
+ "action": "unlocked",
158
+ "wallet": wallet,
159
+ "openclaw_config_hint": _config_hint(wallet),
160
+ }
161
+ elif args.command == "create":
162
+ payload = {
163
+ "ok": True,
164
+ "wallet": create_user_btc_wallet(
165
+ args.user_id,
166
+ password=_read_secret(
167
+ prompt="BTC wallet password: ",
168
+ confirm_prompt="Confirm BTC wallet password: ",
169
+ stdin_mode=bool(args.password_stdin),
170
+ ),
171
+ label=args.label,
172
+ network=effective_network,
173
+ service_url=args.service_url,
174
+ reveal_seed_phrase=bool(args.reveal_seed),
175
+ account_index=args.account_index,
176
+ ),
177
+ }
178
+ elif args.command == "import":
179
+ if args.password_stdin and args.seed_stdin:
180
+ password, seed_phrase = _read_password_and_seed_from_stdin()
181
+ else:
182
+ password = _read_secret(
183
+ prompt="BTC wallet password: ",
184
+ confirm_prompt="Confirm BTC wallet password: ",
185
+ stdin_mode=bool(args.password_stdin),
186
+ )
187
+ seed_phrase = _read_secret(
188
+ prompt="BTC seed phrase: ",
189
+ stdin_mode=bool(args.seed_stdin),
190
+ )
191
+ payload = {
192
+ "ok": True,
193
+ "wallet": import_user_btc_wallet(
194
+ args.user_id,
195
+ password=password,
196
+ seed_phrase=seed_phrase,
197
+ label=args.label,
198
+ network=effective_network,
199
+ service_url=args.service_url,
200
+ account_index=args.account_index,
201
+ ),
202
+ }
203
+ elif args.command == "unlock":
204
+ payload = {
205
+ "ok": True,
206
+ "wallet": unlock_user_btc_wallet(
207
+ args.user_id,
208
+ password=_read_secret(
209
+ prompt="BTC wallet password: ",
210
+ stdin_mode=bool(args.password_stdin),
211
+ ),
212
+ network=effective_network,
213
+ service_url=args.service_url,
214
+ ),
215
+ }
216
+ elif args.command == "reveal-seed":
217
+ payload = {
218
+ "ok": True,
219
+ "wallet": reveal_user_btc_wallet_seed_phrase(
220
+ args.user_id,
221
+ password=_read_secret(
222
+ prompt="BTC wallet password: ",
223
+ stdin_mode=bool(args.password_stdin),
224
+ ),
225
+ network=effective_network,
226
+ service_url=args.service_url,
227
+ ),
228
+ }
229
+ else:
230
+ payload = {
231
+ "ok": True,
232
+ "wallet": lock_user_btc_wallet(
233
+ args.user_id,
234
+ network=effective_network,
235
+ service_url=args.service_url,
236
+ ),
237
+ }
238
+
239
+ print(json.dumps(payload))
240
+ return 0
241
+
242
+
243
+ if __name__ == "__main__":
244
+ raise SystemExit(main())
@@ -0,0 +1,130 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5
+ PACKAGE_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
6
+
7
+ resolve_python_bin() {
8
+ if [ -n "${OPENCLAW_AGENT_WALLET_PYTHON:-}" ] && [ -x "${OPENCLAW_AGENT_WALLET_PYTHON}" ]; then
9
+ printf "%s" "${OPENCLAW_AGENT_WALLET_PYTHON}"
10
+ return 0
11
+ fi
12
+
13
+ if [ -x "/tmp/agent-wallet-venv/bin/python" ]; then
14
+ printf "%s" "/tmp/agent-wallet-venv/bin/python"
15
+ return 0
16
+ fi
17
+
18
+ if [ -x "$PACKAGE_ROOT/.venv/bin/python" ]; then
19
+ printf "%s" "$PACKAGE_ROOT/.venv/bin/python"
20
+ return 0
21
+ fi
22
+
23
+ if command -v python3 >/dev/null 2>&1; then
24
+ command -v python3
25
+ return 0
26
+ fi
27
+
28
+ command -v python
29
+ }
30
+
31
+ PYTHON_BIN=$(resolve_python_bin)
32
+ export OPENCLAW_AGENT_WALLET_PYTHON="$PYTHON_BIN"
33
+
34
+ has_flag() {
35
+ flag=$1
36
+ shift
37
+ for arg in "$@"; do
38
+ case "$arg" in
39
+ "$flag"|"$flag"=*)
40
+ return 0
41
+ ;;
42
+ esac
43
+ done
44
+ return 1
45
+ }
46
+
47
+ prompt_with_default() {
48
+ label=$1
49
+ default_value=$2
50
+ if [ -t 0 ]; then
51
+ printf "%s [%s]: " "$label" "$default_value" >&2
52
+ read -r value
53
+ if [ -z "${value:-}" ]; then
54
+ printf "%s" "$default_value"
55
+ else
56
+ printf "%s" "$value"
57
+ fi
58
+ return 0
59
+ fi
60
+ printf "%s" "$default_value"
61
+ }
62
+
63
+ normalize_network_value() {
64
+ case $(printf "%s" "$1" | tr '[:upper:]' '[:lower:]') in
65
+ 1|mainnet|bitcoin)
66
+ printf "mainnet"
67
+ ;;
68
+ 2|testnet)
69
+ printf "testnet"
70
+ ;;
71
+ 3|regtest)
72
+ printf "regtest"
73
+ ;;
74
+ *)
75
+ return 1
76
+ ;;
77
+ esac
78
+ }
79
+
80
+ prompt_network_choice() {
81
+ default_value=$1
82
+ if ! [ -t 0 ]; then
83
+ printf "%s" "$default_value"
84
+ return 0
85
+ fi
86
+
87
+ case "$default_value" in
88
+ mainnet|bitcoin) default_hint="1" ;;
89
+ testnet) default_hint="2" ;;
90
+ regtest) default_hint="3" ;;
91
+ *) default_hint="1" ;;
92
+ esac
93
+
94
+ while true; do
95
+ printf "BTC network:\n" >&2
96
+ printf " 1) mainnet\n" >&2
97
+ printf " 2) testnet\n" >&2
98
+ printf " 3) regtest\n" >&2
99
+ printf "Choose network [%s]: " "$default_hint" >&2
100
+ read -r choice
101
+ if [ -z "${choice:-}" ]; then
102
+ choice=$default_hint
103
+ fi
104
+ if network=$(normalize_network_value "$choice"); then
105
+ printf "%s" "$network"
106
+ return 0
107
+ fi
108
+ printf "Invalid choice. Enter 1, 2, 3, mainnet, testnet, or regtest.\n" >&2
109
+ done
110
+ }
111
+
112
+ DEFAULT_USER_ID=${OPENCLAW_BTC_USER_ID:-${USER:-openclaw-user}-local}
113
+ DEFAULT_NETWORK=${OPENCLAW_BTC_NETWORK:-mainnet}
114
+ DEFAULT_SERVICE_URL=${OPENCLAW_BTC_SERVICE_URL:-http://127.0.0.1:8080}
115
+
116
+ if ! has_flag --user-id "$@"; then
117
+ USER_ID=$(prompt_with_default "OpenClaw user id" "$DEFAULT_USER_ID")
118
+ set -- "$@" --user-id "$USER_ID"
119
+ fi
120
+
121
+ if ! has_flag --network "$@"; then
122
+ NETWORK=$(prompt_network_choice "$DEFAULT_NETWORK")
123
+ set -- "$@" --network "$NETWORK"
124
+ fi
125
+
126
+ if ! has_flag --service-url "$@"; then
127
+ set -- "$@" --service-url "$DEFAULT_SERVICE_URL"
128
+ fi
129
+
130
+ exec "$PYTHON_BIN" "$SCRIPT_DIR/manage_openclaw_btc_wallet.py" reveal-seed "$@"