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.
@@ -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()