-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathwallet_state_manager.py
2488 lines (2282 loc) · 122 KB
/
wallet_state_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import asyncio
import logging
import multiprocessing.context
import time
import traceback
from contextlib import asynccontextmanager
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Callable,
Dict,
Iterator,
List,
Optional,
Set,
Tuple,
Type,
TypeVar,
Union,
)
import aiosqlite
from chia_rs import G1Element, G2Element, PrivateKey
from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.consensus.coinbase import farmer_parent_id, pool_parent_id
from chia.consensus.constants import ConsensusConstants
from chia.data_layer.data_layer_wallet import DataLayerWallet
from chia.data_layer.dl_wallet_store import DataLayerStore
from chia.pools.pool_puzzles import (
SINGLETON_LAUNCHER_HASH,
get_most_recent_singleton_coin_from_coin_spend,
solution_to_pool_state,
)
from chia.pools.pool_wallet import PoolWallet
from chia.protocols.wallet_protocol import CoinState
from chia.rpc.rpc_server import StateChangedProtocol
from chia.server.outbound_message import NodeType
from chia.server.server import ChiaServer
from chia.server.ws_connection import WSChiaConnection
from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_record import CoinRecord
from chia.types.coin_spend import CoinSpend, compute_additions
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import encode_puzzle_hash
from chia.util.db_synchronous import db_synchronous_on
from chia.util.db_wrapper import DBWrapper2
from chia.util.errors import Err
from chia.util.hash import std_hash
from chia.util.ints import uint16, uint32, uint64, uint128
from chia.util.lru_cache import LRUCache
from chia.util.misc import UInt32Range, UInt64Range, VersionedBlob
from chia.util.path import path_from_root
from chia.util.streamable import Streamable
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
from chia.wallet.cat_wallet.cat_info import CATCoinData, CATInfo, CRCATInfo
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, CAT_MOD_HASH, construct_cat_puzzle, match_cat_puzzle
from chia.wallet.cat_wallet.cat_wallet import CATWallet
from chia.wallet.cat_wallet.dao_cat_wallet import DAOCATWallet
from chia.wallet.conditions import Condition, ConditionValidTimes, parse_timelock_info
from chia.wallet.dao_wallet.dao_utils import (
get_p2_singleton_puzhash,
match_dao_cat_puzzle,
match_finished_puzzle,
match_funding_puzzle,
match_proposal_puzzle,
match_treasury_puzzle,
)
from chia.wallet.dao_wallet.dao_wallet import DAOWallet
from chia.wallet.db_wallet.db_wallet_puzzles import MIRROR_PUZZLE_HASH
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.derive_keys import (
_derive_path,
_derive_path_unhardened,
master_sk_to_wallet_sk,
master_sk_to_wallet_sk_intermediate,
master_sk_to_wallet_sk_unhardened,
master_sk_to_wallet_sk_unhardened_intermediate,
)
from chia.wallet.did_wallet.did_info import DIDCoinData
from chia.wallet.did_wallet.did_wallet import DIDWallet
from chia.wallet.did_wallet.did_wallet_puzzles import DID_INNERPUZ_MOD, match_did_puzzle
from chia.wallet.key_val_store import KeyValStore
from chia.wallet.nft_wallet.nft_puzzles import get_metadata_and_phs, get_new_owner_did
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.nft_wallet.uncurry_nft import NFTCoinData, UncurriedNFT
from chia.wallet.notification_manager import NotificationManager
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.puzzles.clawback.drivers import generate_clawback_spend_bundle, match_clawback_puzzle
from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata, ClawbackVersion
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
puzzle_hash_for_synthetic_public_key,
)
from chia.wallet.sign_coin_spends import sign_coin_spends
from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton, get_singleton_id_from_puzzle
from chia.wallet.trade_manager import TradeManager
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.address_type import AddressType
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager
from chia.wallet.util.query_filter import HashFilter
from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.tx_config import TXConfig, TXConfigLoader
from chia.wallet.util.wallet_sync_utils import (
PeerRequestException,
fetch_coin_spend_for_coin_state,
last_change_height_cs,
)
from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType
from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, ProofsChecker, construct_pending_approval_state
from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet
from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential
from chia.wallet.vc_wallet.vc_store import VCStore
from chia.wallet.vc_wallet.vc_wallet import VCWallet
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_blockchain import WalletBlockchain
from chia.wallet.wallet_coin_record import MetadataTypes, WalletCoinRecord
from chia.wallet.wallet_coin_store import WalletCoinStore
from chia.wallet.wallet_info import WalletInfo
from chia.wallet.wallet_interested_store import WalletInterestedStore
from chia.wallet.wallet_nft_store import WalletNftStore
from chia.wallet.wallet_pool_store import WalletPoolStore
from chia.wallet.wallet_protocol import WalletProtocol
from chia.wallet.wallet_puzzle_store import WalletPuzzleStore
from chia.wallet.wallet_retry_store import WalletRetryStore
from chia.wallet.wallet_transaction_store import WalletTransactionStore
from chia.wallet.wallet_user_store import WalletUserStore
TWalletType = TypeVar("TWalletType", bound=WalletProtocol[Any])
if TYPE_CHECKING:
from chia.wallet.wallet_node import WalletNode
PendingTxCallback = Callable[[], None]
class WalletStateManager:
interested_ph_cache: Dict[bytes32, List[int]] = {}
interested_coin_cache: Dict[bytes32, List[int]] = {}
constants: ConsensusConstants
config: Dict[str, Any]
tx_store: WalletTransactionStore
puzzle_store: WalletPuzzleStore
user_store: WalletUserStore
nft_store: WalletNftStore
vc_store: VCStore
basic_store: KeyValStore
# Makes sure only one asyncio thread is changing the blockchain state at one time
lock: asyncio.Lock
log: logging.Logger
# TODO Don't allow user to send tx until wallet is synced
_sync_target: Optional[uint32]
state_changed_callback: Optional[StateChangedProtocol] = None
pending_tx_callback: Optional[PendingTxCallback]
db_path: Path
db_wrapper: DBWrapper2
main_wallet: Wallet
wallets: Dict[uint32, WalletProtocol[Any]]
private_key: PrivateKey
trade_manager: TradeManager
notification_manager: NotificationManager
blockchain: WalletBlockchain
coin_store: WalletCoinStore
interested_store: WalletInterestedStore
retry_store: WalletRetryStore
multiprocessing_context: multiprocessing.context.BaseContext
server: ChiaServer
root_path: Path
wallet_node: WalletNode
pool_store: WalletPoolStore
dl_store: DataLayerStore
default_cats: Dict[str, Any]
asset_to_wallet_map: Dict[AssetType, Any]
initial_num_public_keys: int
decorator_manager: PuzzleDecoratorManager
@staticmethod
async def create(
private_key: PrivateKey,
config: Dict[str, Any],
db_path: Path,
constants: ConsensusConstants,
server: ChiaServer,
root_path: Path,
wallet_node: WalletNode,
) -> WalletStateManager:
self = WalletStateManager()
self.config = config
self.constants = constants
self.server = server
self.root_path = root_path
self.log = logging.getLogger(__name__)
self.lock = asyncio.Lock()
self.log.debug(f"Starting in db path: {db_path}")
fingerprint = private_key.get_g1().get_fingerprint()
sql_log_path: Optional[Path] = None
if self.config.get("log_sqlite_cmds", False):
sql_log_path = path_from_root(self.root_path, "log/wallet_sql.log")
self.log.info(f"logging SQL commands to {sql_log_path}")
self.db_wrapper = await DBWrapper2.create(
database=db_path,
reader_count=self.config.get("db_readers", 4),
log_path=sql_log_path,
synchronous=db_synchronous_on(self.config.get("db_sync", "auto")),
)
self.initial_num_public_keys = config["initial_num_public_keys"]
min_num_public_keys = 425
if not config.get("testing", False) and self.initial_num_public_keys < min_num_public_keys:
self.initial_num_public_keys = min_num_public_keys
self.coin_store = await WalletCoinStore.create(self.db_wrapper)
self.tx_store = await WalletTransactionStore.create(self.db_wrapper)
self.puzzle_store = await WalletPuzzleStore.create(self.db_wrapper)
self.user_store = await WalletUserStore.create(self.db_wrapper)
self.nft_store = await WalletNftStore.create(self.db_wrapper)
self.vc_store = await VCStore.create(self.db_wrapper)
self.basic_store = await KeyValStore.create(self.db_wrapper)
self.trade_manager = await TradeManager.create(self, self.db_wrapper)
self.notification_manager = await NotificationManager.create(self, self.db_wrapper)
self.pool_store = await WalletPoolStore.create(self.db_wrapper)
self.dl_store = await DataLayerStore.create(self.db_wrapper)
self.interested_store = await WalletInterestedStore.create(self.db_wrapper)
self.retry_store = await WalletRetryStore.create(self.db_wrapper)
self.default_cats = DEFAULT_CATS
self.wallet_node = wallet_node
self._sync_target = None
self.blockchain = await WalletBlockchain.create(self.basic_store, self.constants)
self.state_changed_callback = None
self.pending_tx_callback = None
self.db_path = db_path
puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, [])
self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators)
main_wallet_info = await self.user_store.get_wallet_by_id(1)
assert main_wallet_info is not None
self.private_key = private_key
self.main_wallet = await Wallet.create(self, main_wallet_info)
self.wallets = {main_wallet_info.id: self.main_wallet}
self.asset_to_wallet_map = {
AssetType.CAT: CATWallet,
}
wallet: Optional[WalletProtocol[Any]] = None
for wallet_info in await self.get_all_wallet_info_entries():
wallet_type = WalletType(wallet_info.type)
if wallet_type == WalletType.STANDARD_WALLET:
if wallet_info.id == 1:
continue
wallet = await Wallet.create(self, wallet_info)
elif wallet_type == WalletType.CAT:
wallet = await CATWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.DECENTRALIZED_ID:
wallet = await DIDWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.NFT:
wallet = await NFTWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.POOLING_WALLET:
wallet = await PoolWallet.create_from_db(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.DATA_LAYER: # pragma: no cover
wallet = await DataLayerWallet.create(
self,
wallet_info,
)
elif wallet_type == WalletType.DAO: # pragma: no cover
wallet = await DAOWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.DAO_CAT: # pragma: no cover
wallet = await DAOCATWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.VC: # pragma: no cover
wallet = await VCWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.CRCAT: # pragma: no cover
wallet = await CRCATWallet.create(
self,
self.main_wallet,
wallet_info,
)
if wallet is not None:
self.wallets[wallet_info.id] = wallet
return self
def get_public_key_unhardened(self, index: uint32) -> G1Element:
return master_sk_to_wallet_sk_unhardened(self.private_key, index).get_g1()
async def get_private_key(self, puzzle_hash: bytes32) -> PrivateKey:
record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash)
if record is None:
raise ValueError(f"No key for puzzle hash: {puzzle_hash.hex()}")
if record.hardened:
return master_sk_to_wallet_sk(self.private_key, record.index)
return master_sk_to_wallet_sk_unhardened(self.private_key, record.index)
async def get_synthetic_private_key_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[PrivateKey]:
record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash)
if record is None:
return None
if record.hardened:
base_key = master_sk_to_wallet_sk(self.private_key, record.index)
else:
base_key = master_sk_to_wallet_sk_unhardened(self.private_key, record.index)
return calculate_synthetic_secret_key(base_key, DEFAULT_HIDDEN_PUZZLE_HASH)
async def get_private_key_for_pubkey(self, pubkey: G1Element) -> Optional[PrivateKey]:
record = await self.puzzle_store.record_for_pubkey(pubkey)
if record is None:
return None
if record.hardened:
return master_sk_to_wallet_sk(self.private_key, record.index)
return master_sk_to_wallet_sk_unhardened(self.private_key, record.index)
def get_wallet(self, id: uint32, required_type: Type[TWalletType]) -> TWalletType:
wallet = self.wallets[id]
if not isinstance(wallet, required_type):
raise Exception(
f"wallet id {id} is of type {type(wallet).__name__} but type {required_type.__name__} is required",
)
return wallet
async def create_more_puzzle_hashes(
self,
from_zero: bool = False,
mark_existing_as_used: bool = True,
up_to_index: Optional[uint32] = None,
num_additional_phs: Optional[int] = None,
) -> None:
"""
For all wallets in the user store, generates the first few puzzle hashes so
that we can restore the wallet from only the private keys.
"""
targets = list(self.wallets.keys())
self.log.debug("Target wallets to generate puzzle hashes for: %s", repr(targets))
unused: Optional[uint32] = (
uint32(up_to_index + 1) if up_to_index is not None else await self.puzzle_store.get_unused_derivation_path()
)
if unused is None:
# This handles the case where the database has entries but they have all been used
unused = await self.puzzle_store.get_last_derivation_path()
self.log.debug("Tried finding unused: %s", unused)
if unused is None:
# This handles the case where the database is empty
unused = uint32(0)
self.log.debug(f"Requested to generate puzzle hashes to at least index {unused}")
start_t = time.time()
to_generate = num_additional_phs if num_additional_phs is not None else self.initial_num_public_keys
new_paths: bool = False
for wallet_id in targets:
target_wallet = self.wallets[wallet_id]
if not target_wallet.require_derivation_paths():
self.log.debug("Skipping wallet %s as no derivation paths required", wallet_id)
continue
last: Optional[uint32] = await self.puzzle_store.get_last_derivation_path_for_wallet(wallet_id)
self.log.debug(
"Fetched last record for wallet %r: %s (from_zero=%r, unused=%r)", wallet_id, last, from_zero, unused
)
start_index = 0
derivation_paths: List[DerivationRecord] = []
if last is not None:
start_index = last + 1
# If the key was replaced (from_zero=True), we should generate the puzzle hashes for the new key
if from_zero:
start_index = 0
last_index = unused + to_generate
if start_index >= last_index:
self.log.debug(f"Nothing to create for for wallet_id: {wallet_id}, index: {start_index}")
else:
creating_msg = (
f"Creating puzzle hashes from {start_index} to {last_index - 1} for wallet_id: {wallet_id}"
)
self.log.info(f"Start: {creating_msg}")
intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key)
intermediate_sk_un = master_sk_to_wallet_sk_unhardened_intermediate(self.private_key)
for index in range(start_index, last_index):
if target_wallet.type() == WalletType.POOLING_WALLET:
continue
# Hardened
pubkey: G1Element = _derive_path(intermediate_sk, [index]).get_g1()
puzzlehash: Optional[bytes32] = target_wallet.puzzle_hash_for_pk(pubkey)
if puzzlehash is None:
self.log.error(f"Unable to create puzzles with wallet {target_wallet}")
break
self.log.debug(f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash.hex()}")
new_paths = True
derivation_paths.append(
DerivationRecord(
uint32(index),
puzzlehash,
pubkey,
target_wallet.type(),
uint32(target_wallet.id()),
True,
)
)
# Unhardened
pubkey_unhardened: G1Element = _derive_path_unhardened(intermediate_sk_un, [index]).get_g1()
puzzlehash_unhardened: Optional[bytes32] = target_wallet.puzzle_hash_for_pk(pubkey_unhardened)
if puzzlehash_unhardened is None:
self.log.error(f"Unable to create puzzles with wallet {target_wallet}")
break
self.log.debug(
f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}"
)
# We await sleep here to allow an asyncio context switch (since the other parts of this loop do
# not have await and therefore block). This can prevent networking layer from responding to ping.
await asyncio.sleep(0)
derivation_paths.append(
DerivationRecord(
uint32(index),
puzzlehash_unhardened,
pubkey_unhardened,
target_wallet.type(),
uint32(target_wallet.id()),
False,
)
)
self.log.info(f"Done: {creating_msg} Time: {time.time() - start_t} seconds")
await self.puzzle_store.add_derivation_paths(derivation_paths)
if len(derivation_paths) > 0:
if wallet_id == self.main_wallet.id():
await self.wallet_node.new_peak_queue.subscribe_to_puzzle_hashes(
[record.puzzle_hash for record in derivation_paths]
)
self.state_changed("new_derivation_index", data_object={"index": derivation_paths[-1].index})
# By default, we'll mark previously generated unused puzzle hashes as used if we have new paths
if mark_existing_as_used and unused > 0 and new_paths:
self.log.info(f"Updating last used derivation index: {unused - 1}")
await self.puzzle_store.set_used_up_to(uint32(unused - 1))
async def update_wallet_puzzle_hashes(self, wallet_id: uint32) -> None:
derivation_paths: List[DerivationRecord] = []
target_wallet = self.wallets[wallet_id]
last: Optional[uint32] = await self.puzzle_store.get_last_derivation_path_for_wallet(wallet_id)
unused: Optional[uint32] = await self.puzzle_store.get_unused_derivation_path()
if unused is None:
# This handles the case where the database has entries but they have all been used
unused = await self.puzzle_store.get_last_derivation_path()
if unused is None:
# This handles the case where the database is empty
unused = uint32(0)
if last is not None:
for index in range(unused, last):
# Since DID are not released yet we can assume they are only using unhardened keys derivation
pubkey: G1Element = self.get_public_key_unhardened(uint32(index))
puzzlehash = target_wallet.puzzle_hash_for_pk(pubkey)
self.log.info(f"Generating public key at index {index} puzzle hash {puzzlehash.hex()}")
derivation_paths.append(
DerivationRecord(
uint32(index),
puzzlehash,
pubkey,
WalletType(target_wallet.wallet_info.type),
uint32(target_wallet.wallet_info.id),
False,
)
)
await self.puzzle_store.add_derivation_paths(derivation_paths)
async def get_unused_derivation_record(self, wallet_id: uint32, *, hardened: bool = False) -> DerivationRecord:
"""
Creates a puzzle hash for the given wallet, and then makes more puzzle hashes
for every wallet to ensure we always have more in the database. Never reusue the
same public key more than once (for privacy).
"""
async with self.puzzle_store.lock:
# If we have no unused public keys, we will create new ones
unused: Optional[uint32] = await self.puzzle_store.get_unused_derivation_path()
if unused is None:
self.log.debug("No unused paths, generate more ")
await self.create_more_puzzle_hashes()
# Now we must have unused public keys
unused = await self.puzzle_store.get_unused_derivation_path()
assert unused is not None
self.log.debug("Fetching derivation record for: %s %s %s", unused, wallet_id, hardened)
record: Optional[DerivationRecord] = await self.puzzle_store.get_derivation_record(
unused, wallet_id, hardened
)
if record is None:
raise ValueError(f"Missing derivation '{unused}' for wallet id '{wallet_id}' (hardened={hardened})")
# Set this key to used so we never use it again
await self.puzzle_store.set_used_up_to(record.index)
# Create more puzzle hashes / keys
await self.create_more_puzzle_hashes()
return record
async def get_current_derivation_record_for_wallet(self, wallet_id: uint32) -> Optional[DerivationRecord]:
async with self.puzzle_store.lock:
# If we have no unused public keys, we will create new ones
current: Optional[DerivationRecord] = await self.puzzle_store.get_current_derivation_record_for_wallet(
wallet_id
)
return current
def set_callback(self, callback: StateChangedProtocol) -> None:
"""
Callback to be called when the state of the wallet changes.
"""
self.state_changed_callback = callback
def set_pending_callback(self, callback: PendingTxCallback) -> None:
"""
Callback to be called when new pending transaction enters the store
"""
self.pending_tx_callback = callback
def state_changed(
self, state: str, wallet_id: Optional[int] = None, data_object: Optional[Dict[str, Any]] = None
) -> None:
"""
Calls the callback if it's present.
"""
if self.state_changed_callback is None:
return None
change_data: Dict[str, Any] = {"state": state}
if wallet_id is not None:
change_data["wallet_id"] = wallet_id
if data_object is not None:
change_data["additional_data"] = data_object
self.state_changed_callback(state, change_data)
def tx_pending_changed(self) -> None:
"""
Notifies the wallet node that there's new tx pending
"""
if self.pending_tx_callback is None:
return None
self.pending_tx_callback()
async def synced(self) -> bool:
if len(self.server.get_connections(NodeType.FULL_NODE)) == 0:
return False
latest = await self.blockchain.get_peak_block()
if latest is None:
return False
if "simulator" in self.config.get("selected_network", ""):
return True # sim is always synced if we have a genesis block.
if latest.height - await self.blockchain.get_finished_sync_up_to() > 1:
return False
latest_timestamp = self.blockchain.get_latest_timestamp()
has_pending_queue_items = self.wallet_node.new_peak_queue.has_pending_data_process_items()
if latest_timestamp > int(time.time()) - 5 * 60 and not has_pending_queue_items:
return True
return False
@property
def sync_mode(self) -> bool:
return self._sync_target is not None
@property
def sync_target(self) -> Optional[uint32]:
return self._sync_target
@asynccontextmanager
async def set_sync_mode(self, target_height: uint32) -> AsyncIterator[uint32]:
if self.log.level == logging.DEBUG:
self.log.debug(f"set_sync_mode enter {await self.blockchain.get_finished_sync_up_to()}-{target_height}")
async with self.lock:
self._sync_target = target_height
start_time = time.time()
start_height = await self.blockchain.get_finished_sync_up_to()
self.log.info(f"set_sync_mode syncing - range: {start_height}-{target_height}")
self.state_changed("sync_changed")
try:
yield start_height
except Exception:
self.log.exception(
f"set_sync_mode failed - range: {start_height}-{target_height}, seconds: {time.time() - start_time}"
)
finally:
self.state_changed("sync_changed")
if self.log.level == logging.DEBUG:
self.log.debug(
f"set_sync_mode exit - range: {start_height}-{target_height}, "
f"get_finished_sync_up_to: {await self.blockchain.get_finished_sync_up_to()}, "
f"seconds: {time.time() - start_time}"
)
self._sync_target = None
async def get_confirmed_spendable_balance_for_wallet(
self, wallet_id: int, unspent_records: Optional[Set[WalletCoinRecord]] = None
) -> uint128:
"""
Returns the balance amount of all coins that are spendable.
"""
spendable: Set[WalletCoinRecord] = await self.get_spendable_coins_for_wallet(wallet_id, unspent_records)
spendable_amount: uint128 = uint128(0)
for record in spendable:
spendable_amount = uint128(spendable_amount + record.coin.amount)
return spendable_amount
async def does_coin_belong_to_wallet(
self, coin: Coin, wallet_id: int, hint_dict: Dict[bytes32, bytes32] = {}
) -> bool:
"""
Returns true if we have the key for this coin.
"""
wallet_identifier = await self.get_wallet_identifier_for_coin(coin, hint_dict)
return wallet_identifier is not None and wallet_identifier.id == wallet_id
async def get_confirmed_balance_for_wallet(
self,
wallet_id: int,
unspent_coin_records: Optional[Set[WalletCoinRecord]] = None,
) -> uint128:
"""
Returns the confirmed balance, including coinbase rewards that are not spendable.
"""
# lock only if unspent_coin_records is None
if unspent_coin_records is None:
if self.wallets[uint32(wallet_id)].type() == WalletType.CRCAT:
coin_type = CoinType.CRCAT
else:
coin_type = CoinType.NORMAL
unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id, coin_type)
return uint128(sum(cr.coin.amount for cr in unspent_coin_records))
async def get_unconfirmed_balance(
self, wallet_id: int, unspent_coin_records: Optional[Set[WalletCoinRecord]] = None
) -> uint128:
"""
Returns the balance, including coinbase rewards that are not spendable, and unconfirmed
transactions.
"""
# This API should change so that get_balance_from_coin_records is called for Set[WalletCoinRecord]
# and this method is called only for the unspent_coin_records==None case.
if unspent_coin_records is None:
wallet_type: WalletType = self.wallets[uint32(wallet_id)].type()
if wallet_type == WalletType.CRCAT:
unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id, CoinType.CRCAT)
pending_crcat = await self.coin_store.get_unspent_coins_for_wallet(wallet_id, CoinType.CRCAT_PENDING)
unspent_coin_records = unspent_coin_records.union(pending_crcat)
else:
unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id)
unconfirmed_tx: List[TransactionRecord] = await self.tx_store.get_unconfirmed_for_wallet(wallet_id)
all_unspent_coins: Set[Coin] = {cr.coin for cr in unspent_coin_records}
for record in unconfirmed_tx:
for addition in record.additions:
# This change or a self transaction
if await self.does_coin_belong_to_wallet(addition, wallet_id, record.hint_dict()):
all_unspent_coins.add(addition)
for removal in record.removals:
if (
await self.does_coin_belong_to_wallet(removal, wallet_id, record.hint_dict())
and removal in all_unspent_coins
):
all_unspent_coins.remove(removal)
return uint128(sum(coin.amount for coin in all_unspent_coins))
async def unconfirmed_removals_for_wallet(self, wallet_id: int) -> Dict[bytes32, Coin]:
"""
Returns new removals transactions that have not been confirmed yet.
"""
removals: Dict[bytes32, Coin] = {}
unconfirmed_tx = await self.tx_store.get_unconfirmed_for_wallet(wallet_id)
for record in unconfirmed_tx:
for coin in record.removals:
removals[coin.name()] = coin
trade_removals: Dict[bytes32, WalletCoinRecord] = await self.trade_manager.get_locked_coins()
return {**removals, **{coin_id: cr.coin for coin_id, cr in trade_removals.items() if cr.wallet_id == wallet_id}}
async def determine_coin_type(
self, peer: WSChiaConnection, coin_state: CoinState, fork_height: Optional[uint32]
) -> Tuple[Optional[WalletIdentifier], Optional[Streamable]]:
if coin_state.created_height is not None and (
self.is_pool_reward(uint32(coin_state.created_height), coin_state.coin)
or self.is_farmer_reward(uint32(coin_state.created_height), coin_state.coin)
):
return None, None
response: List[CoinState] = await self.wallet_node.get_coin_state(
[coin_state.coin.parent_coin_info], peer=peer, fork_height=fork_height
)
if len(response) == 0:
self.log.warning(f"Could not find a parent coin with ID: {coin_state.coin.parent_coin_info}")
return None, None
parent_coin_state = response[0]
assert parent_coin_state.spent_height == coin_state.created_height
coin_spend = await fetch_coin_spend_for_coin_state(parent_coin_state, peer)
puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
solution = Program.from_bytes(bytes(coin_spend.solution))
uncurried = uncurry_puzzle(puzzle)
dao_ids = []
wallets = self.wallets.values()
for wallet in wallets:
if wallet.type() == WalletType.DAO.value:
assert isinstance(wallet, DAOWallet)
dao_ids.append(wallet.dao_info.treasury_id)
funding_puzzle_check = match_funding_puzzle(uncurried, solution, coin_state.coin, dao_ids)
if funding_puzzle_check:
return await self.get_dao_wallet_from_coinspend_hint(coin_spend, coin_state), None
# Check if the coin is a DAO Treasury
dao_curried_args = match_treasury_puzzle(uncurried.mod, uncurried.args)
if dao_curried_args is not None:
return await self.handle_dao_treasury(dao_curried_args, parent_coin_state, coin_state, coin_spend), None
# Check if the coin is a Proposal and that it isn't the timer coin (amount == 0)
dao_curried_args = match_proposal_puzzle(uncurried.mod, uncurried.args)
if (dao_curried_args is not None) and (coin_state.coin.amount != 0):
return await self.handle_dao_proposal(dao_curried_args, parent_coin_state, coin_state, coin_spend), None
# Check if the coin is a finished proposal
dao_curried_args = match_finished_puzzle(uncurried.mod, uncurried.args)
if dao_curried_args is not None:
return (
await self.handle_dao_finished_proposals(dao_curried_args, parent_coin_state, coin_state, coin_spend),
None,
)
# Check if the coin is a DAO CAT
dao_cat_args = match_dao_cat_puzzle(uncurried)
if dao_cat_args:
return await self.handle_dao_cat(dao_cat_args, parent_coin_state, coin_state, coin_spend), None
# Check if the coin is a CAT
cat_curried_args = match_cat_puzzle(uncurried)
if cat_curried_args is not None:
cat_mod_hash, tail_program_hash, cat_inner_puzzle = cat_curried_args
cat_data: CATCoinData = CATCoinData(
bytes32(cat_mod_hash.atom),
bytes32(tail_program_hash.atom),
cat_inner_puzzle,
parent_coin_state.coin.parent_coin_info,
uint64(parent_coin_state.coin.amount),
)
return (
await self.handle_cat(
cat_data,
parent_coin_state,
coin_state,
coin_spend,
peer,
fork_height,
),
cat_data,
)
# Check if the coin is a NFT
# hint
# First spend where 1 mojo coin -> Singleton launcher -> NFT -> NFT
uncurried_nft = UncurriedNFT.uncurry(uncurried.mod, uncurried.args)
if uncurried_nft is not None and coin_state.coin.amount % 2 == 1:
nft_data = NFTCoinData(uncurried_nft, parent_coin_state, coin_spend)
return await self.handle_nft(nft_data), nft_data
# Check if the coin is a DID
did_curried_args = match_did_puzzle(uncurried.mod, uncurried.args)
if did_curried_args is not None and coin_state.coin.amount % 2 == 1:
p2_puzzle, recovery_list_hash, num_verification, singleton_struct, metadata = did_curried_args
did_data: DIDCoinData = DIDCoinData(
p2_puzzle,
bytes32(recovery_list_hash.atom),
uint16(num_verification.as_int()),
singleton_struct,
metadata,
get_inner_puzzle_from_singleton(coin_spend.puzzle_reveal.to_program()),
parent_coin_state,
)
return await self.handle_did(did_data, parent_coin_state, coin_state, coin_spend, peer), did_data
# Check if the coin is clawback
solution = coin_spend.solution.to_program()
clawback_coin_data = match_clawback_puzzle(uncurried, puzzle, solution)
if clawback_coin_data is not None:
return await self.handle_clawback(clawback_coin_data, coin_state, coin_spend, peer), clawback_coin_data
# Check if the coin is a VC
is_vc, err_msg = VerifiedCredential.is_vc(uncurried)
if is_vc:
vc: VerifiedCredential = VerifiedCredential.get_next_from_coin_spend(coin_spend)
return await self.handle_vc(vc), vc
await self.notification_manager.potentially_add_new_notification(coin_state, coin_spend)
return None, None
async def auto_claim_coins(self) -> None:
# Get unspent clawback coin
current_timestamp = self.blockchain.get_latest_timestamp()
clawback_coins: Dict[Coin, ClawbackMetadata] = {}
tx_fee = uint64(self.config.get("auto_claim", {}).get("tx_fee", 0))
assert self.wallet_node.logged_in_fingerprint is not None
tx_config_loader: TXConfigLoader = TXConfigLoader.from_json_dict(self.config.get("auto_claim", {}))
if tx_config_loader.min_coin_amount is None:
tx_config_loader = tx_config_loader.override(
min_coin_amount=self.config.get("auto_claim", {}).get("min_amount"),
)
tx_config: TXConfig = tx_config_loader.autofill(
constants=self.constants,
config=self.config,
logged_in_fingerprint=self.wallet_node.logged_in_fingerprint,
)
unspent_coins = await self.coin_store.get_coin_records(
coin_type=CoinType.CLAWBACK,
wallet_type=WalletType.STANDARD_WALLET,
spent_range=UInt32Range(stop=uint32(0)),
amount_range=UInt64Range(
start=tx_config.coin_selection_config.min_coin_amount,
stop=tx_config.coin_selection_config.max_coin_amount,
),
)
for coin in unspent_coins.records:
try:
metadata: MetadataTypes = coin.parsed_metadata()
assert isinstance(metadata, ClawbackMetadata)
if await metadata.is_recipient(self.puzzle_store):
coin_timestamp = await self.wallet_node.get_timestamp_for_height(coin.confirmed_block_height)
if current_timestamp - coin_timestamp >= metadata.time_lock:
clawback_coins[coin.coin] = metadata
if len(clawback_coins) >= self.config.get("auto_claim", {}).get("batch_size", 50):
await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config)
clawback_coins = {}
except Exception as e:
self.log.error(f"Failed to claim clawback coin {coin.coin.name().hex()}: %s", e)
if len(clawback_coins) > 0:
await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config)
async def spend_clawback_coins(
self,
clawback_coins: Dict[Coin, ClawbackMetadata],
fee: uint64,
tx_config: TXConfig,
force: bool = False,
extra_conditions: Tuple[Condition, ...] = tuple(),
) -> List[bytes32]:
assert len(clawback_coins) > 0
coin_spends: List[CoinSpend] = []
message: bytes32 = std_hash(b"".join([c.name() for c in clawback_coins.keys()]))
now: uint64 = uint64(int(time.time()))
derivation_record: Optional[DerivationRecord] = None
amount: uint64 = uint64(0)
for coin, metadata in clawback_coins.items():
try:
self.log.info(f"Claiming clawback coin {coin.name().hex()}")
# Get incoming tx
incoming_tx = await self.tx_store.get_transaction_record(coin.name())
assert incoming_tx is not None, f"Cannot find incoming tx for clawback coin {coin.name().hex()}"
if incoming_tx.sent > 0 and not force:
self.log.error(
f"Clawback coin {coin.name().hex()} is already in a pending spend bundle. {incoming_tx}"
)
continue
recipient_puzhash: bytes32 = metadata.recipient_puzzle_hash
sender_puzhash: bytes32 = metadata.sender_puzzle_hash
is_recipient: bool = await metadata.is_recipient(self.puzzle_store)
if is_recipient:
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(recipient_puzhash)
else:
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(sender_puzhash)
assert derivation_record is not None
amount = uint64(amount + coin.amount)
# Remove the clawback hint since it is unnecessary for the XCH coin
memos: List[bytes] = [] if len(incoming_tx.memos) == 0 else incoming_tx.memos[0][1][1:]
inner_puzzle: Program = self.main_wallet.puzzle_for_pk(derivation_record.pubkey)
inner_solution: Program = self.main_wallet.make_solution(
primaries=[
Payment(
derivation_record.puzzle_hash,
uint64(coin.amount),
memos, # Forward memo of the first coin
)
],
coin_announcements=None if len(coin_spends) > 0 or fee == 0 else {message},
conditions=extra_conditions,
)
coin_spend: CoinSpend = generate_clawback_spend_bundle(coin, metadata, inner_puzzle, inner_solution)
coin_spends.append(coin_spend)
# Update incoming tx to prevent double spend and mark it is pending
await self.tx_store.increment_sent(incoming_tx.name, "", MempoolInclusionStatus.PENDING, None)
except Exception as e:
self.log.error(f"Failed to create clawback spend bundle for {coin.name().hex()}: {e}")
if len(coin_spends) == 0:
return []
spend_bundle: SpendBundle = await self.sign_transaction(coin_spends)
if fee > 0:
chia_tx = await self.main_wallet.create_tandem_xch_tx(
fee, tx_config, Announcement(coin_spends[0].coin.name(), message)
)
assert chia_tx.spend_bundle is not None
spend_bundle = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
assert derivation_record is not None
tx_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=now,
to_puzzle_hash=derivation_record.puzzle_hash,
amount=amount,
fee_amount=uint64(fee),
confirmed=False,
sent=uint32(0),
spend_bundle=spend_bundle,
additions=spend_bundle.additions(),
removals=spend_bundle.removals(),
wallet_id=uint32(1),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_CLAWBACK),
name=spend_bundle.name(),
memos=list(compute_memos(spend_bundle).items()),
valid_times=parse_timelock_info(extra_conditions),
)
await self.add_pending_transaction(tx_record)
return [tx_record.name]
async def filter_spam(self, new_coin_state: List[CoinState]) -> List[CoinState]:
xch_spam_amount = self.config.get("xch_spam_amount", 1000000)
# No need to filter anything if the filter is set to 1 or 0 mojos
if xch_spam_amount <= 1:
return new_coin_state
spam_filter_after_n_txs = self.config.get("spam_filter_after_n_txs", 200)
small_unspent_count = await self.coin_store.count_small_unspent(xch_spam_amount)
# if small_unspent_count > spam_filter_after_n_txs:
filtered_cs: List[CoinState] = []
is_standard_wallet_phs: Set[bytes32] = set()
for cs in new_coin_state:
# Only apply filter to new coins being sent to our wallet, that are very small
if (