clawchain-wallet 1.0.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/SKILL.md +243 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +89 -0
- package/dist/src/executor.d.ts +23 -0
- package/dist/src/executor.js +65 -0
- package/dist/src/tools.d.ts +41 -0
- package/dist/src/tools.js +528 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +4 -0
- package/index.ts +103 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +76 -0
- package/python/clawchain_cli.py +1193 -0
- package/python/requirements.txt +2 -0
- package/scripts/postinstall.js +243 -0
- package/src/executor.ts +88 -0
- package/src/tools.ts +623 -0
- package/src/types.ts +45 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ClawChain CLI - ClawChain 私有链钱包管理工具
|
|
4
|
+
支持:创建钱包、导入钱包、管理钱包、查余额、转账、查询交易
|
|
5
|
+
|
|
6
|
+
固定配置:
|
|
7
|
+
- RPC URL: https://n1.clawchain.net
|
|
8
|
+
- Chain ID: 1911
|
|
9
|
+
- 代币名称: CLAW
|
|
10
|
+
- 链名称: ClawChain
|
|
11
|
+
|
|
12
|
+
支持两种调用方式:
|
|
13
|
+
1. 传统命令行参数方式
|
|
14
|
+
2. JSON stdin 方式(更安全,避免命令注入)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
import urllib.request
|
|
23
|
+
import urllib.error
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional, Dict, List, Any
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from web3 import Web3
|
|
30
|
+
from eth_account import Account
|
|
31
|
+
except ImportError:
|
|
32
|
+
print(json.dumps({"success": False, "error": "请先安装 web3 库: pip install web3"}))
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
# ============== 固定配置 ==============
|
|
36
|
+
CLAWCHAIN_CONFIG = {
|
|
37
|
+
"rpc_url": "https://n1.clawchain.net",
|
|
38
|
+
"chain_id": 1911,
|
|
39
|
+
"gas_price": "auto",
|
|
40
|
+
"gas_limit": 21000,
|
|
41
|
+
"chain_name": "ClawChain",
|
|
42
|
+
"token_symbol": "CLAW"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# 默认钱包存储目录
|
|
46
|
+
DEFAULT_WALLETS_DIR = Path.home() / ".clawchain" / "wallets"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_wallets_dir(custom_dir: Optional[str] = None) -> Path:
|
|
50
|
+
"""获取钱包存储目录"""
|
|
51
|
+
if custom_dir:
|
|
52
|
+
wallets_dir = Path(custom_dir)
|
|
53
|
+
else:
|
|
54
|
+
wallets_dir = DEFAULT_WALLETS_DIR
|
|
55
|
+
wallets_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
return wallets_dir
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_web3() -> Web3:
|
|
60
|
+
"""获取 Web3 实例"""
|
|
61
|
+
w3 = Web3(Web3.HTTPProvider(CLAWCHAIN_CONFIG["rpc_url"]))
|
|
62
|
+
if not w3.is_connected():
|
|
63
|
+
raise ConnectionError(f"无法连接到 ClawChain RPC 节点 {CLAWCHAIN_CONFIG['rpc_url']}")
|
|
64
|
+
return w3
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def output_result(data: dict):
|
|
68
|
+
"""输出 JSON 格式结果"""
|
|
69
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_wallet_index(wallets_dir: Path) -> Dict[str, Any]:
|
|
73
|
+
"""加载钱包索引文件"""
|
|
74
|
+
index_file = wallets_dir / "index.json"
|
|
75
|
+
if index_file.exists():
|
|
76
|
+
with open(index_file, "r", encoding="utf-8") as f:
|
|
77
|
+
return json.load(f)
|
|
78
|
+
return {"wallets": {}}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def save_wallet_index(wallets_dir: Path, index: Dict[str, Any]):
|
|
82
|
+
"""保存钱包索引文件"""
|
|
83
|
+
index_file = wallets_dir / "index.json"
|
|
84
|
+
with open(index_file, "w", encoding="utf-8") as f:
|
|
85
|
+
json.dump(index, f, ensure_ascii=False, indent=2)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def generate_wallet_id(name: str) -> str:
|
|
89
|
+
"""生成钱包文件名安全的ID"""
|
|
90
|
+
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)
|
|
91
|
+
return f"{safe_name}_{int(time.time())}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def generate_auto_wallet_name(wallets_dir: Path) -> str:
|
|
95
|
+
"""自动生成钱包名称,格式为 wallet-01, wallet-02 等"""
|
|
96
|
+
index = load_wallet_index(wallets_dir)
|
|
97
|
+
existing_names = {w.get("name", "") for w in index.get("wallets", {}).values()}
|
|
98
|
+
|
|
99
|
+
# 找出已使用的 wallet-XX 编号
|
|
100
|
+
used_numbers = set()
|
|
101
|
+
for name in existing_names:
|
|
102
|
+
if name.startswith("wallet-") and len(name) == 9: # wallet-XX 格式
|
|
103
|
+
try:
|
|
104
|
+
num = int(name[7:])
|
|
105
|
+
used_numbers.add(num)
|
|
106
|
+
except ValueError:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
# 找到第一个未使用的编号
|
|
110
|
+
next_num = 1
|
|
111
|
+
while next_num in used_numbers:
|
|
112
|
+
next_num += 1
|
|
113
|
+
|
|
114
|
+
return f"wallet-{next_num:02d}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_existing_wallet_names(wallets_dir: Path) -> List[str]:
|
|
118
|
+
"""获取所有已存在的钱包名称"""
|
|
119
|
+
index = load_wallet_index(wallets_dir)
|
|
120
|
+
return [w.get("name", "") for w in index.get("wallets", {}).values()]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ============== 钱包创建命令 ==============
|
|
124
|
+
|
|
125
|
+
def cmd_wallet_create(args):
|
|
126
|
+
"""创建新钱包"""
|
|
127
|
+
try:
|
|
128
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
129
|
+
|
|
130
|
+
# 生成新钱包
|
|
131
|
+
account = Account.create()
|
|
132
|
+
|
|
133
|
+
# 确定钱包名称
|
|
134
|
+
if args.name:
|
|
135
|
+
wallet_name = args.name
|
|
136
|
+
else:
|
|
137
|
+
# 自动生成名称 wallet-01, wallet-02 等
|
|
138
|
+
wallet_name = generate_auto_wallet_name(wallets_dir)
|
|
139
|
+
|
|
140
|
+
wallet_id = generate_wallet_id(wallet_name)
|
|
141
|
+
|
|
142
|
+
# 检查名称是否重复
|
|
143
|
+
index = load_wallet_index(wallets_dir)
|
|
144
|
+
existing_names = get_existing_wallet_names(wallets_dir)
|
|
145
|
+
|
|
146
|
+
for existing_wallet in index.get("wallets", {}).values():
|
|
147
|
+
if existing_wallet.get("name") == wallet_name:
|
|
148
|
+
# 提供建议的可用名称
|
|
149
|
+
suggested_name = generate_auto_wallet_name(wallets_dir)
|
|
150
|
+
output_result({
|
|
151
|
+
"success": False,
|
|
152
|
+
"error": f"钱包名称 '{wallet_name}' 已存在,请使用其他名称",
|
|
153
|
+
"suggestion": f"建议使用名称: '{suggested_name}'",
|
|
154
|
+
"existing_wallets": existing_names
|
|
155
|
+
})
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# 保存钱包文件
|
|
159
|
+
wallet_file = wallets_dir / f"{wallet_id}.json"
|
|
160
|
+
wallet_data = {
|
|
161
|
+
"id": wallet_id,
|
|
162
|
+
"name": wallet_name,
|
|
163
|
+
"address": account.address,
|
|
164
|
+
"private_key": account.key.hex(),
|
|
165
|
+
"created_at": datetime.now().isoformat(),
|
|
166
|
+
"chain": CLAWCHAIN_CONFIG["chain_name"]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
with open(wallet_file, "w", encoding="utf-8") as f:
|
|
170
|
+
json.dump(wallet_data, f, ensure_ascii=False, indent=2)
|
|
171
|
+
|
|
172
|
+
# 更新索引
|
|
173
|
+
index["wallets"][wallet_id] = {
|
|
174
|
+
"name": wallet_name,
|
|
175
|
+
"address": account.address,
|
|
176
|
+
"created_at": wallet_data["created_at"]
|
|
177
|
+
}
|
|
178
|
+
save_wallet_index(wallets_dir, index)
|
|
179
|
+
|
|
180
|
+
output_result({
|
|
181
|
+
"success": True,
|
|
182
|
+
"message": f"钱包 '{wallet_name}' 创建成功",
|
|
183
|
+
"wallet": {
|
|
184
|
+
"id": wallet_id,
|
|
185
|
+
"name": wallet_name,
|
|
186
|
+
"address": account.address,
|
|
187
|
+
"private_key": account.key.hex()
|
|
188
|
+
},
|
|
189
|
+
"warning": "请妥善保管私钥,丢失将无法找回!"
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
output_result({"success": False, "error": str(e)})
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ============== 钱包导入命令 ==============
|
|
197
|
+
|
|
198
|
+
def cmd_wallet_import(args):
|
|
199
|
+
"""从私钥导入钱包"""
|
|
200
|
+
try:
|
|
201
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
202
|
+
|
|
203
|
+
# 处理私钥格式
|
|
204
|
+
private_key = args.private_key
|
|
205
|
+
if not private_key.startswith("0x"):
|
|
206
|
+
private_key = "0x" + private_key
|
|
207
|
+
|
|
208
|
+
# 验证私钥
|
|
209
|
+
try:
|
|
210
|
+
account = Account.from_key(private_key)
|
|
211
|
+
except Exception:
|
|
212
|
+
output_result({"success": False, "error": "无效的私钥格式"})
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
# 确定钱包名称
|
|
216
|
+
if args.name:
|
|
217
|
+
wallet_name = args.name
|
|
218
|
+
else:
|
|
219
|
+
# 自动生成名称 wallet-01, wallet-02 等
|
|
220
|
+
wallet_name = generate_auto_wallet_name(wallets_dir)
|
|
221
|
+
|
|
222
|
+
wallet_id = generate_wallet_id(wallet_name)
|
|
223
|
+
|
|
224
|
+
# 检查名称和地址是否重复
|
|
225
|
+
index = load_wallet_index(wallets_dir)
|
|
226
|
+
existing_names = get_existing_wallet_names(wallets_dir)
|
|
227
|
+
|
|
228
|
+
for existing_wallet in index.get("wallets", {}).values():
|
|
229
|
+
if existing_wallet.get("name") == wallet_name:
|
|
230
|
+
# 提供建议的可用名称
|
|
231
|
+
suggested_name = generate_auto_wallet_name(wallets_dir)
|
|
232
|
+
output_result({
|
|
233
|
+
"success": False,
|
|
234
|
+
"error": f"钱包名称 '{wallet_name}' 已存在,请使用其他名称",
|
|
235
|
+
"suggestion": f"建议使用名称: '{suggested_name}'",
|
|
236
|
+
"existing_wallets": existing_names
|
|
237
|
+
})
|
|
238
|
+
return
|
|
239
|
+
if existing_wallet.get("address").lower() == account.address.lower():
|
|
240
|
+
output_result({
|
|
241
|
+
"success": False,
|
|
242
|
+
"error": f"地址 {account.address} 的钱包已存在,名称为 '{existing_wallet.get('name')}'"
|
|
243
|
+
})
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# 保存钱包文件
|
|
247
|
+
wallet_file = wallets_dir / f"{wallet_id}.json"
|
|
248
|
+
wallet_data = {
|
|
249
|
+
"id": wallet_id,
|
|
250
|
+
"name": wallet_name,
|
|
251
|
+
"address": account.address,
|
|
252
|
+
"private_key": private_key,
|
|
253
|
+
"created_at": datetime.now().isoformat(),
|
|
254
|
+
"imported": True,
|
|
255
|
+
"chain": CLAWCHAIN_CONFIG["chain_name"]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
with open(wallet_file, "w", encoding="utf-8") as f:
|
|
259
|
+
json.dump(wallet_data, f, ensure_ascii=False, indent=2)
|
|
260
|
+
|
|
261
|
+
# 更新索引
|
|
262
|
+
index["wallets"][wallet_id] = {
|
|
263
|
+
"name": wallet_name,
|
|
264
|
+
"address": account.address,
|
|
265
|
+
"created_at": wallet_data["created_at"],
|
|
266
|
+
"imported": True
|
|
267
|
+
}
|
|
268
|
+
save_wallet_index(wallets_dir, index)
|
|
269
|
+
|
|
270
|
+
output_result({
|
|
271
|
+
"success": True,
|
|
272
|
+
"message": f"钱包 '{wallet_name}' 导入成功",
|
|
273
|
+
"wallet": {
|
|
274
|
+
"id": wallet_id,
|
|
275
|
+
"name": wallet_name,
|
|
276
|
+
"address": account.address
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
output_result({"success": False, "error": str(e)})
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ============== 钱包列表命令 ==============
|
|
285
|
+
|
|
286
|
+
def cmd_wallet_list(args):
|
|
287
|
+
"""列出所有钱包"""
|
|
288
|
+
try:
|
|
289
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
290
|
+
index = load_wallet_index(wallets_dir)
|
|
291
|
+
|
|
292
|
+
wallets = []
|
|
293
|
+
w3 = None
|
|
294
|
+
|
|
295
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
296
|
+
wallet_entry = {
|
|
297
|
+
"id": wallet_id,
|
|
298
|
+
"name": wallet_info.get("name"),
|
|
299
|
+
"address": wallet_info.get("address"),
|
|
300
|
+
"created_at": wallet_info.get("created_at"),
|
|
301
|
+
"imported": wallet_info.get("imported", False)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# 如果需要显示余额
|
|
305
|
+
if args.show_balance:
|
|
306
|
+
try:
|
|
307
|
+
if w3 is None:
|
|
308
|
+
w3 = get_web3()
|
|
309
|
+
balance_wei = w3.eth.get_balance(wallet_info["address"])
|
|
310
|
+
balance_claw = w3.from_wei(balance_wei, 'ether')
|
|
311
|
+
wallet_entry["balance"] = f"{balance_claw} CLAW"
|
|
312
|
+
wallet_entry["balance_wei"] = str(balance_wei)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
wallet_entry["balance"] = f"查询失败: {str(e)}"
|
|
315
|
+
|
|
316
|
+
wallets.append(wallet_entry)
|
|
317
|
+
|
|
318
|
+
output_result({
|
|
319
|
+
"success": True,
|
|
320
|
+
"count": len(wallets),
|
|
321
|
+
"wallets": wallets,
|
|
322
|
+
"chain": CLAWCHAIN_CONFIG["chain_name"]
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
output_result({"success": False, "error": str(e)})
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ============== 钱包详情命令 ==============
|
|
330
|
+
|
|
331
|
+
def cmd_wallet_info(args):
|
|
332
|
+
"""获取钱包详情"""
|
|
333
|
+
try:
|
|
334
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
335
|
+
index = load_wallet_index(wallets_dir)
|
|
336
|
+
|
|
337
|
+
# 通过名称或 ID 查找钱包
|
|
338
|
+
target_wallet = None
|
|
339
|
+
target_id = None
|
|
340
|
+
|
|
341
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
342
|
+
if wallet_info.get("name") == args.identifier or wallet_id == args.identifier:
|
|
343
|
+
target_wallet = wallet_info
|
|
344
|
+
target_id = wallet_id
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
if not target_wallet:
|
|
348
|
+
output_result({
|
|
349
|
+
"success": False,
|
|
350
|
+
"error": f"未找到钱包: {args.identifier}"
|
|
351
|
+
})
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
# 读取完整钱包文件
|
|
355
|
+
wallet_file = wallets_dir / f"{target_id}.json"
|
|
356
|
+
if not wallet_file.exists():
|
|
357
|
+
output_result({
|
|
358
|
+
"success": False,
|
|
359
|
+
"error": f"钱包文件丢失: {target_id}"
|
|
360
|
+
})
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
with open(wallet_file, "r", encoding="utf-8") as f:
|
|
364
|
+
wallet_data = json.load(f)
|
|
365
|
+
|
|
366
|
+
# 查询余额
|
|
367
|
+
try:
|
|
368
|
+
w3 = get_web3()
|
|
369
|
+
balance_wei = w3.eth.get_balance(wallet_data["address"])
|
|
370
|
+
balance_claw = w3.from_wei(balance_wei, 'ether')
|
|
371
|
+
wallet_data["balance"] = f"{balance_claw} CLAW"
|
|
372
|
+
wallet_data["balance_wei"] = str(balance_wei)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
wallet_data["balance"] = f"查询失败: {str(e)}"
|
|
375
|
+
|
|
376
|
+
# 如果不显示私钥则隐藏
|
|
377
|
+
if not args.show_private_key:
|
|
378
|
+
wallet_data["private_key"] = "***已隐藏,使用 --show-private-key 显示***"
|
|
379
|
+
|
|
380
|
+
output_result({
|
|
381
|
+
"success": True,
|
|
382
|
+
"wallet": wallet_data
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
output_result({"success": False, "error": str(e)})
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ============== 钱包删除命令 ==============
|
|
390
|
+
|
|
391
|
+
def cmd_wallet_delete(args):
|
|
392
|
+
"""删除钱包"""
|
|
393
|
+
try:
|
|
394
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
395
|
+
index = load_wallet_index(wallets_dir)
|
|
396
|
+
|
|
397
|
+
# 通过名称或 ID 查找钱包
|
|
398
|
+
target_id = None
|
|
399
|
+
target_name = None
|
|
400
|
+
|
|
401
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
402
|
+
if wallet_info.get("name") == args.identifier or wallet_id == args.identifier:
|
|
403
|
+
target_id = wallet_id
|
|
404
|
+
target_name = wallet_info.get("name")
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
if not target_id:
|
|
408
|
+
output_result({
|
|
409
|
+
"success": False,
|
|
410
|
+
"error": f"未找到钱包: {args.identifier}"
|
|
411
|
+
})
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# 删除钱包文件
|
|
415
|
+
wallet_file = wallets_dir / f"{target_id}.json"
|
|
416
|
+
if wallet_file.exists():
|
|
417
|
+
# 备份到 deleted 目录
|
|
418
|
+
deleted_dir = wallets_dir / "deleted"
|
|
419
|
+
deleted_dir.mkdir(exist_ok=True)
|
|
420
|
+
backup_file = deleted_dir / f"{target_id}_{int(time.time())}.json"
|
|
421
|
+
wallet_file.rename(backup_file)
|
|
422
|
+
|
|
423
|
+
# 从索引中移除
|
|
424
|
+
del index["wallets"][target_id]
|
|
425
|
+
save_wallet_index(wallets_dir, index)
|
|
426
|
+
|
|
427
|
+
output_result({
|
|
428
|
+
"success": True,
|
|
429
|
+
"message": f"钱包 '{target_name}' 已删除(备份已保存到 deleted 目录)",
|
|
430
|
+
"deleted_wallet": {
|
|
431
|
+
"id": target_id,
|
|
432
|
+
"name": target_name
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
output_result({"success": False, "error": str(e)})
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ============== 钱包重命名命令 ==============
|
|
441
|
+
|
|
442
|
+
def cmd_wallet_rename(args):
|
|
443
|
+
"""重命名钱包"""
|
|
444
|
+
try:
|
|
445
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
446
|
+
index = load_wallet_index(wallets_dir)
|
|
447
|
+
|
|
448
|
+
# 通过名称或 ID 查找钱包
|
|
449
|
+
target_id = None
|
|
450
|
+
old_name = None
|
|
451
|
+
|
|
452
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
453
|
+
if wallet_info.get("name") == args.identifier or wallet_id == args.identifier:
|
|
454
|
+
target_id = wallet_id
|
|
455
|
+
old_name = wallet_info.get("name")
|
|
456
|
+
break
|
|
457
|
+
|
|
458
|
+
if not target_id:
|
|
459
|
+
output_result({
|
|
460
|
+
"success": False,
|
|
461
|
+
"error": f"未找到钱包: {args.identifier}"
|
|
462
|
+
})
|
|
463
|
+
return
|
|
464
|
+
|
|
465
|
+
# 检查新名称是否重复
|
|
466
|
+
for wallet_info in index.get("wallets", {}).values():
|
|
467
|
+
if wallet_info.get("name") == args.new_name:
|
|
468
|
+
output_result({
|
|
469
|
+
"success": False,
|
|
470
|
+
"error": f"钱包名称 '{args.new_name}' 已存在"
|
|
471
|
+
})
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# 更新索引
|
|
475
|
+
index["wallets"][target_id]["name"] = args.new_name
|
|
476
|
+
save_wallet_index(wallets_dir, index)
|
|
477
|
+
|
|
478
|
+
# 更新钱包文件
|
|
479
|
+
wallet_file = wallets_dir / f"{target_id}.json"
|
|
480
|
+
if wallet_file.exists():
|
|
481
|
+
with open(wallet_file, "r", encoding="utf-8") as f:
|
|
482
|
+
wallet_data = json.load(f)
|
|
483
|
+
wallet_data["name"] = args.new_name
|
|
484
|
+
with open(wallet_file, "w", encoding="utf-8") as f:
|
|
485
|
+
json.dump(wallet_data, f, ensure_ascii=False, indent=2)
|
|
486
|
+
|
|
487
|
+
output_result({
|
|
488
|
+
"success": True,
|
|
489
|
+
"message": f"钱包已重命名: '{old_name}' → '{args.new_name}'",
|
|
490
|
+
"wallet": {
|
|
491
|
+
"id": target_id,
|
|
492
|
+
"old_name": old_name,
|
|
493
|
+
"new_name": args.new_name
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
output_result({"success": False, "error": str(e)})
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ============== 余额查询命令 ==============
|
|
502
|
+
|
|
503
|
+
def cmd_balance(args):
|
|
504
|
+
"""查询余额"""
|
|
505
|
+
try:
|
|
506
|
+
w3 = get_web3()
|
|
507
|
+
|
|
508
|
+
address = args.address
|
|
509
|
+
wallet_name = None
|
|
510
|
+
|
|
511
|
+
# 如果提供的是钱包名称而非地址,则查找
|
|
512
|
+
if not address.startswith("0x"):
|
|
513
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
514
|
+
index = load_wallet_index(wallets_dir)
|
|
515
|
+
|
|
516
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
517
|
+
if wallet_info.get("name") == address or wallet_id == address:
|
|
518
|
+
address = wallet_info["address"]
|
|
519
|
+
wallet_name = wallet_info["name"]
|
|
520
|
+
break
|
|
521
|
+
else:
|
|
522
|
+
output_result({
|
|
523
|
+
"success": False,
|
|
524
|
+
"error": f"未找到钱包: {args.address},且不是有效的地址格式"
|
|
525
|
+
})
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# 验证地址格式
|
|
529
|
+
if not w3.is_address(address):
|
|
530
|
+
output_result({
|
|
531
|
+
"success": False,
|
|
532
|
+
"error": f"无效的地址格式: {address}"
|
|
533
|
+
})
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
balance_wei = w3.eth.get_balance(address)
|
|
537
|
+
balance_claw = w3.from_wei(balance_wei, 'ether')
|
|
538
|
+
|
|
539
|
+
result = {
|
|
540
|
+
"success": True,
|
|
541
|
+
"address": address,
|
|
542
|
+
"balance": f"{balance_claw} CLAW",
|
|
543
|
+
"balance_wei": str(balance_wei),
|
|
544
|
+
"chain": CLAWCHAIN_CONFIG["chain_name"]
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if wallet_name:
|
|
548
|
+
result["wallet_name"] = wallet_name
|
|
549
|
+
|
|
550
|
+
output_result(result)
|
|
551
|
+
|
|
552
|
+
except ConnectionError as e:
|
|
553
|
+
output_result({"success": False, "error": str(e)})
|
|
554
|
+
except Exception as e:
|
|
555
|
+
output_result({"success": False, "error": f"查询余额失败: {str(e)}"})
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ============== 转账命令 ==============
|
|
559
|
+
|
|
560
|
+
def cmd_transfer(args):
|
|
561
|
+
"""转账 CLAW"""
|
|
562
|
+
try:
|
|
563
|
+
w3 = get_web3()
|
|
564
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
565
|
+
|
|
566
|
+
# 获取发送方钱包
|
|
567
|
+
index = load_wallet_index(wallets_dir)
|
|
568
|
+
sender_wallet = None
|
|
569
|
+
sender_id = None
|
|
570
|
+
|
|
571
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
572
|
+
if wallet_info.get("name") == args.from_wallet or wallet_id == args.from_wallet:
|
|
573
|
+
sender_id = wallet_id
|
|
574
|
+
break
|
|
575
|
+
|
|
576
|
+
if not sender_id:
|
|
577
|
+
output_result({
|
|
578
|
+
"success": False,
|
|
579
|
+
"error": f"未找到发送方钱包: {args.from_wallet}"
|
|
580
|
+
})
|
|
581
|
+
return
|
|
582
|
+
|
|
583
|
+
# 读取钱包私钥
|
|
584
|
+
wallet_file = wallets_dir / f"{sender_id}.json"
|
|
585
|
+
with open(wallet_file, "r", encoding="utf-8") as f:
|
|
586
|
+
sender_wallet = json.load(f)
|
|
587
|
+
|
|
588
|
+
private_key = sender_wallet["private_key"]
|
|
589
|
+
from_address = sender_wallet["address"]
|
|
590
|
+
|
|
591
|
+
# 处理接收地址
|
|
592
|
+
to_address = args.to
|
|
593
|
+
to_wallet_name = None
|
|
594
|
+
|
|
595
|
+
if not to_address.startswith("0x"):
|
|
596
|
+
# 可能是钱包名称
|
|
597
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
598
|
+
if wallet_info.get("name") == to_address or wallet_id == to_address:
|
|
599
|
+
to_address = wallet_info["address"]
|
|
600
|
+
to_wallet_name = wallet_info["name"]
|
|
601
|
+
break
|
|
602
|
+
else:
|
|
603
|
+
output_result({
|
|
604
|
+
"success": False,
|
|
605
|
+
"error": f"未找到接收方钱包: {args.to},且不是有效的地址格式"
|
|
606
|
+
})
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
if not w3.is_address(to_address):
|
|
610
|
+
output_result({
|
|
611
|
+
"success": False,
|
|
612
|
+
"error": f"无效的接收地址: {to_address}"
|
|
613
|
+
})
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
# 转换金额
|
|
617
|
+
value = w3.to_wei(args.amount, 'ether')
|
|
618
|
+
|
|
619
|
+
# 检查余额
|
|
620
|
+
balance = w3.eth.get_balance(from_address)
|
|
621
|
+
if balance < value:
|
|
622
|
+
output_result({
|
|
623
|
+
"success": False,
|
|
624
|
+
"error": f"余额不足。当前余额: {w3.from_wei(balance, 'ether')} CLAW,需要: {args.amount} CLAW"
|
|
625
|
+
})
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
# 获取 gas price
|
|
629
|
+
gas_price = w3.eth.gas_price
|
|
630
|
+
|
|
631
|
+
# 获取 nonce
|
|
632
|
+
nonce = w3.eth.get_transaction_count(from_address)
|
|
633
|
+
|
|
634
|
+
# 构建交易
|
|
635
|
+
tx = {
|
|
636
|
+
'nonce': nonce,
|
|
637
|
+
'to': to_address,
|
|
638
|
+
'value': value,
|
|
639
|
+
'gas': CLAWCHAIN_CONFIG["gas_limit"],
|
|
640
|
+
'gasPrice': gas_price,
|
|
641
|
+
'chainId': CLAWCHAIN_CONFIG["chain_id"]
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
# 估算 gas(可选,用于更精确的 gas 限制)
|
|
645
|
+
try:
|
|
646
|
+
estimated_gas = w3.eth.estimate_gas(tx)
|
|
647
|
+
tx['gas'] = int(estimated_gas * 1.2) # 增加 20% 余量
|
|
648
|
+
except:
|
|
649
|
+
pass
|
|
650
|
+
|
|
651
|
+
# 检查总花费
|
|
652
|
+
total_cost = value + (tx['gas'] * gas_price)
|
|
653
|
+
if balance < total_cost:
|
|
654
|
+
output_result({
|
|
655
|
+
"success": False,
|
|
656
|
+
"error": f"余额不足以支付 gas 费用。当前余额: {w3.from_wei(balance, 'ether')} CLAW,总需要: {w3.from_wei(total_cost, 'ether')} CLAW"
|
|
657
|
+
})
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
# 签名并发送
|
|
661
|
+
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
662
|
+
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
663
|
+
tx_hash_hex = tx_hash.hex()
|
|
664
|
+
|
|
665
|
+
result = {
|
|
666
|
+
"success": True,
|
|
667
|
+
"message": "交易已发送",
|
|
668
|
+
"transaction": {
|
|
669
|
+
"hash": tx_hash_hex,
|
|
670
|
+
"from": from_address,
|
|
671
|
+
"from_wallet": sender_wallet["name"],
|
|
672
|
+
"to": to_address,
|
|
673
|
+
"amount": f"{args.amount} CLAW",
|
|
674
|
+
"gas_price": f"{w3.from_wei(gas_price, 'gwei')} Gwei",
|
|
675
|
+
"estimated_fee": f"{w3.from_wei(tx['gas'] * gas_price, 'ether')} CLAW"
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if to_wallet_name:
|
|
680
|
+
result["transaction"]["to_wallet"] = to_wallet_name
|
|
681
|
+
|
|
682
|
+
# 等待确认
|
|
683
|
+
if args.wait:
|
|
684
|
+
try:
|
|
685
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
686
|
+
if receipt.status == 1:
|
|
687
|
+
result["confirmed"] = True
|
|
688
|
+
result["block_number"] = receipt.blockNumber
|
|
689
|
+
result["actual_gas_used"] = receipt.gasUsed
|
|
690
|
+
result["actual_fee"] = f"{w3.from_wei(receipt.gasUsed * gas_price, 'ether')} CLAW"
|
|
691
|
+
else:
|
|
692
|
+
result["confirmed"] = False
|
|
693
|
+
result["error"] = "交易执行失败"
|
|
694
|
+
except Exception as e:
|
|
695
|
+
result["confirmed"] = False
|
|
696
|
+
result["error"] = f"等待确认超时: {str(e)}"
|
|
697
|
+
|
|
698
|
+
output_result(result)
|
|
699
|
+
|
|
700
|
+
except Exception as e:
|
|
701
|
+
output_result({"success": False, "error": str(e)})
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ============== 交易查询命令 ==============
|
|
705
|
+
|
|
706
|
+
def cmd_tx_info(args):
|
|
707
|
+
"""查询交易详情"""
|
|
708
|
+
try:
|
|
709
|
+
w3 = get_web3()
|
|
710
|
+
|
|
711
|
+
tx_hash = args.hash
|
|
712
|
+
if not tx_hash.startswith("0x"):
|
|
713
|
+
tx_hash = "0x" + tx_hash
|
|
714
|
+
|
|
715
|
+
tx = w3.eth.get_transaction(tx_hash)
|
|
716
|
+
|
|
717
|
+
result = {
|
|
718
|
+
"success": True,
|
|
719
|
+
"transaction": {
|
|
720
|
+
"hash": tx.hash.hex(),
|
|
721
|
+
"from": tx["from"],
|
|
722
|
+
"to": tx.to,
|
|
723
|
+
"value": f"{w3.from_wei(tx.value, 'ether')} CLAW",
|
|
724
|
+
"value_wei": str(tx.value),
|
|
725
|
+
"gas": tx.gas,
|
|
726
|
+
"gas_price": f"{w3.from_wei(tx.gasPrice, 'gwei')} Gwei",
|
|
727
|
+
"nonce": tx.nonce,
|
|
728
|
+
"block_number": tx.blockNumber
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# 获取回执
|
|
733
|
+
try:
|
|
734
|
+
receipt = w3.eth.get_transaction_receipt(tx_hash)
|
|
735
|
+
result["transaction"]["status"] = "成功" if receipt.status == 1 else "失败"
|
|
736
|
+
result["transaction"]["gas_used"] = receipt.gasUsed
|
|
737
|
+
result["transaction"]["actual_fee"] = f"{w3.from_wei(receipt.gasUsed * tx.gasPrice, 'ether')} CLAW"
|
|
738
|
+
except:
|
|
739
|
+
result["transaction"]["status"] = "待确认"
|
|
740
|
+
|
|
741
|
+
output_result(result)
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
output_result({"success": False, "error": f"查询交易失败: {str(e)}"})
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# ============== 交易历史命令 ==============
|
|
748
|
+
|
|
749
|
+
def cmd_tx_history(args):
|
|
750
|
+
"""查询钱包交易历史"""
|
|
751
|
+
try:
|
|
752
|
+
w3 = get_web3()
|
|
753
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
754
|
+
|
|
755
|
+
# 获取地址
|
|
756
|
+
address = args.address
|
|
757
|
+
wallet_name = None
|
|
758
|
+
|
|
759
|
+
if not address.startswith("0x"):
|
|
760
|
+
index = load_wallet_index(wallets_dir)
|
|
761
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
762
|
+
if wallet_info.get("name") == address or wallet_id == address:
|
|
763
|
+
address = wallet_info["address"]
|
|
764
|
+
wallet_name = wallet_info["name"]
|
|
765
|
+
break
|
|
766
|
+
else:
|
|
767
|
+
output_result({
|
|
768
|
+
"success": False,
|
|
769
|
+
"error": f"未找到钱包: {args.address}"
|
|
770
|
+
})
|
|
771
|
+
return
|
|
772
|
+
|
|
773
|
+
address = address.lower()
|
|
774
|
+
latest_block = w3.eth.block_number
|
|
775
|
+
start_block = max(0, latest_block - args.blocks)
|
|
776
|
+
|
|
777
|
+
transactions = []
|
|
778
|
+
|
|
779
|
+
for block_num in range(start_block, latest_block + 1):
|
|
780
|
+
try:
|
|
781
|
+
block = w3.eth.get_block(block_num, full_transactions=True)
|
|
782
|
+
for tx in block.transactions:
|
|
783
|
+
if tx['from'].lower() == address or (tx.to and tx.to.lower() == address):
|
|
784
|
+
direction = "发送" if tx['from'].lower() == address else "接收"
|
|
785
|
+
transactions.append({
|
|
786
|
+
"hash": tx.hash.hex(),
|
|
787
|
+
"block": block_num,
|
|
788
|
+
"from": tx['from'],
|
|
789
|
+
"to": tx.to,
|
|
790
|
+
"value": f"{w3.from_wei(tx.value, 'ether')} CLAW",
|
|
791
|
+
"direction": direction,
|
|
792
|
+
"timestamp": block.timestamp
|
|
793
|
+
})
|
|
794
|
+
except:
|
|
795
|
+
continue
|
|
796
|
+
|
|
797
|
+
result = {
|
|
798
|
+
"success": True,
|
|
799
|
+
"address": address,
|
|
800
|
+
"scanned_blocks": args.blocks,
|
|
801
|
+
"transaction_count": len(transactions),
|
|
802
|
+
"transactions": transactions[-args.limit:]
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if wallet_name:
|
|
806
|
+
result["wallet_name"] = wallet_name
|
|
807
|
+
|
|
808
|
+
output_result(result)
|
|
809
|
+
|
|
810
|
+
except Exception as e:
|
|
811
|
+
output_result({"success": False, "error": str(e)})
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
# ============== 链信息命令 ==============
|
|
815
|
+
|
|
816
|
+
def cmd_chain_info(args):
|
|
817
|
+
"""查询链信息"""
|
|
818
|
+
try:
|
|
819
|
+
w3 = get_web3()
|
|
820
|
+
|
|
821
|
+
output_result({
|
|
822
|
+
"success": True,
|
|
823
|
+
"chain": {
|
|
824
|
+
"name": CLAWCHAIN_CONFIG["chain_name"],
|
|
825
|
+
"token_symbol": CLAWCHAIN_CONFIG["token_symbol"],
|
|
826
|
+
"rpc_url": CLAWCHAIN_CONFIG["rpc_url"],
|
|
827
|
+
"chain_id": CLAWCHAIN_CONFIG["chain_id"],
|
|
828
|
+
"connected": True,
|
|
829
|
+
"latest_block": w3.eth.block_number,
|
|
830
|
+
"gas_price": f"{w3.from_wei(w3.eth.gas_price, 'gwei')} Gwei"
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
except ConnectionError:
|
|
835
|
+
output_result({
|
|
836
|
+
"success": True,
|
|
837
|
+
"chain": {
|
|
838
|
+
"name": CLAWCHAIN_CONFIG["chain_name"],
|
|
839
|
+
"token_symbol": CLAWCHAIN_CONFIG["token_symbol"],
|
|
840
|
+
"rpc_url": CLAWCHAIN_CONFIG["rpc_url"],
|
|
841
|
+
"chain_id": CLAWCHAIN_CONFIG["chain_id"],
|
|
842
|
+
"connected": False
|
|
843
|
+
}
|
|
844
|
+
})
|
|
845
|
+
except Exception as e:
|
|
846
|
+
output_result({"success": False, "error": str(e)})
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
# ============== 区块查询命令 ==============
|
|
850
|
+
|
|
851
|
+
def cmd_block_info(args):
|
|
852
|
+
"""查询区块信息"""
|
|
853
|
+
try:
|
|
854
|
+
w3 = get_web3()
|
|
855
|
+
|
|
856
|
+
if args.number == "latest":
|
|
857
|
+
block = w3.eth.get_block('latest')
|
|
858
|
+
else:
|
|
859
|
+
block = w3.eth.get_block(int(args.number))
|
|
860
|
+
|
|
861
|
+
output_result({
|
|
862
|
+
"success": True,
|
|
863
|
+
"block": {
|
|
864
|
+
"number": block.number,
|
|
865
|
+
"hash": block.hash.hex(),
|
|
866
|
+
"parent_hash": block.parentHash.hex(),
|
|
867
|
+
"timestamp": block.timestamp,
|
|
868
|
+
"transaction_count": len(block.transactions),
|
|
869
|
+
"gas_used": block.gasUsed,
|
|
870
|
+
"gas_limit": block.gasLimit
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
except Exception as e:
|
|
875
|
+
output_result({"success": False, "error": str(e)})
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
# ============== 导出钱包命令 ==============
|
|
879
|
+
|
|
880
|
+
def cmd_wallet_export(args):
|
|
881
|
+
"""导出钱包(显示私钥)"""
|
|
882
|
+
try:
|
|
883
|
+
wallets_dir = get_wallets_dir(args.wallets_dir)
|
|
884
|
+
index = load_wallet_index(wallets_dir)
|
|
885
|
+
|
|
886
|
+
# 通过名称或 ID 查找钱包
|
|
887
|
+
target_id = None
|
|
888
|
+
|
|
889
|
+
for wallet_id, wallet_info in index.get("wallets", {}).items():
|
|
890
|
+
if wallet_info.get("name") == args.identifier or wallet_id == args.identifier:
|
|
891
|
+
target_id = wallet_id
|
|
892
|
+
break
|
|
893
|
+
|
|
894
|
+
if not target_id:
|
|
895
|
+
output_result({
|
|
896
|
+
"success": False,
|
|
897
|
+
"error": f"未找到钱包: {args.identifier}"
|
|
898
|
+
})
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
# 读取完整钱包文件
|
|
902
|
+
wallet_file = wallets_dir / f"{target_id}.json"
|
|
903
|
+
with open(wallet_file, "r", encoding="utf-8") as f:
|
|
904
|
+
wallet_data = json.load(f)
|
|
905
|
+
|
|
906
|
+
output_result({
|
|
907
|
+
"success": True,
|
|
908
|
+
"wallet": {
|
|
909
|
+
"name": wallet_data["name"],
|
|
910
|
+
"address": wallet_data["address"],
|
|
911
|
+
"private_key": wallet_data["private_key"]
|
|
912
|
+
},
|
|
913
|
+
"warning": "请妥善保管私钥,不要泄露给他人!"
|
|
914
|
+
})
|
|
915
|
+
|
|
916
|
+
except Exception as e:
|
|
917
|
+
output_result({"success": False, "error": str(e)})
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
# ============== 领取空投命令 ==============
|
|
921
|
+
|
|
922
|
+
# Faucet API 配置
|
|
923
|
+
FAUCET_API_URL = "https://faucet.clawchain.net/api/claim"
|
|
924
|
+
|
|
925
|
+
def cmd_claim_airdrop(args):
|
|
926
|
+
"""通过 moltbook 帖子领取 CLAW 空投"""
|
|
927
|
+
try:
|
|
928
|
+
post_url = args.post_url
|
|
929
|
+
|
|
930
|
+
# 验证 URL 格式
|
|
931
|
+
if not post_url or not post_url.startswith("https://"):
|
|
932
|
+
output_result({
|
|
933
|
+
"success": False,
|
|
934
|
+
"error": "无效的帖子 URL,必须是 https:// 开头的完整 URL"
|
|
935
|
+
})
|
|
936
|
+
return
|
|
937
|
+
|
|
938
|
+
# 验证是否是 moltbook URL
|
|
939
|
+
if "moltbook.com" not in post_url:
|
|
940
|
+
output_result({
|
|
941
|
+
"success": False,
|
|
942
|
+
"error": "帖子 URL 必须是 moltbook.com 的链接",
|
|
943
|
+
"hint": "请先在 moltbook 上发布包含你钱包地址的帖子或回复"
|
|
944
|
+
})
|
|
945
|
+
return
|
|
946
|
+
|
|
947
|
+
# 构建请求数据
|
|
948
|
+
request_data = json.dumps({"post_url": post_url}).encode("utf-8")
|
|
949
|
+
|
|
950
|
+
# 发送请求到 faucet API
|
|
951
|
+
req = urllib.request.Request(
|
|
952
|
+
FAUCET_API_URL,
|
|
953
|
+
data=request_data,
|
|
954
|
+
headers={
|
|
955
|
+
"Content-Type": "application/json",
|
|
956
|
+
"User-Agent": "ClawChainWallet/1.0"
|
|
957
|
+
},
|
|
958
|
+
method="POST"
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
try:
|
|
962
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
963
|
+
response_data = json.loads(response.read().decode("utf-8"))
|
|
964
|
+
|
|
965
|
+
output_result({
|
|
966
|
+
"success": True,
|
|
967
|
+
"message": "空投领取请求已发送",
|
|
968
|
+
"post_url": post_url,
|
|
969
|
+
"faucet_response": response_data
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
except urllib.error.HTTPError as e:
|
|
973
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
974
|
+
try:
|
|
975
|
+
error_data = json.loads(error_body)
|
|
976
|
+
error_msg = error_data.get("error", error_data.get("message", str(e)))
|
|
977
|
+
except:
|
|
978
|
+
error_msg = error_body or str(e)
|
|
979
|
+
|
|
980
|
+
output_result({
|
|
981
|
+
"success": False,
|
|
982
|
+
"error": f"Faucet API 错误: {error_msg}",
|
|
983
|
+
"status_code": e.code,
|
|
984
|
+
"post_url": post_url
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
except urllib.error.URLError as e:
|
|
988
|
+
output_result({
|
|
989
|
+
"success": False,
|
|
990
|
+
"error": f"网络错误: {str(e.reason)}",
|
|
991
|
+
"hint": "请检查网络连接或稍后重试"
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
except Exception as e:
|
|
995
|
+
output_result({"success": False, "error": str(e)})
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
# ============== JSON Stdin 处理 ==============
|
|
999
|
+
|
|
1000
|
+
class JsonStdinArgs:
|
|
1001
|
+
"""模拟 argparse.Namespace,用于 JSON stdin 模式"""
|
|
1002
|
+
def __init__(self, data: Dict[str, Any]):
|
|
1003
|
+
self._data = data
|
|
1004
|
+
self.wallets_dir = data.get("wallets_dir")
|
|
1005
|
+
self.command = data.get("command")
|
|
1006
|
+
args = data.get("args", {})
|
|
1007
|
+
|
|
1008
|
+
# 通用参数
|
|
1009
|
+
self.name = args.get("name")
|
|
1010
|
+
self.identifier = args.get("identifier")
|
|
1011
|
+
self.new_name = args.get("new_name")
|
|
1012
|
+
self.address = args.get("address")
|
|
1013
|
+
self.hash = args.get("hash")
|
|
1014
|
+
self.number = args.get("number", "latest")
|
|
1015
|
+
|
|
1016
|
+
# 钱包导入参数
|
|
1017
|
+
self.private_key = args.get("private_key")
|
|
1018
|
+
|
|
1019
|
+
# 布尔参数
|
|
1020
|
+
self.show_balance = args.get("show_balance", False)
|
|
1021
|
+
self.show_private_key = args.get("show_private_key", False)
|
|
1022
|
+
self.wait = args.get("wait", False)
|
|
1023
|
+
|
|
1024
|
+
# 转账参数
|
|
1025
|
+
self.from_wallet = args.get("from_wallet")
|
|
1026
|
+
self.to = args.get("to")
|
|
1027
|
+
self.amount = args.get("amount")
|
|
1028
|
+
|
|
1029
|
+
# 交易历史参数
|
|
1030
|
+
self.blocks = args.get("blocks", 1000)
|
|
1031
|
+
self.limit = args.get("limit", 20)
|
|
1032
|
+
|
|
1033
|
+
# 空投领取参数
|
|
1034
|
+
self.post_url = args.get("post_url")
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def handle_json_stdin():
|
|
1038
|
+
"""处理 JSON stdin 输入模式"""
|
|
1039
|
+
try:
|
|
1040
|
+
# 从 stdin 读取 JSON
|
|
1041
|
+
json_input = sys.stdin.read()
|
|
1042
|
+
data = json.loads(json_input)
|
|
1043
|
+
|
|
1044
|
+
# 创建参数对象
|
|
1045
|
+
args = JsonStdinArgs(data)
|
|
1046
|
+
|
|
1047
|
+
if not args.command:
|
|
1048
|
+
output_result({"success": False, "error": "缺少 command 字段"})
|
|
1049
|
+
return
|
|
1050
|
+
|
|
1051
|
+
# 命令映射
|
|
1052
|
+
commands = {
|
|
1053
|
+
"wallet-create": cmd_wallet_create,
|
|
1054
|
+
"wallet-import": cmd_wallet_import,
|
|
1055
|
+
"wallet-list": cmd_wallet_list,
|
|
1056
|
+
"wallet-info": cmd_wallet_info,
|
|
1057
|
+
"wallet-delete": cmd_wallet_delete,
|
|
1058
|
+
"wallet-rename": cmd_wallet_rename,
|
|
1059
|
+
"wallet-export": cmd_wallet_export,
|
|
1060
|
+
"balance": cmd_balance,
|
|
1061
|
+
"transfer": cmd_transfer,
|
|
1062
|
+
"tx-info": cmd_tx_info,
|
|
1063
|
+
"tx-history": cmd_tx_history,
|
|
1064
|
+
"chain-info": cmd_chain_info,
|
|
1065
|
+
"block-info": cmd_block_info,
|
|
1066
|
+
"claim-airdrop": cmd_claim_airdrop
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
handler = commands.get(args.command)
|
|
1070
|
+
if handler:
|
|
1071
|
+
handler(args)
|
|
1072
|
+
else:
|
|
1073
|
+
output_result({"success": False, "error": f"未知命令: {args.command}"})
|
|
1074
|
+
|
|
1075
|
+
except json.JSONDecodeError as e:
|
|
1076
|
+
output_result({"success": False, "error": f"JSON 解析错误: {str(e)}"})
|
|
1077
|
+
except Exception as e:
|
|
1078
|
+
output_result({"success": False, "error": f"处理错误: {str(e)}"})
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
# ============== 主程序 ==============
|
|
1082
|
+
|
|
1083
|
+
def main():
|
|
1084
|
+
# 检查是否使用 JSON stdin 模式
|
|
1085
|
+
if len(sys.argv) == 2 and sys.argv[1] == "--json-stdin":
|
|
1086
|
+
handle_json_stdin()
|
|
1087
|
+
return
|
|
1088
|
+
|
|
1089
|
+
parser = argparse.ArgumentParser(
|
|
1090
|
+
description="ClawChain CLI - ClawChain 私有链钱包管理工具"
|
|
1091
|
+
)
|
|
1092
|
+
parser.add_argument("--wallets-dir", dest="wallets_dir", help="自定义钱包存储目录")
|
|
1093
|
+
parser.add_argument("--json-stdin", dest="json_stdin", action="store_true", help="从 stdin 读取 JSON 参数")
|
|
1094
|
+
|
|
1095
|
+
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
|
1096
|
+
|
|
1097
|
+
# === wallet create ===
|
|
1098
|
+
wallet_create = subparsers.add_parser("wallet-create", help="创建新钱包")
|
|
1099
|
+
wallet_create.add_argument("--name", help="钱包名称")
|
|
1100
|
+
|
|
1101
|
+
# === wallet import ===
|
|
1102
|
+
wallet_import = subparsers.add_parser("wallet-import", help="导入钱包")
|
|
1103
|
+
wallet_import.add_argument("--private-key", dest="private_key", required=True, help="私钥")
|
|
1104
|
+
wallet_import.add_argument("--name", help="钱包名称")
|
|
1105
|
+
|
|
1106
|
+
# === wallet list ===
|
|
1107
|
+
wallet_list = subparsers.add_parser("wallet-list", help="列出所有钱包")
|
|
1108
|
+
wallet_list.add_argument("--show-balance", dest="show_balance", action="store_true", help="显示余额")
|
|
1109
|
+
|
|
1110
|
+
# === wallet info ===
|
|
1111
|
+
wallet_info = subparsers.add_parser("wallet-info", help="获取钱包详情")
|
|
1112
|
+
wallet_info.add_argument("identifier", help="钱包名称或 ID")
|
|
1113
|
+
wallet_info.add_argument("--show-private-key", dest="show_private_key", action="store_true", help="显示私钥")
|
|
1114
|
+
|
|
1115
|
+
# === wallet delete ===
|
|
1116
|
+
wallet_delete = subparsers.add_parser("wallet-delete", help="删除钱包")
|
|
1117
|
+
wallet_delete.add_argument("identifier", help="钱包名称或 ID")
|
|
1118
|
+
|
|
1119
|
+
# === wallet rename ===
|
|
1120
|
+
wallet_rename = subparsers.add_parser("wallet-rename", help="重命名钱包")
|
|
1121
|
+
wallet_rename.add_argument("identifier", help="钱包名称或 ID")
|
|
1122
|
+
wallet_rename.add_argument("new_name", help="新名称")
|
|
1123
|
+
|
|
1124
|
+
# === wallet export ===
|
|
1125
|
+
wallet_export = subparsers.add_parser("wallet-export", help="导出钱包私钥")
|
|
1126
|
+
wallet_export.add_argument("identifier", help="钱包名称或 ID")
|
|
1127
|
+
|
|
1128
|
+
# === balance ===
|
|
1129
|
+
balance = subparsers.add_parser("balance", help="查询余额")
|
|
1130
|
+
balance.add_argument("address", help="钱包地址、名称或 ID")
|
|
1131
|
+
|
|
1132
|
+
# === transfer ===
|
|
1133
|
+
transfer = subparsers.add_parser("transfer", help="转账 CLAW")
|
|
1134
|
+
transfer.add_argument("--from", dest="from_wallet", required=True, help="发送方钱包名称或 ID")
|
|
1135
|
+
transfer.add_argument("--to", required=True, help="接收方地址、钱包名称或 ID")
|
|
1136
|
+
transfer.add_argument("--amount", type=float, required=True, help="转账金额 (CLAW)")
|
|
1137
|
+
transfer.add_argument("--wait", action="store_true", help="等待交易确认")
|
|
1138
|
+
|
|
1139
|
+
# === tx info ===
|
|
1140
|
+
tx_info = subparsers.add_parser("tx-info", help="查询交易详情")
|
|
1141
|
+
tx_info.add_argument("hash", help="交易哈希")
|
|
1142
|
+
|
|
1143
|
+
# === tx history ===
|
|
1144
|
+
tx_history = subparsers.add_parser("tx-history", help="查询交易历史")
|
|
1145
|
+
tx_history.add_argument("address", help="钱包地址、名称或 ID")
|
|
1146
|
+
tx_history.add_argument("--blocks", type=int, default=1000, help="扫描区块数量 (默认: 1000)")
|
|
1147
|
+
tx_history.add_argument("--limit", type=int, default=20, help="显示数量限制 (默认: 20)")
|
|
1148
|
+
|
|
1149
|
+
# === chain info ===
|
|
1150
|
+
subparsers.add_parser("chain-info", help="查询链信息")
|
|
1151
|
+
|
|
1152
|
+
# === block info ===
|
|
1153
|
+
block_info = subparsers.add_parser("block-info", help="查询区块信息")
|
|
1154
|
+
block_info.add_argument("number", nargs="?", default="latest", help="区块号 (默认: latest)")
|
|
1155
|
+
|
|
1156
|
+
# === claim airdrop ===
|
|
1157
|
+
claim_airdrop = subparsers.add_parser("claim-airdrop", help="通过 moltbook 帖子领取 CLAW 空投")
|
|
1158
|
+
claim_airdrop.add_argument("--post-url", dest="post_url", required=True, help="moltbook 帖子或回复的 URL")
|
|
1159
|
+
|
|
1160
|
+
# 解析参数
|
|
1161
|
+
args = parser.parse_args()
|
|
1162
|
+
|
|
1163
|
+
if not args.command:
|
|
1164
|
+
parser.print_help()
|
|
1165
|
+
return
|
|
1166
|
+
|
|
1167
|
+
# 执行命令
|
|
1168
|
+
commands = {
|
|
1169
|
+
"wallet-create": cmd_wallet_create,
|
|
1170
|
+
"wallet-import": cmd_wallet_import,
|
|
1171
|
+
"wallet-list": cmd_wallet_list,
|
|
1172
|
+
"wallet-info": cmd_wallet_info,
|
|
1173
|
+
"wallet-delete": cmd_wallet_delete,
|
|
1174
|
+
"wallet-rename": cmd_wallet_rename,
|
|
1175
|
+
"wallet-export": cmd_wallet_export,
|
|
1176
|
+
"balance": cmd_balance,
|
|
1177
|
+
"transfer": cmd_transfer,
|
|
1178
|
+
"tx-info": cmd_tx_info,
|
|
1179
|
+
"tx-history": cmd_tx_history,
|
|
1180
|
+
"chain-info": cmd_chain_info,
|
|
1181
|
+
"block-info": cmd_block_info,
|
|
1182
|
+
"claim-airdrop": cmd_claim_airdrop
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
handler = commands.get(args.command)
|
|
1186
|
+
if handler:
|
|
1187
|
+
handler(args)
|
|
1188
|
+
else:
|
|
1189
|
+
parser.print_help()
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
if __name__ == "__main__":
|
|
1193
|
+
main()
|