lunalib 1.5.1__py3-none-any.whl
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.
Potentially problematic release.
This version of lunalib might be problematic. Click here for more details.
- core/__init__.py +0 -0
- core/blockchain.py +172 -0
- core/crypto.py +32 -0
- core/wallet.py +408 -0
- gtx/__init__.py +0 -0
- gtx/bill_registry.py +122 -0
- gtx/digital_bill.py +273 -0
- gtx/genesis.py +338 -0
- lunalib/__init__.py +21 -0
- lunalib/cli.py +18 -0
- lunalib/core/__init__.py +0 -0
- lunalib/core/blockchain.py +803 -0
- lunalib/core/crypto.py +270 -0
- lunalib/core/mempool.py +342 -0
- lunalib/core/sm2.py +723 -0
- lunalib/core/wallet.py +1342 -0
- lunalib/core/wallet_manager.py +638 -0
- lunalib/core/wallet_sync_helper.py +163 -0
- lunalib/gtx/__init__.py +0 -0
- lunalib/gtx/bill_registry.py +122 -0
- lunalib/gtx/digital_bill.py +273 -0
- lunalib/gtx/genesis.py +349 -0
- lunalib/luna_lib.py +87 -0
- lunalib/mining/__init__.py +0 -0
- lunalib/mining/cuda_manager.py +137 -0
- lunalib/mining/difficulty.py +106 -0
- lunalib/mining/miner.py +617 -0
- lunalib/requirements.txt +44 -0
- lunalib/storage/__init__.py +0 -0
- lunalib/storage/cache.py +148 -0
- lunalib/storage/database.py +222 -0
- lunalib/storage/encryption.py +105 -0
- lunalib/transactions/__init__.py +0 -0
- lunalib/transactions/security.py +234 -0
- lunalib/transactions/transactions.py +399 -0
- lunalib/transactions/validator.py +71 -0
- lunalib-1.5.1.dist-info/METADATA +283 -0
- lunalib-1.5.1.dist-info/RECORD +53 -0
- lunalib-1.5.1.dist-info/WHEEL +5 -0
- lunalib-1.5.1.dist-info/entry_points.txt +2 -0
- lunalib-1.5.1.dist-info/top_level.txt +6 -0
- mining/__init__.py +0 -0
- mining/cuda_manager.py +137 -0
- mining/difficulty.py +106 -0
- mining/miner.py +107 -0
- storage/__init__.py +0 -0
- storage/cache.py +148 -0
- storage/database.py +222 -0
- storage/encryption.py +105 -0
- transactions/__init__.py +0 -0
- transactions/security.py +172 -0
- transactions/transactions.py +424 -0
- transactions/validator.py +71 -0
core/__init__.py
ADDED
|
File without changes
|
core/blockchain.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from storage.cache import BlockchainCache
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BlockchainManager:
|
|
9
|
+
"""Manages blockchain interactions and scanning"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, endpoint_url="https://bank.linglin.art"):
|
|
12
|
+
self.endpoint_url = endpoint_url.rstrip('/')
|
|
13
|
+
self.cache = BlockchainCache()
|
|
14
|
+
self.network_connected = False
|
|
15
|
+
|
|
16
|
+
def get_blockchain_height(self) -> int:
|
|
17
|
+
"""Get current blockchain height"""
|
|
18
|
+
try:
|
|
19
|
+
# Try height endpoint first
|
|
20
|
+
response = requests.get(f'{self.endpoint_url}/blockchain/height', timeout=10)
|
|
21
|
+
if response.status_code == 200:
|
|
22
|
+
data = response.json()
|
|
23
|
+
return data.get('height', 0)
|
|
24
|
+
|
|
25
|
+
# Fallback to blocks endpoint
|
|
26
|
+
response = requests.get(f'{self.endpoint_url}/blockchain/blocks', timeout=10)
|
|
27
|
+
if response.status_code == 200:
|
|
28
|
+
data = response.json()
|
|
29
|
+
blocks = data.get('blocks', [])
|
|
30
|
+
return len(blocks)
|
|
31
|
+
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(f"Blockchain height error: {e}")
|
|
34
|
+
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
def get_block(self, height: int) -> Optional[Dict]:
|
|
38
|
+
"""Get block by height"""
|
|
39
|
+
# Check cache first
|
|
40
|
+
cached_block = self.cache.get_block(height)
|
|
41
|
+
if cached_block:
|
|
42
|
+
return cached_block
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
response = requests.get(f'{self.endpoint_url}/blockchain/block/{height}', timeout=10)
|
|
46
|
+
if response.status_code == 200:
|
|
47
|
+
block = response.json()
|
|
48
|
+
self.cache.save_block(height, block.get('hash', ''), block)
|
|
49
|
+
return block
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Get block error: {e}")
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def get_blocks_range(self, start_height: int, end_height: int) -> List[Dict]:
|
|
56
|
+
"""Get range of blocks"""
|
|
57
|
+
blocks = []
|
|
58
|
+
|
|
59
|
+
# Check cache first
|
|
60
|
+
cached_blocks = self.cache.get_block_range(start_height, end_height)
|
|
61
|
+
if len(cached_blocks) == (end_height - start_height + 1):
|
|
62
|
+
return cached_blocks
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response = requests.get(
|
|
66
|
+
f'{self.endpoint_url}/blockchain/range?start={start_height}&end={end_height}',
|
|
67
|
+
timeout=30
|
|
68
|
+
)
|
|
69
|
+
if response.status_code == 200:
|
|
70
|
+
blocks = response.json().get('blocks', [])
|
|
71
|
+
# Cache the blocks
|
|
72
|
+
for block in blocks:
|
|
73
|
+
height = block.get('index', 0)
|
|
74
|
+
self.cache.save_block(height, block.get('hash', ''), block)
|
|
75
|
+
else:
|
|
76
|
+
# Fallback: get blocks individually
|
|
77
|
+
for height in range(start_height, end_height + 1):
|
|
78
|
+
block = self.get_block(height)
|
|
79
|
+
if block:
|
|
80
|
+
blocks.append(block)
|
|
81
|
+
time.sleep(0.01) # Be nice to the API
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"Get blocks range error: {e}")
|
|
85
|
+
|
|
86
|
+
return blocks
|
|
87
|
+
|
|
88
|
+
def get_mempool(self) -> List[Dict]:
|
|
89
|
+
"""Get current mempool transactions"""
|
|
90
|
+
try:
|
|
91
|
+
response = requests.get(f'{self.endpoint_url}/mempool', timeout=10)
|
|
92
|
+
if response.status_code == 200:
|
|
93
|
+
return response.json()
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Mempool error: {e}")
|
|
96
|
+
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
def broadcast_transaction(self, transaction: Dict) -> bool:
|
|
100
|
+
"""Broadcast transaction to network"""
|
|
101
|
+
try:
|
|
102
|
+
response = requests.post(
|
|
103
|
+
f'{self.endpoint_url}/mempool/add',
|
|
104
|
+
json=transaction,
|
|
105
|
+
timeout=30
|
|
106
|
+
)
|
|
107
|
+
return response.status_code == 201
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"Broadcast error: {e}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def check_network_connection(self) -> bool:
|
|
113
|
+
"""Check if network is accessible"""
|
|
114
|
+
try:
|
|
115
|
+
response = requests.get(f'{self.endpoint_url}/health', timeout=5)
|
|
116
|
+
self.network_connected = response.status_code == 200
|
|
117
|
+
return self.network_connected
|
|
118
|
+
except:
|
|
119
|
+
self.network_connected = False
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def scan_transactions_for_address(self, address: str, start_height: int = 0, end_height: int = None) -> List[Dict]:
|
|
123
|
+
"""Scan blockchain for transactions involving an address"""
|
|
124
|
+
if end_height is None:
|
|
125
|
+
end_height = self.get_blockchain_height()
|
|
126
|
+
|
|
127
|
+
transactions = []
|
|
128
|
+
|
|
129
|
+
# Scan in batches for efficiency
|
|
130
|
+
batch_size = 100
|
|
131
|
+
for batch_start in range(start_height, end_height + 1, batch_size):
|
|
132
|
+
batch_end = min(batch_start + batch_size - 1, end_height)
|
|
133
|
+
blocks = self.get_blocks_range(batch_start, batch_end)
|
|
134
|
+
|
|
135
|
+
for block in blocks:
|
|
136
|
+
block_transactions = self._find_address_transactions(block, address)
|
|
137
|
+
transactions.extend(block_transactions)
|
|
138
|
+
|
|
139
|
+
return transactions
|
|
140
|
+
|
|
141
|
+
def _find_address_transactions(self, block: Dict, address: str) -> List[Dict]:
|
|
142
|
+
"""Find transactions in block that involve the address"""
|
|
143
|
+
transactions = []
|
|
144
|
+
address_lower = address.lower()
|
|
145
|
+
|
|
146
|
+
# Check block reward
|
|
147
|
+
miner = block.get('miner', '').lower()
|
|
148
|
+
if miner == address_lower:
|
|
149
|
+
reward_tx = {
|
|
150
|
+
'type': 'reward',
|
|
151
|
+
'from': 'network',
|
|
152
|
+
'to': address,
|
|
153
|
+
'amount': block.get('reward', 0),
|
|
154
|
+
'block_height': block.get('index'),
|
|
155
|
+
'timestamp': block.get('timestamp'),
|
|
156
|
+
'hash': f"reward_{block.get('index')}_{address}",
|
|
157
|
+
'status': 'confirmed'
|
|
158
|
+
}
|
|
159
|
+
transactions.append(reward_tx)
|
|
160
|
+
|
|
161
|
+
# Check regular transactions
|
|
162
|
+
for tx in block.get('transactions', []):
|
|
163
|
+
from_addr = (tx.get('from') or '').lower()
|
|
164
|
+
to_addr = (tx.get('to') or '').lower()
|
|
165
|
+
|
|
166
|
+
if from_addr == address_lower or to_addr == address_lower:
|
|
167
|
+
enhanced_tx = tx.copy()
|
|
168
|
+
enhanced_tx['block_height'] = block.get('index')
|
|
169
|
+
enhanced_tx['status'] = 'confirmed'
|
|
170
|
+
transactions.append(enhanced_tx)
|
|
171
|
+
|
|
172
|
+
return transactions
|
core/crypto.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import secrets
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
class KeyManager:
|
|
6
|
+
"""Manages cryptographic keys and signing"""
|
|
7
|
+
|
|
8
|
+
def generate_private_key(self):
|
|
9
|
+
"""Generate a new private key"""
|
|
10
|
+
return secrets.token_hex(32)
|
|
11
|
+
|
|
12
|
+
def derive_public_key(self, private_key):
|
|
13
|
+
"""Derive public key from private key"""
|
|
14
|
+
return hashlib.sha256(private_key.encode()).hexdigest()
|
|
15
|
+
|
|
16
|
+
def derive_address(self, public_key):
|
|
17
|
+
"""Derive address from public key"""
|
|
18
|
+
address_hash = hashlib.sha256(public_key.encode()).hexdigest()
|
|
19
|
+
return f"LUN_{address_hash[:16]}_{secrets.token_hex(4)}"
|
|
20
|
+
|
|
21
|
+
def sign_data(self, data, private_key):
|
|
22
|
+
"""Sign data with private key"""
|
|
23
|
+
# In production, use proper ECDSA
|
|
24
|
+
# For now, simplified implementation
|
|
25
|
+
sign_string = data + private_key
|
|
26
|
+
return hashlib.sha256(sign_string.encode()).hexdigest()
|
|
27
|
+
|
|
28
|
+
def verify_signature(self, data, signature, public_key):
|
|
29
|
+
"""Verify signature with public key"""
|
|
30
|
+
# Simplified verification
|
|
31
|
+
expected_public = self.derive_public_key(public_key)
|
|
32
|
+
return public_key == expected_public
|
core/wallet.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# wallet.py
|
|
2
|
+
import time
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from cryptography.fernet import Fernet
|
|
6
|
+
import base64
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
class LunaWallet:
|
|
10
|
+
"""Luna wallet implementation with proper key management"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, data_dir=None):
|
|
13
|
+
self.data_dir = data_dir
|
|
14
|
+
self.wallets = {} # Main wallet storage: {address: wallet_data}
|
|
15
|
+
self.current_wallet_address = None # Track which wallet is active
|
|
16
|
+
|
|
17
|
+
# Initialize with an empty current wallet state
|
|
18
|
+
self._reset_current_wallet()
|
|
19
|
+
|
|
20
|
+
def _reset_current_wallet(self):
|
|
21
|
+
"""Reset current wallet to empty state"""
|
|
22
|
+
self.address = None
|
|
23
|
+
self.balance = 0.0
|
|
24
|
+
self.available_balance = 0.0
|
|
25
|
+
self.created = time.time()
|
|
26
|
+
self.private_key = None
|
|
27
|
+
self.public_key = None
|
|
28
|
+
self.encrypted_private_key = None
|
|
29
|
+
self.label = "New Wallet"
|
|
30
|
+
self.is_locked = True
|
|
31
|
+
self.pending_transactions = {} # {tx_hash: amount}
|
|
32
|
+
|
|
33
|
+
def _generate_address(self):
|
|
34
|
+
"""Generate wallet address"""
|
|
35
|
+
base_data = f"LUN_{int(time.time()*1000)}_{hashlib.sha256(str(time.time()).encode()).hexdigest()[:8]}"
|
|
36
|
+
return hashlib.sha256(base_data.encode()).hexdigest()[:32]
|
|
37
|
+
|
|
38
|
+
def _generate_private_key(self):
|
|
39
|
+
"""Generate private key"""
|
|
40
|
+
return f"priv_{hashlib.sha256(str(time.time()).encode()).hexdigest()}"
|
|
41
|
+
|
|
42
|
+
def _derive_public_key(self, private_key=None):
|
|
43
|
+
"""Derive public key from private key"""
|
|
44
|
+
priv_key = private_key or self.private_key
|
|
45
|
+
if not priv_key:
|
|
46
|
+
return None
|
|
47
|
+
return f"pub_{priv_key[-16:]}"
|
|
48
|
+
|
|
49
|
+
def get_wallet_info(self):
|
|
50
|
+
"""Get complete wallet information for current wallet"""
|
|
51
|
+
if not self.address:
|
|
52
|
+
return None
|
|
53
|
+
return {
|
|
54
|
+
'address': self.address,
|
|
55
|
+
'balance': self.balance,
|
|
56
|
+
'available_balance': self.available_balance,
|
|
57
|
+
'pending_amount': self.get_pending_amount(),
|
|
58
|
+
'created': self.created,
|
|
59
|
+
'private_key': self.private_key,
|
|
60
|
+
'public_key': self.public_key,
|
|
61
|
+
'encrypted_private_key': self.encrypted_private_key,
|
|
62
|
+
'label': self.label,
|
|
63
|
+
'is_locked': self.is_locked
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def create_new_wallet(self, name, password):
|
|
67
|
+
"""Create a new wallet and add to collection without switching"""
|
|
68
|
+
# Generate new wallet data
|
|
69
|
+
address = self._generate_address()
|
|
70
|
+
private_key = self._generate_private_key()
|
|
71
|
+
public_key = f"pub_{private_key[-16:]}"
|
|
72
|
+
|
|
73
|
+
# Encrypt private key
|
|
74
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
75
|
+
fernet = Fernet(key)
|
|
76
|
+
encrypted_private_key = fernet.encrypt(private_key.encode())
|
|
77
|
+
|
|
78
|
+
# Create new wallet data
|
|
79
|
+
new_wallet_data = {
|
|
80
|
+
'address': address,
|
|
81
|
+
'balance': 0.0,
|
|
82
|
+
'available_balance': 0.0,
|
|
83
|
+
'created': time.time(),
|
|
84
|
+
'private_key': private_key,
|
|
85
|
+
'public_key': public_key,
|
|
86
|
+
'encrypted_private_key': encrypted_private_key,
|
|
87
|
+
'label': name,
|
|
88
|
+
'is_locked': True,
|
|
89
|
+
'pending_transactions': {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# CRITICAL: Add to wallets collection
|
|
93
|
+
self.wallets[address] = new_wallet_data
|
|
94
|
+
|
|
95
|
+
print(f"DEBUG: Created new wallet {address}, total wallets: {len(self.wallets)}")
|
|
96
|
+
|
|
97
|
+
return new_wallet_data
|
|
98
|
+
def create_wallet(self, name, password):
|
|
99
|
+
"""Create a new wallet and set it as current"""
|
|
100
|
+
# Generate new wallet data
|
|
101
|
+
address = self._generate_address()
|
|
102
|
+
private_key = self._generate_private_key()
|
|
103
|
+
public_key = f"pub_{private_key[-16:]}"
|
|
104
|
+
|
|
105
|
+
# Encrypt private key
|
|
106
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
107
|
+
fernet = Fernet(key)
|
|
108
|
+
encrypted_private_key = fernet.encrypt(private_key.encode())
|
|
109
|
+
|
|
110
|
+
# Create wallet data
|
|
111
|
+
wallet_data = {
|
|
112
|
+
'address': address,
|
|
113
|
+
'balance': 0.0,
|
|
114
|
+
'available_balance': 0.0,
|
|
115
|
+
'created': time.time(),
|
|
116
|
+
'private_key': private_key,
|
|
117
|
+
'public_key': public_key,
|
|
118
|
+
'encrypted_private_key': encrypted_private_key,
|
|
119
|
+
'label': name,
|
|
120
|
+
'is_locked': True,
|
|
121
|
+
'pending_transactions': {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# CRITICAL: Add to wallets collection
|
|
125
|
+
self.wallets[address] = wallet_data
|
|
126
|
+
|
|
127
|
+
# Set as current wallet
|
|
128
|
+
self._set_current_wallet(wallet_data)
|
|
129
|
+
|
|
130
|
+
print(f"DEBUG: Created and switched to wallet {address}, total wallets: {len(self.wallets)}")
|
|
131
|
+
|
|
132
|
+
return wallet_data
|
|
133
|
+
def _set_current_wallet(self, wallet_data):
|
|
134
|
+
"""Set the current wallet from wallet data"""
|
|
135
|
+
self.current_wallet_address = wallet_data['address']
|
|
136
|
+
self.address = wallet_data['address']
|
|
137
|
+
self.balance = wallet_data['balance']
|
|
138
|
+
self.pending_transactions = wallet_data.get('pending_transactions', {})
|
|
139
|
+
self._recalculate_available_balance()
|
|
140
|
+
self.created = wallet_data['created']
|
|
141
|
+
self.private_key = wallet_data['private_key']
|
|
142
|
+
self.public_key = wallet_data['public_key']
|
|
143
|
+
self.encrypted_private_key = wallet_data['encrypted_private_key']
|
|
144
|
+
self.label = wallet_data['label']
|
|
145
|
+
self.is_locked = wallet_data.get('is_locked', True)
|
|
146
|
+
|
|
147
|
+
def switch_wallet(self, address, password=None):
|
|
148
|
+
"""Switch to a different wallet in the collection"""
|
|
149
|
+
if address in self.wallets:
|
|
150
|
+
wallet_data = self.wallets[address]
|
|
151
|
+
self._set_current_wallet(wallet_data)
|
|
152
|
+
|
|
153
|
+
# If password provided, unlock the wallet
|
|
154
|
+
if password:
|
|
155
|
+
return self.unlock_wallet(address, password)
|
|
156
|
+
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def unlock_wallet(self, address, password):
|
|
161
|
+
"""Unlock wallet with password"""
|
|
162
|
+
if address not in self.wallets:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
wallet_data = self.wallets[address]
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
if wallet_data.get('encrypted_private_key'):
|
|
169
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
170
|
+
fernet = Fernet(key)
|
|
171
|
+
decrypted_key = fernet.decrypt(wallet_data['encrypted_private_key'])
|
|
172
|
+
wallet_data['private_key'] = decrypted_key.decode()
|
|
173
|
+
wallet_data['is_locked'] = False
|
|
174
|
+
|
|
175
|
+
# If this is the current wallet, update current state
|
|
176
|
+
if self.current_wallet_address == address:
|
|
177
|
+
self.private_key = wallet_data['private_key']
|
|
178
|
+
self.is_locked = False
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
except:
|
|
182
|
+
pass
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def is_unlocked(self):
|
|
187
|
+
"""Check if current wallet is unlocked"""
|
|
188
|
+
if not self.current_wallet_address:
|
|
189
|
+
return False
|
|
190
|
+
wallet_data = self.wallets.get(self.current_wallet_address, {})
|
|
191
|
+
return not wallet_data.get('is_locked', True)
|
|
192
|
+
|
|
193
|
+
def export_private_key(self, address, password):
|
|
194
|
+
"""Export private key with password decryption"""
|
|
195
|
+
if address not in self.wallets:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
wallet_data = self.wallets[address]
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if wallet_data.get('encrypted_private_key'):
|
|
202
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
203
|
+
fernet = Fernet(key)
|
|
204
|
+
decrypted_key = fernet.decrypt(wallet_data['encrypted_private_key'])
|
|
205
|
+
return decrypted_key.decode()
|
|
206
|
+
except:
|
|
207
|
+
pass
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def import_wallet(self, wallet_data, password=None):
|
|
211
|
+
"""Import wallet from data"""
|
|
212
|
+
if isinstance(wallet_data, dict):
|
|
213
|
+
address = wallet_data.get('address')
|
|
214
|
+
if not address:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# Ensure pending_transactions exists
|
|
218
|
+
if 'pending_transactions' not in wallet_data:
|
|
219
|
+
wallet_data['pending_transactions'] = {}
|
|
220
|
+
|
|
221
|
+
# Add to wallets collection
|
|
222
|
+
self.wallets[address] = wallet_data.copy()
|
|
223
|
+
|
|
224
|
+
# Set as current wallet
|
|
225
|
+
self._set_current_wallet(wallet_data)
|
|
226
|
+
|
|
227
|
+
if password and wallet_data.get('encrypted_private_key'):
|
|
228
|
+
return self.unlock_wallet(address, password)
|
|
229
|
+
|
|
230
|
+
return True
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
def update_balance(self, new_balance):
|
|
234
|
+
"""Update current wallet balance"""
|
|
235
|
+
self.balance = float(new_balance)
|
|
236
|
+
self._recalculate_available_balance()
|
|
237
|
+
|
|
238
|
+
# Also update in wallets collection
|
|
239
|
+
if self.current_wallet_address and self.current_wallet_address in self.wallets:
|
|
240
|
+
self.wallets[self.current_wallet_address]['balance'] = self.balance
|
|
241
|
+
self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
|
|
242
|
+
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
def _recalculate_available_balance(self):
|
|
246
|
+
"""Recalculate available balance based on pending transactions"""
|
|
247
|
+
pending_amount = sum(self.pending_transactions.values())
|
|
248
|
+
self.available_balance = max(0.0, self.balance - pending_amount)
|
|
249
|
+
|
|
250
|
+
def add_pending_transaction(self, tx_hash: str, amount: float) -> bool:
|
|
251
|
+
"""Add a transaction to pending list (e.g., when sent to mempool)"""
|
|
252
|
+
if amount > self.available_balance:
|
|
253
|
+
return False
|
|
254
|
+
self.pending_transactions[tx_hash] = float(amount)
|
|
255
|
+
self._recalculate_available_balance()
|
|
256
|
+
|
|
257
|
+
# Update in wallets collection
|
|
258
|
+
if self.current_wallet_address and self.current_wallet_address in self.wallets:
|
|
259
|
+
self.wallets[self.current_wallet_address]['pending_transactions'] = self.pending_transactions
|
|
260
|
+
self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
|
|
261
|
+
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
def confirm_pending_transaction(self, tx_hash: str) -> bool:
|
|
265
|
+
"""Confirm a pending transaction (e.g., added to blockchain)"""
|
|
266
|
+
if tx_hash in self.pending_transactions:
|
|
267
|
+
del self.pending_transactions[tx_hash]
|
|
268
|
+
self._recalculate_available_balance()
|
|
269
|
+
|
|
270
|
+
# Update in wallets collection
|
|
271
|
+
if self.current_wallet_address and self.current_wallet_address in self.wallets:
|
|
272
|
+
self.wallets[self.current_wallet_address]['pending_transactions'] = self.pending_transactions
|
|
273
|
+
self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
|
|
274
|
+
|
|
275
|
+
return True
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def get_pending_amount(self) -> float:
|
|
279
|
+
"""Get total amount in pending transactions"""
|
|
280
|
+
return sum(self.pending_transactions.values())
|
|
281
|
+
|
|
282
|
+
def get_pending_transactions(self) -> dict:
|
|
283
|
+
"""Get all pending transactions"""
|
|
284
|
+
return self.pending_transactions.copy()
|
|
285
|
+
|
|
286
|
+
def clear_pending_transaction(self, tx_hash: str) -> bool:
|
|
287
|
+
"""Remove a pending transaction (e.g., if it failed)"""
|
|
288
|
+
return self.confirm_pending_transaction(tx_hash)
|
|
289
|
+
|
|
290
|
+
def get_balance(self):
|
|
291
|
+
"""Get current wallet balance"""
|
|
292
|
+
return self.balance
|
|
293
|
+
|
|
294
|
+
def get_wallet_by_address(self, address):
|
|
295
|
+
"""Get wallet by address from wallets collection"""
|
|
296
|
+
return self.wallets.get(address)
|
|
297
|
+
|
|
298
|
+
def list_wallets(self):
|
|
299
|
+
"""List all wallets in collection"""
|
|
300
|
+
return list(self.wallets.keys())
|
|
301
|
+
|
|
302
|
+
def get_current_wallet_info(self):
|
|
303
|
+
"""Get current wallet information"""
|
|
304
|
+
if not self.current_wallet_address:
|
|
305
|
+
return None
|
|
306
|
+
return self.wallets.get(self.current_wallet_address)
|
|
307
|
+
|
|
308
|
+
def save_to_file(self, filename=None):
|
|
309
|
+
"""Save wallet to file"""
|
|
310
|
+
if not self.data_dir:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
if filename is None:
|
|
314
|
+
filename = f"wallet_{self.address}.json"
|
|
315
|
+
|
|
316
|
+
filepath = os.path.join(self.data_dir, filename)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
# Ensure directory exists
|
|
320
|
+
os.makedirs(self.data_dir, exist_ok=True)
|
|
321
|
+
|
|
322
|
+
# Prepare encrypted private key for serialization
|
|
323
|
+
encrypted_key_data = None
|
|
324
|
+
if self.encrypted_private_key:
|
|
325
|
+
# Ensure it's bytes before encoding
|
|
326
|
+
if isinstance(self.encrypted_private_key, bytes):
|
|
327
|
+
encrypted_key_data = base64.b64encode(self.encrypted_private_key).decode('utf-8')
|
|
328
|
+
else:
|
|
329
|
+
encrypted_key_data = base64.b64encode(self.encrypted_private_key.encode()).decode('utf-8')
|
|
330
|
+
|
|
331
|
+
# Prepare wallets for serialization (remove any non-serializable data)
|
|
332
|
+
serializable_wallets = {}
|
|
333
|
+
for addr, wallet_info in self.wallets.items():
|
|
334
|
+
serializable_wallet = wallet_info.copy()
|
|
335
|
+
# Ensure encrypted_private_key is serializable
|
|
336
|
+
if serializable_wallet.get('encrypted_private_key') and isinstance(serializable_wallet['encrypted_private_key'], bytes):
|
|
337
|
+
serializable_wallet['encrypted_private_key'] = base64.b64encode(
|
|
338
|
+
serializable_wallet['encrypted_private_key']
|
|
339
|
+
).decode('utf-8')
|
|
340
|
+
serializable_wallets[addr] = serializable_wallet
|
|
341
|
+
|
|
342
|
+
wallet_data = {
|
|
343
|
+
'address': self.address,
|
|
344
|
+
'balance': self.balance,
|
|
345
|
+
'available_balance': self.available_balance,
|
|
346
|
+
'pending_amount': self.get_pending_amount(),
|
|
347
|
+
'pending_transactions': self.pending_transactions,
|
|
348
|
+
'created': self.created,
|
|
349
|
+
'public_key': self.public_key,
|
|
350
|
+
'encrypted_private_key': encrypted_key_data,
|
|
351
|
+
'label': self.label,
|
|
352
|
+
'is_locked': self.is_locked,
|
|
353
|
+
'wallets': serializable_wallets,
|
|
354
|
+
'current_wallet_address': self.current_wallet_address
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
with open(filepath, 'w') as f:
|
|
358
|
+
json.dump(wallet_data, f, indent=2)
|
|
359
|
+
return True
|
|
360
|
+
except Exception as e:
|
|
361
|
+
print(f"Error saving wallet: {e}")
|
|
362
|
+
import traceback
|
|
363
|
+
traceback.print_exc()
|
|
364
|
+
return False
|
|
365
|
+
|
|
366
|
+
def load_from_file(self, filename, password=None):
|
|
367
|
+
"""Load wallet from file"""
|
|
368
|
+
if not self.data_dir:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
filepath = os.path.join(self.data_dir, filename)
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
with open(filepath, 'r') as f:
|
|
375
|
+
wallet_data = json.load(f)
|
|
376
|
+
|
|
377
|
+
# Load wallets collection
|
|
378
|
+
self.wallets = wallet_data.get('wallets', {})
|
|
379
|
+
|
|
380
|
+
# Ensure all wallets have pending_transactions field
|
|
381
|
+
for addr, w_data in self.wallets.items():
|
|
382
|
+
if 'pending_transactions' not in w_data:
|
|
383
|
+
w_data['pending_transactions'] = {}
|
|
384
|
+
|
|
385
|
+
# Load current wallet address
|
|
386
|
+
self.current_wallet_address = wallet_data.get('current_wallet_address')
|
|
387
|
+
|
|
388
|
+
# If we have a current wallet, load its data
|
|
389
|
+
if self.current_wallet_address and self.current_wallet_address in self.wallets:
|
|
390
|
+
current_wallet_data = self.wallets[self.current_wallet_address]
|
|
391
|
+
self._set_current_wallet(current_wallet_data)
|
|
392
|
+
|
|
393
|
+
# Handle encrypted private key
|
|
394
|
+
encrypted_key = wallet_data.get('encrypted_private_key')
|
|
395
|
+
if encrypted_key:
|
|
396
|
+
self.encrypted_private_key = base64.b64decode(encrypted_key.encode())
|
|
397
|
+
# Also update in wallets collection
|
|
398
|
+
if self.current_wallet_address in self.wallets:
|
|
399
|
+
self.wallets[self.current_wallet_address]['encrypted_private_key'] = self.encrypted_private_key
|
|
400
|
+
|
|
401
|
+
# If password provided and we have encrypted key, unlock
|
|
402
|
+
if password and self.encrypted_private_key and self.current_wallet_address:
|
|
403
|
+
return self.unlock_wallet(self.current_wallet_address, password)
|
|
404
|
+
|
|
405
|
+
return True
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(f"Error loading wallet: {e}")
|
|
408
|
+
return False
|
gtx/__init__.py
ADDED
|
File without changes
|