lunalib 1.6.7__py3-none-any.whl → 1.7.9__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.
Files changed (54) hide show
  1. lunalib/.gitignore +3 -0
  2. lunalib/__pycache__/__init__.cpython-310.pyc +0 -0
  3. lunalib/core/__pycache__/__init__.cpython-310.pyc +0 -0
  4. lunalib/core/__pycache__/blockchain.cpython-310.pyc +0 -0
  5. lunalib/core/__pycache__/crypto.cpython-310.pyc +0 -0
  6. lunalib/core/__pycache__/mempool.cpython-310.pyc +0 -0
  7. lunalib/core/__pycache__/wallet.cpython-310.pyc +0 -0
  8. lunalib/core/blockchain.py +182 -2
  9. lunalib/core/daemon.py +108 -5
  10. lunalib/core/mempool.py +1 -0
  11. lunalib/core/wallet.py +856 -492
  12. lunalib/core/wallet_db.py +68 -0
  13. lunalib/gtx/__pycache__/__init__.cpython-310.pyc +0 -0
  14. lunalib/gtx/__pycache__/bill_registry.cpython-310.pyc +0 -0
  15. lunalib/gtx/__pycache__/digital_bill.cpython-310.pyc +0 -0
  16. lunalib/gtx/__pycache__/genesis.cpython-310.pyc +0 -0
  17. lunalib/mining/__pycache__/__init__.cpython-310.pyc +0 -0
  18. lunalib/mining/__pycache__/cuda_manager.cpython-310.pyc +0 -0
  19. lunalib/mining/__pycache__/difficulty.cpython-310.pyc +0 -0
  20. lunalib/mining/__pycache__/miner.cpython-310.pyc +0 -0
  21. lunalib/mining/miner.py +33 -14
  22. lunalib/storage/__pycache__/__init__.cpython-310.pyc +0 -0
  23. lunalib/storage/__pycache__/cache.cpython-310.pyc +0 -0
  24. lunalib/storage/__pycache__/database.cpython-310.pyc +0 -0
  25. lunalib/storage/__pycache__/encryption.cpython-310.pyc +0 -0
  26. lunalib/tests/__pycache__/conftest.cpython-310-pytest-9.0.1.pyc +0 -0
  27. lunalib/tests/__pycache__/test_blockchain.cpython-310-pytest-9.0.1.pyc +0 -0
  28. lunalib/tests/__pycache__/test_crypto.cpython-310-pytest-9.0.1.pyc +0 -0
  29. lunalib/tests/__pycache__/test_gtx.cpython-310-pytest-9.0.1.pyc +0 -0
  30. lunalib/tests/__pycache__/test_mining.cpython-310-pytest-9.0.1.pyc +0 -0
  31. lunalib/tests/__pycache__/test_storage.cpython-310-pytest-9.0.1.pyc +0 -0
  32. lunalib/tests/__pycache__/test_transactions.cpython-310-pytest-9.0.1.pyc +0 -0
  33. lunalib/tests/__pycache__/test_wallet.cpython-310-pytest-9.0.1.pyc +0 -0
  34. lunalib/tests/conftest.py +41 -0
  35. lunalib/tests/init.py +0 -0
  36. lunalib/tests/integration/__pycache__/test_integration.cpython-310-pytest-9.0.1.pyc +0 -0
  37. lunalib/tests/integration/test_integration.py +62 -0
  38. lunalib/tests/test_blockchain.py +34 -0
  39. lunalib/tests/test_crypto.py +42 -0
  40. lunalib/tests/test_gtx.py +135 -0
  41. lunalib/tests/test_mining.py +244 -0
  42. lunalib/tests/test_security_suite.py +832 -0
  43. lunalib/tests/test_storage.py +84 -0
  44. lunalib/tests/test_transactions.py +103 -0
  45. lunalib/tests/test_wallet.py +91 -0
  46. lunalib/transactions/__pycache__/__init__.cpython-310.pyc +0 -0
  47. lunalib/transactions/__pycache__/security.cpython-310.pyc +0 -0
  48. lunalib/transactions/__pycache__/transactions.cpython-310.pyc +0 -0
  49. lunalib/transactions/__pycache__/validator.cpython-310.pyc +0 -0
  50. {lunalib-1.6.7.dist-info → lunalib-1.7.9.dist-info}/METADATA +2 -1
  51. lunalib-1.7.9.dist-info/RECORD +77 -0
  52. lunalib-1.6.7.dist-info/RECORD +0 -33
  53. {lunalib-1.6.7.dist-info → lunalib-1.7.9.dist-info}/WHEEL +0 -0
  54. {lunalib-1.6.7.dist-info → lunalib-1.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,68 @@
1
+
2
+ from lunalib.storage.database import WalletDatabase
3
+
4
+
5
+ class WalletDB:
6
+ def __init__(self, data_dir=None):
7
+ # data_dir is kept for compatibility, but db_path is passed to WalletDatabase
8
+ import os
9
+ self.data_dir = data_dir or os.path.expanduser("~/.lunawallet")
10
+ os.makedirs(self.data_dir, exist_ok=True)
11
+ self.db_path = os.path.join(self.data_dir, "wallets.db")
12
+ print(f"[WalletDB] data_dir: {self.data_dir}")
13
+ print(f"[WalletDB] db_path: {self.db_path}")
14
+ self.db = WalletDatabase(db_path=self.db_path)
15
+
16
+ def save_wallet(self, address, label, public_key, encrypted_private_key, is_locked, created, balance, available_balance):
17
+ # Compose wallet_data dict for WalletDatabase
18
+ wallet_data = {
19
+ 'address': address,
20
+ 'label': label,
21
+ 'public_key': public_key,
22
+ 'encrypted_private_key': encrypted_private_key,
23
+ 'balance': balance,
24
+ 'created': created,
25
+ 'metadata': {'is_locked': bool(is_locked), 'available_balance': available_balance}
26
+ }
27
+ return self.db.save_wallet(wallet_data)
28
+
29
+ def load_wallet(self, address):
30
+ w = self.db.load_wallet(address)
31
+ if w:
32
+ # For compatibility, flatten metadata fields
33
+ meta = w.get('metadata', {})
34
+ w['is_locked'] = meta.get('is_locked', False)
35
+ w['available_balance'] = meta.get('available_balance', 0.0)
36
+ return w
37
+
38
+ def list_wallets(self):
39
+ # Return list of tuples for compatibility: (address, label, is_locked, balance, available_balance)
40
+ import sqlite3
41
+ conn = sqlite3.connect(self.db_path)
42
+ cursor = conn.cursor()
43
+ cursor.execute('SELECT address, label, metadata, balance FROM wallets')
44
+ rows = cursor.fetchall()
45
+ conn.close()
46
+ result = []
47
+ for row in rows:
48
+ address, label, metadata_json, balance = row
49
+ meta = {}
50
+ try:
51
+ meta = json.loads(metadata_json) if metadata_json else {}
52
+ except Exception:
53
+ pass
54
+ is_locked = meta.get('is_locked', False)
55
+ available_balance = meta.get('available_balance', 0.0)
56
+ result.append((address, label, is_locked, balance, available_balance))
57
+ return result
58
+
59
+
60
+ def close(self):
61
+ # No persistent connection to close in WalletDatabase
62
+ pass
63
+
64
+ # Example usage:
65
+ if __name__ == "__main__":
66
+ db = WalletDB()
67
+ print("Wallets:", db.list_wallets())
68
+ db.close()
lunalib/mining/miner.py CHANGED
@@ -687,16 +687,20 @@ class Miner:
687
687
  # Validate all transactions
688
688
  valid_transactions = self._validate_transactions(mempool)
689
689
 
690
- # If no valid transactions, create a reward transaction
691
- if not valid_transactions:
692
- reward_tx = self._create_empty_block_reward(new_index)
693
- valid_transactions = [reward_tx]
694
-
695
690
  # Calculate block difficulty based on transactions
696
691
  block_difficulty = self._calculate_block_difficulty(valid_transactions)
697
692
 
698
- # Calculate block reward using exponential difficulty system
699
- total_reward = self._calculate_exponential_block_reward(block_difficulty)
693
+ # Calculate block reward:
694
+ # - Empty blocks use LINEAR system: difficulty 1 = 1 LKC, difficulty 2 = 2 LKC, etc.
695
+ # - Blocks with transactions use EXPONENTIAL system: 10^(difficulty-1)
696
+ if not valid_transactions:
697
+ # Empty block: linear reward
698
+ total_reward = float(block_difficulty)
699
+ reward_tx = self._create_empty_block_reward(new_index, block_difficulty, total_reward)
700
+ valid_transactions = [reward_tx]
701
+ else:
702
+ # Regular block: exponential reward
703
+ total_reward = self._calculate_exponential_block_reward(block_difficulty)
700
704
 
701
705
  # Create block data
702
706
  block_data = {
@@ -843,17 +847,19 @@ class Miner:
843
847
  """
844
848
  return self.difficulty_system.calculate_block_reward(difficulty)
845
849
 
846
- def _create_empty_block_reward(self, block_index: int) -> Dict:
847
- """Create reward transaction for empty blocks"""
850
+ def _create_empty_block_reward(self, block_index: int, difficulty: int, reward_amount: float) -> Dict:
851
+ """Create reward transaction for empty blocks using LINEAR reward system (difficulty = reward)"""
848
852
  return {
849
853
  'type': 'reward',
850
854
  'from': 'network',
851
855
  'to': self.config.miner_address,
852
- 'amount': 1.0,
856
+ 'amount': reward_amount,
853
857
  'timestamp': time.time(),
854
858
  'block_height': block_index,
859
+ 'difficulty': difficulty,
855
860
  'hash': f"reward_{block_index}_{int(time.time())}",
856
- 'description': 'Empty block mining reward'
861
+ 'description': f'Empty block mining reward (Linear: Difficulty {difficulty} = {reward_amount} LKC)',
862
+ 'is_empty_block': True
857
863
  }
858
864
 
859
865
  def _cuda_mine(self, block_data: Dict, difficulty: int) -> Optional[Dict]:
@@ -1021,14 +1027,27 @@ class Miner:
1021
1027
  safe_print(f"⚠️ Could not validate previous hash: {e}")
1022
1028
 
1023
1029
  # Validate reward matches difficulty
1024
- expected_reward = self.difficulty_system.calculate_block_reward(difficulty)
1030
+ # Empty blocks use LINEAR system (difficulty = reward)
1031
+ # Regular blocks use EXPONENTIAL system (10^(difficulty-1))
1032
+ transactions = block.get('transactions', [])
1033
+ is_empty_block = len(transactions) == 1 and transactions[0].get('is_empty_block', False)
1034
+
1035
+ if is_empty_block:
1036
+ # Empty block: linear reward
1037
+ expected_reward = float(difficulty)
1038
+ else:
1039
+ # Regular block: exponential reward
1040
+ expected_reward = self.difficulty_system.calculate_block_reward(difficulty)
1041
+
1025
1042
  actual_reward = block.get('reward', 0)
1026
1043
 
1027
1044
  # Allow some tolerance for floating point comparison
1028
1045
  if abs(actual_reward - expected_reward) > 0.01:
1029
- return False, f"Reward mismatch: expected {expected_reward} LKC for difficulty {difficulty}, got {actual_reward} LKC"
1046
+ reward_type = "Linear" if is_empty_block else "Exponential"
1047
+ return False, f"Reward mismatch ({reward_type}): expected {expected_reward} LKC for difficulty {difficulty}, got {actual_reward} LKC"
1030
1048
 
1031
- safe_print(f"✅ Block validation passed: Hash meets difficulty {difficulty}, Reward: {actual_reward} LKC")
1049
+ reward_type = "Linear" if is_empty_block else "Exponential"
1050
+ safe_print(f"✅ Block validation passed ({reward_type}): Hash meets difficulty {difficulty}, Reward: {actual_reward} LKC")
1032
1051
  return True, ""
1033
1052
 
1034
1053
  def _clear_transactions_from_mempool(self, transactions: List[Dict]):
@@ -0,0 +1,41 @@
1
+ import pytest
2
+ import tempfile
3
+ import os
4
+ import time
5
+ from lunalib.core.wallet import LunaWallet
6
+ from lunalib.mining.miner import GenesisMiner
7
+ from lunalib.gtx.genesis import GTXGenesis
8
+
9
+ @pytest.fixture
10
+ def temp_dir():
11
+ """Create temporary directory for test data"""
12
+ with tempfile.TemporaryDirectory() as tmpdir:
13
+ yield tmpdir
14
+
15
+ @pytest.fixture
16
+ def test_wallet(temp_dir):
17
+ """Create a test wallet"""
18
+ wallet = LunaWallet(data_dir=temp_dir)
19
+ wallet_data = wallet.create_wallet("Test Wallet", "test_password")
20
+ return wallet, wallet_data
21
+
22
+ @pytest.fixture
23
+ def test_miner():
24
+ """Create a test miner"""
25
+ return GenesisMiner()
26
+
27
+ @pytest.fixture
28
+ def test_gtx(temp_dir):
29
+ """Create test GTX system"""
30
+ return GTXGenesis()
31
+
32
+ @pytest.fixture
33
+ def sample_transaction_data(test_wallet):
34
+ """Create sample transaction data"""
35
+ wallet, wallet_data = test_wallet
36
+ return {
37
+ "from": wallet_data["address"],
38
+ "to": "LUN_test_recipient_12345",
39
+ "amount": 100.0,
40
+ "memo": "Test transaction"
41
+ }
lunalib/tests/init.py ADDED
File without changes
@@ -0,0 +1,62 @@
1
+ import pytest
2
+ import tempfile
3
+ from lunalib.core.wallet import LunaWallet
4
+ from lunalib.mining.miner import GenesisMiner
5
+ from lunalib.gtx.genesis import GTXGenesis
6
+
7
+ class TestIntegration:
8
+ def test_complete_workflow(self, temp_dir):
9
+ """Test complete wallet -> mining -> GTX workflow"""
10
+ # Create wallet
11
+ wallet = LunaWallet(data_dir=temp_dir)
12
+ wallet_data = wallet.create_wallet("Integration Test", "test_pass")
13
+
14
+ # Initialize systems
15
+ miner = GenesisMiner()
16
+ gtx = GTXGenesis()
17
+
18
+ # Mock mining a bill
19
+ with pytest.MonkeyPatch().context() as m:
20
+ m.setattr(miner, '_perform_mining', lambda *args: {
21
+ "success": True,
22
+ "hash": "000integration",
23
+ "nonce": 999,
24
+ "mining_time": 1.0
25
+ })
26
+
27
+ # Mine a bill
28
+ bill = miner.mine_bill(1000, wallet_data['address'])
29
+
30
+ assert bill['success'] is True
31
+ assert bill['denomination'] == 1000
32
+ assert bill['luna_value'] == 1000
33
+
34
+ # Create proper bill data for verification
35
+ metadata_hash = "test_metadata_hash_12345"
36
+ signature = metadata_hash # This will pass METHOD 1 verification
37
+
38
+ # Register the bill in GTX system for verification
39
+ bill_info = {
40
+ 'bill_serial': bill['bill_serial'],
41
+ 'denomination': 1000,
42
+ 'user_address': wallet_data['address'],
43
+ 'hash': "000integration",
44
+ 'mining_time': 1.0,
45
+ 'difficulty': 4,
46
+ 'luna_value': 1000,
47
+ 'timestamp': bill['timestamp'],
48
+ 'bill_data': {
49
+ 'public_key': 'test_public_key',
50
+ 'signature': signature, # Use the signature that matches metadata_hash
51
+ 'metadata_hash': metadata_hash, # This must match signature for METHOD 1
52
+ 'issued_to': wallet_data['address'],
53
+ 'front_serial': bill['bill_serial'],
54
+ 'type': 'GTX_Genesis',
55
+ 'back_serial': '' # Add this to avoid errors
56
+ }
57
+ }
58
+ gtx.bill_registry.register_bill(bill_info)
59
+
60
+ # Verify the bill
61
+ verification = gtx.verify_bill(bill['bill_serial'])
62
+ assert verification['valid'] is True
@@ -0,0 +1,34 @@
1
+ import pytest
2
+ from unittest.mock import Mock, patch
3
+ from lunalib.core.blockchain import BlockchainManager
4
+
5
+ class TestBlockchain:
6
+ @patch('lunalib.core.blockchain.requests.get')
7
+ def test_blockchain_height(self, mock_get):
8
+ """Test blockchain height retrieval"""
9
+ mock_get.return_value.status_code = 200
10
+ mock_get.return_value.json.return_value = {'height': 1500}
11
+
12
+ blockchain = BlockchainManager()
13
+ height = blockchain.get_blockchain_height()
14
+
15
+ assert height == 1500
16
+
17
+ @patch('lunalib.core.blockchain.requests.get')
18
+ def test_network_connection(self, mock_get):
19
+ """Test network connection checking"""
20
+ mock_get.return_value.status_code = 200
21
+
22
+ blockchain = BlockchainManager()
23
+ is_connected = blockchain.check_network_connection()
24
+
25
+ assert is_connected is True
26
+
27
+ def test_transaction_scanning(self):
28
+ """Test transaction scanning functionality"""
29
+ blockchain = BlockchainManager()
30
+
31
+ # This would typically be mocked in a real test
32
+ # For now, test the method exists and returns expected type
33
+ transactions = blockchain.scan_transactions_for_address("LUN_test", 0, 10)
34
+ assert isinstance(transactions, list)
@@ -0,0 +1,42 @@
1
+ import pytest
2
+ from lunalib.core.crypto import KeyManager
3
+
4
+ class TestCrypto:
5
+ def test_key_generation(self):
6
+ """Test cryptographic key generation"""
7
+ key_manager = KeyManager()
8
+
9
+ private_key = key_manager.generate_private_key()
10
+ public_key = key_manager.derive_public_key(private_key)
11
+ address = key_manager.derive_address(public_key)
12
+
13
+ assert len(private_key) == 64 # 32 bytes in hex
14
+ assert len(public_key) == 64 # 32 bytes in hex
15
+ assert address.startswith("LUN_")
16
+
17
+ def test_data_signing(self):
18
+ """Test data signing and verification"""
19
+ key_manager = KeyManager()
20
+
21
+ private_key = key_manager.generate_private_key()
22
+ public_key = key_manager.derive_public_key(private_key)
23
+
24
+ test_data = "Hello, Luna Library!"
25
+ signature = key_manager.sign_data(test_data, private_key)
26
+
27
+ # Basic signature format check
28
+ assert len(signature) == 64
29
+ assert all(c in "0123456789abcdef" for c in signature.lower())
30
+
31
+ def test_address_generation_uniqueness(self):
32
+ """Test that addresses are unique"""
33
+ key_manager = KeyManager()
34
+
35
+ addresses = set()
36
+ for _ in range(10):
37
+ private_key = key_manager.generate_private_key()
38
+ public_key = key_manager.derive_public_key(private_key)
39
+ address = key_manager.derive_address(public_key)
40
+ addresses.add(address)
41
+
42
+ assert len(addresses) == 10 # All addresses should be unique
@@ -0,0 +1,135 @@
1
+ import sys
2
+ def safe_print(*args, **kwargs):
3
+ encoding = sys.stdout.encoding or 'utf-8'
4
+ try:
5
+ print(*args, **kwargs)
6
+ except UnicodeEncodeError:
7
+ print(*(str(a).encode(encoding, errors='replace').decode(encoding) for a in args), **kwargs)
8
+ import pytest
9
+ from lunalib.gtx.genesis import GTXGenesis
10
+ from lunalib.gtx.digital_bill import DigitalBill
11
+ import time
12
+ class TestGTXGenesis:
13
+ def test_digital_bill_creation(self, test_gtx):
14
+ """Test digital bill creation"""
15
+ bill = test_gtx.create_genesis_bill(
16
+ denomination=1000,
17
+ user_address="LUN_test_address_123",
18
+ custom_data={"issuer": "test"}
19
+ )
20
+
21
+ assert bill.denomination == 1000
22
+ assert bill.user_address == "LUN_test_address_123"
23
+ assert bill.bill_data["issuer"] == "test"
24
+ assert bill.bill_serial.startswith("GTX1000_")
25
+
26
+ def test_bill_mining_data(self, test_gtx):
27
+ """Test bill mining data generation"""
28
+ bill = test_gtx.create_genesis_bill(100, "LUN_test")
29
+ mining_data = bill.get_mining_data(nonce=123)
30
+
31
+ assert mining_data["type"] == "GTX_Genesis"
32
+ assert mining_data["denomination"] == 100
33
+ assert mining_data["nonce"] == 123
34
+ assert "previous_hash" in mining_data
35
+
36
+ def test_bill_finalization(self, test_gtx):
37
+ """Test bill finalization after mining"""
38
+ bill = test_gtx.create_genesis_bill(1000, "LUN_test")
39
+
40
+ final_bill = bill.finalize(
41
+ hash="000abc123",
42
+ nonce=12345,
43
+ mining_time=3.2
44
+ )
45
+
46
+ assert final_bill["success"] is True
47
+ assert final_bill["hash"] == "000abc123"
48
+ assert final_bill["mining_time"] == 3.2
49
+ assert "transaction_data" in final_bill
50
+
51
+ def test_bill_verification(self, test_gtx, temp_dir):
52
+ """Test bill verification with proper bill data structure"""
53
+ import time
54
+ import hashlib
55
+ from unittest.mock import patch
56
+
57
+ # Create a complete bill_info structure that matches what register_bill expects
58
+ bill_serial = "GTX_TEST_12345"
59
+ timestamp = time.time()
60
+ denomination = 100
61
+ user_address = "LUN_test_verify"
62
+
63
+ # Create metadata that will pass one of the verification methods
64
+ metadata_hash = hashlib.sha256(f"test_metadata_{timestamp}".encode()).hexdigest()
65
+ public_key = f"pub_test_key_{timestamp}"
66
+ signature = metadata_hash # This will pass METHOD 1 verification
67
+
68
+ # Create the complete bill_info structure
69
+ bill_info = {
70
+ 'bill_serial': bill_serial,
71
+ 'denomination': denomination,
72
+ 'user_address': user_address,
73
+ 'hash': hashlib.sha256(bill_serial.encode()).hexdigest(),
74
+ 'mining_time': 1.5,
75
+ 'difficulty': 4,
76
+ 'luna_value': denomination,
77
+ 'timestamp': timestamp,
78
+ 'bill_data': {
79
+ 'public_key': public_key,
80
+ 'signature': signature,
81
+ 'metadata_hash': metadata_hash,
82
+ 'issued_to': user_address,
83
+ 'front_serial': bill_serial,
84
+ 'type': 'GTX_Genesis',
85
+ 'back_serial': ''
86
+ }
87
+ }
88
+
89
+ # Register the bill first
90
+ test_gtx.bill_registry.register_bill(bill_info)
91
+
92
+ # DEBUG: Check what get_bill returns
93
+ bill_data_from_registry = test_gtx.bill_registry.get_bill(bill_serial)
94
+ safe_print(f"DEBUG: Bill data from registry: {bill_data_from_registry}")
95
+
96
+ # Test verification - should pass METHOD 1
97
+ result = test_gtx.verify_bill(bill_serial)
98
+ safe_print(f"DEBUG: Verification result: {result}")
99
+
100
+ assert result["valid"] is True
101
+
102
+ def test_invalid_bill_verification(self, test_gtx):
103
+ """Test verification of non-existent bill"""
104
+ result = test_gtx.verify_bill("GTX_invalid_serial")
105
+ assert result["valid"] is False
106
+ assert "error" in result
107
+
108
+ def test_portfolio_management(self, test_gtx, temp_dir):
109
+ """Test user portfolio functionality"""
110
+ # Use a unique user address to avoid conflicts
111
+ unique_user = f"LUN_test_user_{int(time.time())}"
112
+
113
+ # Add some test bills to registry
114
+ for denom in [100, 1000, 10000]:
115
+ bill = test_gtx.create_genesis_bill(denom, unique_user)
116
+ final_bill = bill.finalize(f"000hash{denom}", 123, 1.0)
117
+
118
+ # Register the bill properly
119
+ bill_info = {
120
+ 'bill_serial': final_bill['bill_serial'],
121
+ 'denomination': denom,
122
+ 'user_address': unique_user,
123
+ 'hash': f"000hash{denom}",
124
+ 'mining_time': 1.0,
125
+ 'difficulty': 4,
126
+ 'luna_value': denom,
127
+ 'timestamp': final_bill['timestamp'],
128
+ 'bill_data': final_bill
129
+ }
130
+ test_gtx.bill_registry.register_bill(bill_info)
131
+
132
+ portfolio = test_gtx.get_user_portfolio(unique_user)
133
+
134
+ assert portfolio["user_address"] == unique_user
135
+ assert portfolio["total_bills"] == 3