forked from Chia-Network/chia-blockchain
-
Notifications
You must be signed in to change notification settings - Fork 0
/
wallet_rpc_api.py
3259 lines (2968 loc) · 157 KB
/
wallet_rpc_api.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 dataclasses
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey
from chia.consensus.block_rewards import calculate_base_farmer_reward
from chia.data_layer.data_layer_wallet import DataLayerWallet
from chia.pools.pool_wallet import PoolWallet
from chia.pools.pool_wallet_info import FARMING_TO_POOL, PoolState, PoolWalletInfo, create_pool_state
from chia.protocols.protocol_message_types import ProtocolMessageTypes
from chia.protocols.wallet_protocol import CoinState
from chia.rpc.rpc_server import Endpoint, EndpointResult, default_get_connections
from chia.server.outbound_message import NodeType, make_msg
from chia.server.ws_connection import WSChiaConnection
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin, coin_as_list
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
from chia.types.signing_mode import SigningMode
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
from chia.util.byte_types import hexstr_to_bytes
from chia.util.config import load_config
from chia.util.errors import KeychainIsLocked
from chia.util.ints import uint16, uint32, uint64
from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic
from chia.util.path import path_from_root
from chia.util.ws_message import WsRpcMessage, create_payload_dict
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
from chia.wallet.cat_wallet.cat_wallet import CATWallet
from chia.wallet.derive_keys import (
MAX_POOL_WALLETS,
master_sk_to_farmer_sk,
master_sk_to_pool_sk,
master_sk_to_singleton_owner_sk,
match_address_to_sk,
)
from chia.wallet.did_wallet import did_wallet_puzzles
from chia.wallet.did_wallet.did_info import DIDInfo
from chia.wallet.did_wallet.did_wallet import DIDWallet
from chia.wallet.did_wallet.did_wallet_puzzles import (
DID_INNERPUZ_MOD,
match_did_puzzle,
metadata_to_program,
program_to_metadata,
)
from chia.wallet.nft_wallet import nft_puzzles
from chia.wallet.nft_wallet.nft_info import NFTCoinInfo, NFTInfo
from chia.wallet.nft_wallet.nft_puzzles import get_metadata_and_phs
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT
from chia.wallet.notification_store import Notification
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key
from chia.wallet.singleton import create_singleton_puzzle
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.address_type import AddressType, is_valid_address
from chia.wallet.util.compute_hints import compute_coin_hints
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType
from chia.wallet.wallet import CHIP_0002_SIGN_MESSAGE_PREFIX, Wallet
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
from chia.wallet.wallet_node import WalletNode
from chia.wallet.wallet_protocol import WalletProtocol
# Timeout for response from wallet/full node for sending a transaction
TIMEOUT = 30
MAX_DERIVATION_INDEX_DELTA = 1000
MAX_NFT_CHUNK_SIZE = 25
log = logging.getLogger(__name__)
class WalletRpcApi:
def __init__(self, wallet_node: WalletNode):
assert wallet_node is not None
self.service = wallet_node
self.service_name = "chia_wallet"
def get_routes(self) -> Dict[str, Endpoint]:
return {
# Key management
"/log_in": self.log_in,
"/get_logged_in_fingerprint": self.get_logged_in_fingerprint,
"/get_public_keys": self.get_public_keys,
"/get_private_key": self.get_private_key,
"/generate_mnemonic": self.generate_mnemonic,
"/add_key": self.add_key,
"/delete_key": self.delete_key,
"/check_delete_key": self.check_delete_key,
"/delete_all_keys": self.delete_all_keys,
# Wallet node
"/set_wallet_resync_on_startup": self.set_wallet_resync_on_startup,
"/get_sync_status": self.get_sync_status,
"/get_height_info": self.get_height_info,
"/push_tx": self.push_tx,
"/push_transactions": self.push_transactions,
"/farm_block": self.farm_block, # Only when node simulator is running
"/get_timestamp_for_height": self.get_timestamp_for_height,
# this function is just here for backwards-compatibility. It will probably
# be removed in the future
"/get_initial_freeze_period": self.get_initial_freeze_period,
"/get_network_info": self.get_network_info,
# Wallet management
"/get_wallets": self.get_wallets,
"/create_new_wallet": self.create_new_wallet,
# Wallet
"/get_wallet_balance": self.get_wallet_balance,
"/get_transaction": self.get_transaction,
"/get_transactions": self.get_transactions,
"/get_transaction_count": self.get_transaction_count,
"/get_next_address": self.get_next_address,
"/send_transaction": self.send_transaction,
"/send_transaction_multi": self.send_transaction_multi,
"/get_farmed_amount": self.get_farmed_amount,
"/create_signed_transaction": self.create_signed_transaction,
"/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions,
"/select_coins": self.select_coins,
"/get_spendable_coins": self.get_spendable_coins,
"/get_coin_records_by_names": self.get_coin_records_by_names,
"/get_current_derivation_index": self.get_current_derivation_index,
"/extend_derivation_index": self.extend_derivation_index,
"/get_notifications": self.get_notifications,
"/delete_notifications": self.delete_notifications,
"/send_notification": self.send_notification,
"/sign_message_by_address": self.sign_message_by_address,
"/sign_message_by_id": self.sign_message_by_id,
"/verify_signature": self.verify_signature,
"/get_transaction_memo": self.get_transaction_memo,
# CATs and trading
"/cat_set_name": self.cat_set_name,
"/cat_asset_id_to_name": self.cat_asset_id_to_name,
"/cat_get_name": self.cat_get_name,
"/get_stray_cats": self.get_stray_cats,
"/cat_spend": self.cat_spend,
"/cat_get_asset_id": self.cat_get_asset_id,
"/create_offer_for_ids": self.create_offer_for_ids,
"/get_offer_summary": self.get_offer_summary,
"/check_offer_validity": self.check_offer_validity,
"/take_offer": self.take_offer,
"/get_offer": self.get_offer,
"/get_all_offers": self.get_all_offers,
"/get_offers_count": self.get_offers_count,
"/cancel_offer": self.cancel_offer,
"/cancel_offers": self.cancel_offers,
"/get_cat_list": self.get_cat_list,
# DID Wallet
"/did_set_wallet_name": self.did_set_wallet_name,
"/did_get_wallet_name": self.did_get_wallet_name,
"/did_update_recovery_ids": self.did_update_recovery_ids,
"/did_update_metadata": self.did_update_metadata,
"/did_get_pubkey": self.did_get_pubkey,
"/did_get_did": self.did_get_did,
"/did_recovery_spend": self.did_recovery_spend,
"/did_get_recovery_list": self.did_get_recovery_list,
"/did_get_metadata": self.did_get_metadata,
"/did_create_attest": self.did_create_attest,
"/did_get_information_needed_for_recovery": self.did_get_information_needed_for_recovery,
"/did_get_current_coin_info": self.did_get_current_coin_info,
"/did_create_backup_file": self.did_create_backup_file,
"/did_transfer_did": self.did_transfer_did,
"/did_message_spend": self.did_message_spend,
"/did_get_info": self.did_get_info,
"/did_find_lost_did": self.did_find_lost_did,
# NFT Wallet
"/nft_mint_nft": self.nft_mint_nft,
"/nft_count_nfts": self.nft_count_nfts,
"/nft_get_nfts": self.nft_get_nfts,
"/nft_get_by_did": self.nft_get_by_did,
"/nft_set_nft_did": self.nft_set_nft_did,
"/nft_set_nft_status": self.nft_set_nft_status,
"/nft_get_wallet_did": self.nft_get_wallet_did,
"/nft_get_wallets_with_dids": self.nft_get_wallets_with_dids,
"/nft_get_info": self.nft_get_info,
"/nft_transfer_nft": self.nft_transfer_nft,
"/nft_add_uri": self.nft_add_uri,
"/nft_calculate_royalties": self.nft_calculate_royalties,
"/nft_mint_bulk": self.nft_mint_bulk,
"/nft_set_did_bulk": self.nft_set_did_bulk,
"/nft_transfer_bulk": self.nft_transfer_bulk,
# Pool Wallet
"/pw_join_pool": self.pw_join_pool,
"/pw_self_pool": self.pw_self_pool,
"/pw_absorb_rewards": self.pw_absorb_rewards,
"/pw_status": self.pw_status,
# DL Wallet
"/create_new_dl": self.create_new_dl,
"/dl_track_new": self.dl_track_new,
"/dl_stop_tracking": self.dl_stop_tracking,
"/dl_latest_singleton": self.dl_latest_singleton,
"/dl_singletons_by_root": self.dl_singletons_by_root,
"/dl_update_root": self.dl_update_root,
"/dl_update_multiple": self.dl_update_multiple,
"/dl_history": self.dl_history,
"/dl_owned_singletons": self.dl_owned_singletons,
"/dl_get_mirrors": self.dl_get_mirrors,
"/dl_new_mirror": self.dl_new_mirror,
"/dl_delete_mirror": self.dl_delete_mirror,
}
def get_connections(self, request_node_type: Optional[NodeType]) -> List[Dict[str, Any]]:
return default_get_connections(server=self.service.server, request_node_type=request_node_type)
async def _state_changed(self, change: str, change_data: Optional[Dict[str, Any]]) -> List[WsRpcMessage]:
"""
Called by the WalletNode or WalletStateManager when something has changed in the wallet. This
gives us an opportunity to send notifications to all connected clients via WebSocket.
"""
payloads = []
if change in {"sync_changed", "coin_added", "add_connection", "close_connection"}:
# Metrics is the only current consumer for this event
payloads.append(create_payload_dict(change, change_data, self.service_name, "metrics"))
if change in {
"offer_cancelled",
"offer_added",
"wallet_created",
"did_coin_added",
"nft_coin_added",
"nft_coin_removed",
"nft_coin_updated",
"nft_coin_did_set",
"new_block",
"coin_removed",
"coin_added",
"new_derivation_index",
"added_stray_cat",
"pending_transaction",
"tx_update",
"wallet_removed",
"new_on_chain_notification",
}:
payloads.append(create_payload_dict("state_changed", change_data, self.service_name, "wallet_ui"))
return payloads
async def _stop_wallet(self):
"""
Stops a currently running wallet/key, which allows starting the wallet with a new key.
Each key has it's own wallet database.
"""
if self.service is not None:
self.service._close()
await self.service._await_closed(shutting_down=False)
async def _convert_tx_puzzle_hash(self, tx: TransactionRecord) -> TransactionRecord:
return dataclasses.replace(
tx,
to_puzzle_hash=(
await self.service.wallet_state_manager.convert_puzzle_hash(tx.wallet_id, tx.to_puzzle_hash)
),
)
async def get_latest_singleton_coin_spend(
self, peer: WSChiaConnection, coin_id: bytes32, latest: bool = True
) -> Tuple[CoinSpend, CoinState]:
coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
[coin_id], peer=peer
)
if coin_state_list is None or len(coin_state_list) < 1:
raise ValueError(f"Coin record 0x{coin_id.hex()} not found")
coin_state: CoinState = coin_state_list[0]
if latest:
# Find the unspent coin
while coin_state.spent_height is not None:
coin_state_list = await self.service.wallet_state_manager.wallet_node.fetch_children(
coin_state.coin.name(), peer=peer
)
odd_coin = None
for coin in coin_state_list:
if coin.coin.amount % 2 == 1:
if odd_coin is not None:
raise ValueError("This is not a singleton, multiple children coins found.")
odd_coin = coin
if odd_coin is None:
raise ValueError("Cannot find child coin, please wait then retry.")
coin_state = odd_coin
# Get parent coin
parent_coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
[coin_state.coin.parent_coin_info], peer=peer
)
if parent_coin_state_list is None or len(parent_coin_state_list) < 1:
raise ValueError(f"Parent coin record 0x{coin_state.coin.parent_coin_info.hex()} not found")
parent_coin_state: CoinState = parent_coin_state_list[0]
coin_spend: CoinSpend = await self.service.wallet_state_manager.wallet_node.fetch_puzzle_solution(
parent_coin_state.spent_height, parent_coin_state.coin, peer
)
return coin_spend, coin_state
##########################################################################################
# Key management
##########################################################################################
async def log_in(self, request) -> EndpointResult:
"""
Logs in the wallet with a specific key.
"""
fingerprint = request["fingerprint"]
if self.service.logged_in_fingerprint == fingerprint:
return {"fingerprint": fingerprint}
await self._stop_wallet()
started = await self.service._start_with_fingerprint(fingerprint)
if started is True:
return {"fingerprint": fingerprint}
return {"success": False, "error": "Unknown Error"}
async def get_logged_in_fingerprint(self, request: Dict) -> EndpointResult:
return {"fingerprint": self.service.logged_in_fingerprint}
async def get_public_keys(self, request: Dict) -> EndpointResult:
try:
fingerprints = [
sk.get_g1().get_fingerprint() for (sk, seed) in await self.service.keychain_proxy.get_all_private_keys()
]
except KeychainIsLocked:
return {"keyring_is_locked": True}
except Exception as e:
raise Exception(
"Error while getting keys. If the issue persists, restart all services."
f" Original error: {type(e).__name__}: {e}"
) from e
else:
return {"public_key_fingerprints": fingerprints}
async def _get_private_key(self, fingerprint) -> Tuple[Optional[PrivateKey], Optional[bytes]]:
try:
all_keys = await self.service.keychain_proxy.get_all_private_keys()
for sk, seed in all_keys:
if sk.get_g1().get_fingerprint() == fingerprint:
return sk, seed
except Exception as e:
log.error(f"Failed to get private key by fingerprint: {e}")
return None, None
async def get_private_key(self, request) -> EndpointResult:
fingerprint = request["fingerprint"]
sk, seed = await self._get_private_key(fingerprint)
if sk is not None:
s = bytes_to_mnemonic(seed) if seed is not None else None
return {
"private_key": {
"fingerprint": fingerprint,
"sk": bytes(sk).hex(),
"pk": bytes(sk.get_g1()).hex(),
"farmer_pk": bytes(master_sk_to_farmer_sk(sk).get_g1()).hex(),
"pool_pk": bytes(master_sk_to_pool_sk(sk).get_g1()).hex(),
"seed": s,
},
}
return {"success": False, "private_key": {"fingerprint": fingerprint}}
async def generate_mnemonic(self, request: Dict) -> EndpointResult:
return {"mnemonic": generate_mnemonic().split(" ")}
async def add_key(self, request) -> EndpointResult:
if "mnemonic" not in request:
raise ValueError("Mnemonic not in request")
# Adding a key from 24 word mnemonic
mnemonic = request["mnemonic"]
try:
sk = await self.service.keychain_proxy.add_private_key(" ".join(mnemonic))
except KeyError as e:
return {
"success": False,
"error": f"The word '{e.args[0]}' is incorrect.'",
"word": e.args[0],
}
except Exception as e:
return {"success": False, "error": str(e)}
fingerprint = sk.get_g1().get_fingerprint()
await self._stop_wallet()
# Makes sure the new key is added to config properly
started = False
try:
await self.service.keychain_proxy.check_keys(self.service.root_path)
except Exception as e:
log.error(f"Failed to check_keys after adding a new key: {e}")
started = await self.service._start_with_fingerprint(fingerprint=fingerprint)
if started is True:
return {"fingerprint": fingerprint}
raise ValueError("Failed to start")
async def delete_key(self, request) -> EndpointResult:
await self._stop_wallet()
fingerprint = request["fingerprint"]
try:
await self.service.keychain_proxy.delete_key_by_fingerprint(fingerprint)
except Exception as e:
log.error(f"Failed to delete key by fingerprint: {e}")
return {"success": False, "error": str(e)}
path = path_from_root(
self.service.root_path,
f"{self.service.config['database_path']}-{fingerprint}",
)
if path.exists():
path.unlink()
return {}
async def _check_key_used_for_rewards(
self, new_root: Path, sk: PrivateKey, max_ph_to_search: int
) -> Tuple[bool, bool]:
"""Checks if the given key is used for either the farmer rewards or pool rewards
returns a tuple of two booleans
The first is true if the key is used as the Farmer rewards, otherwise false
The second is true if the key is used as the Pool rewards, otherwise false
Returns both false if the key cannot be found with the given fingerprint
"""
if sk is None:
return False, False
config: Dict = load_config(new_root, "config.yaml")
farmer_target = config["farmer"].get("xch_target_address")
pool_target = config["pool"].get("xch_target_address")
address_to_check: List[bytes32] = [decode_puzzle_hash(farmer_target), decode_puzzle_hash(pool_target)]
found_addresses: Set[bytes32] = match_address_to_sk(sk, address_to_check, max_ph_to_search)
found_farmer = address_to_check[0] in found_addresses
found_pool = address_to_check[1] in found_addresses
return found_farmer, found_pool
async def check_delete_key(self, request) -> EndpointResult:
"""Check the key use prior to possible deletion
checks whether key is used for either farm or pool rewards
checks if any wallets have a non-zero balance
"""
used_for_farmer: bool = False
used_for_pool: bool = False
walletBalance: bool = False
fingerprint = request["fingerprint"]
max_ph_to_search = request.get("max_ph_to_search", 100)
sk, _ = await self._get_private_key(fingerprint)
if sk is not None:
used_for_farmer, used_for_pool = await self._check_key_used_for_rewards(
self.service.root_path, sk, max_ph_to_search
)
if self.service.logged_in_fingerprint != fingerprint:
await self._stop_wallet()
await self.service._start_with_fingerprint(fingerprint=fingerprint)
wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries()
for w in wallets:
wallet = self.service.wallet_state_manager.wallets[w.id]
unspent = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(w.id)
balance = await wallet.get_confirmed_balance(unspent)
pending_balance = await wallet.get_unconfirmed_balance(unspent)
if (balance + pending_balance) > 0:
walletBalance = True
break
return {
"fingerprint": fingerprint,
"used_for_farmer_rewards": used_for_farmer,
"used_for_pool_rewards": used_for_pool,
"wallet_balance": walletBalance,
}
async def delete_all_keys(self, request: Dict) -> EndpointResult:
await self._stop_wallet()
try:
await self.service.keychain_proxy.delete_all_keys()
except Exception as e:
log.error(f"Failed to delete all keys: {e}")
return {"success": False, "error": str(e)}
path = path_from_root(self.service.root_path, self.service.config["database_path"])
if path.exists():
path.unlink()
return {}
##########################################################################################
# Wallet Node
##########################################################################################
async def set_wallet_resync_on_startup(self, request) -> Dict[str, Any]:
"""
Resync the current logged in wallet. The transaction and offer records will be kept.
:param request: optionally pass in `enable` as bool to enable/disable resync
:return:
"""
assert self.service.wallet_state_manager is not None
try:
enable = bool(request.get("enable", True))
except ValueError:
raise ValueError("Please provide a boolean value for `enable` parameter in request")
fingerprint = self.service.logged_in_fingerprint
if fingerprint is not None:
self.service.set_resync_on_startup(fingerprint, enable)
else:
raise ValueError("You need to login into wallet to use this RPC call")
return {"success": True}
async def get_sync_status(self, request: Dict) -> EndpointResult:
sync_mode = self.service.wallet_state_manager.sync_mode
has_pending_queue_items = self.service.new_peak_queue.has_pending_data_process_items()
syncing = sync_mode or has_pending_queue_items
synced = await self.service.wallet_state_manager.synced()
return {"synced": synced, "syncing": syncing, "genesis_initialized": True}
async def get_height_info(self, request: Dict) -> EndpointResult:
height = await self.service.wallet_state_manager.blockchain.get_finished_sync_up_to()
return {"height": height}
async def get_network_info(self, request: Dict) -> EndpointResult:
network_name = self.service.config["selected_network"]
address_prefix = self.service.config["network_overrides"]["config"][network_name]["address_prefix"]
return {"network_name": network_name, "network_prefix": address_prefix}
async def push_tx(self, request: Dict) -> EndpointResult:
nodes = self.service.server.get_connections(NodeType.FULL_NODE)
if len(nodes) == 0:
raise ValueError("Wallet is not currently connected to any full node peers")
await self.service.push_tx(SpendBundle.from_bytes(hexstr_to_bytes(request["spend_bundle"])))
return {}
async def push_transactions(self, request: Dict) -> EndpointResult:
wallet = self.service.wallet_state_manager.main_wallet
txs: List[TransactionRecord] = []
for transaction_hexstr in request["transactions"]:
tx = TransactionRecord.from_bytes(hexstr_to_bytes(transaction_hexstr))
txs.append(tx)
async with self.service.wallet_state_manager.lock:
for tx in txs:
await wallet.push_transaction(tx)
return {}
async def farm_block(self, request) -> EndpointResult:
raw_puzzle_hash = decode_puzzle_hash(request["address"])
request = FarmNewBlockProtocol(raw_puzzle_hash)
msg = make_msg(ProtocolMessageTypes.farm_new_block, request)
await self.service.server.send_to_all([msg], NodeType.FULL_NODE)
return {}
async def get_timestamp_for_height(self, request) -> EndpointResult:
return {"timestamp": await self.service.get_timestamp_for_height(uint32(request["height"]))}
##########################################################################################
# Wallet Management
##########################################################################################
async def get_wallets(self, request: Dict) -> EndpointResult:
include_data: bool = request.get("include_data", True)
wallet_type: Optional[WalletType] = None
if "type" in request:
wallet_type = WalletType(request["type"])
wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries(wallet_type)
if not include_data:
result: List[WalletInfo] = []
for wallet in wallets:
result.append(WalletInfo(wallet.id, wallet.name, wallet.type, ""))
wallets = result
response: EndpointResult = {"wallets": wallets}
if self.service.logged_in_fingerprint is not None:
response["fingerprint"] = self.service.logged_in_fingerprint
return response
async def create_new_wallet(self, request: Dict) -> EndpointResult:
wallet_state_manager = self.service.wallet_state_manager
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
main_wallet = wallet_state_manager.main_wallet
fee = uint64(request.get("fee", 0))
if request["wallet_type"] == "cat_wallet":
# If not provided, the name will be autogenerated based on the tail hash.
name = request.get("name", None)
if request["mode"] == "new":
async with self.service.wallet_state_manager.lock:
cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet(
wallet_state_manager,
main_wallet,
{"identifier": "genesis_by_id"},
uint64(request["amount"]),
name,
)
asset_id = cat_wallet.get_asset_id()
self.service.wallet_state_manager.state_changed("wallet_created")
return {"type": cat_wallet.type(), "asset_id": asset_id, "wallet_id": cat_wallet.id()}
elif request["mode"] == "existing":
async with self.service.wallet_state_manager.lock:
cat_wallet = await CATWallet.get_or_create_wallet_for_cat(
wallet_state_manager, main_wallet, request["asset_id"], name
)
return {"type": cat_wallet.type(), "asset_id": request["asset_id"], "wallet_id": cat_wallet.id()}
else: # undefined mode
pass
elif request["wallet_type"] == "did_wallet":
if request["did_type"] == "new":
backup_dids = []
num_needed = 0
for d in request["backup_dids"]:
backup_dids.append(decode_puzzle_hash(d))
if len(backup_dids) > 0:
num_needed = uint64(request["num_of_backup_ids_needed"])
metadata: Dict[str, str] = {}
if "metadata" in request:
if type(request["metadata"]) is dict:
metadata = request["metadata"]
async with self.service.wallet_state_manager.lock:
did_wallet_name: str = request.get("wallet_name", None)
if did_wallet_name is not None:
did_wallet_name = did_wallet_name.strip()
did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet(
wallet_state_manager,
main_wallet,
uint64(request["amount"]),
backup_dids,
uint64(num_needed),
metadata,
did_wallet_name,
uint64(request.get("fee", 0)),
)
my_did_id = encode_puzzle_hash(
bytes32.fromhex(did_wallet.get_my_DID()), AddressType.DID.hrp(self.service.config)
)
nft_wallet_name = did_wallet_name
if nft_wallet_name is not None:
nft_wallet_name = f"{nft_wallet_name} NFT Wallet"
await NFTWallet.create_new_nft_wallet(
wallet_state_manager,
main_wallet,
bytes32.fromhex(did_wallet.get_my_DID()),
nft_wallet_name,
)
return {
"success": True,
"type": did_wallet.type(),
"my_did": my_did_id,
"wallet_id": did_wallet.id(),
}
elif request["did_type"] == "recovery":
async with self.service.wallet_state_manager.lock:
did_wallet = await DIDWallet.create_new_did_wallet_from_recovery(
wallet_state_manager, main_wallet, request["backup_data"]
)
assert did_wallet.did_info.temp_coin is not None
assert did_wallet.did_info.temp_puzhash is not None
assert did_wallet.did_info.temp_pubkey is not None
my_did = did_wallet.get_my_DID()
coin_name = did_wallet.did_info.temp_coin.name().hex()
coin_list = coin_as_list(did_wallet.did_info.temp_coin)
newpuzhash = did_wallet.did_info.temp_puzhash
pubkey = did_wallet.did_info.temp_pubkey
return {
"success": True,
"type": did_wallet.type(),
"my_did": my_did,
"wallet_id": did_wallet.id(),
"coin_name": coin_name,
"coin_list": coin_list,
"newpuzhash": newpuzhash.hex(),
"pubkey": pubkey.hex(),
"backup_dids": did_wallet.did_info.backup_ids,
"num_verifications_required": did_wallet.did_info.num_of_backup_ids_needed,
}
else: # undefined did_type
pass
elif request["wallet_type"] == "nft_wallet":
for wallet in self.service.wallet_state_manager.wallets.values():
did_id: Optional[bytes32] = None
if "did_id" in request and request["did_id"] is not None:
did_id = decode_puzzle_hash(request["did_id"])
if wallet.type() == WalletType.NFT:
assert isinstance(wallet, NFTWallet)
if wallet.get_did() == did_id:
log.info("NFT wallet already existed, skipping.")
return {
"success": True,
"type": wallet.type(),
"wallet_id": wallet.id(),
}
async with self.service.wallet_state_manager.lock:
nft_wallet: NFTWallet = await NFTWallet.create_new_nft_wallet(
wallet_state_manager, main_wallet, did_id, request.get("name", None)
)
return {
"success": True,
"type": nft_wallet.type(),
"wallet_id": nft_wallet.id(),
}
elif request["wallet_type"] == "pool_wallet":
if request["mode"] == "new":
if "initial_target_state" not in request:
raise AttributeError("Daemon didn't send `initial_target_state`. Try updating the daemon.")
owner_puzzle_hash: bytes32 = await self.service.wallet_state_manager.main_wallet.get_puzzle_hash(True)
from chia.pools.pool_wallet_info import initial_pool_state_from_dict
async with self.service.wallet_state_manager.lock:
# We assign a pseudo unique id to each pool wallet, so that each one gets its own deterministic
# owner and auth keys. The public keys will go on the blockchain, and the private keys can be found
# using the root SK and trying each index from zero. The indexes are not fully unique though,
# because the PoolWallet is not created until the tx gets confirmed on chain. Therefore if we
# make multiple pool wallets at the same time, they will have the same ID.
max_pwi = 1
for _, wallet in self.service.wallet_state_manager.wallets.items():
if wallet.type() == WalletType.POOLING_WALLET:
assert isinstance(wallet, PoolWallet)
pool_wallet_index = await wallet.get_pool_wallet_index()
if pool_wallet_index > max_pwi:
max_pwi = pool_wallet_index
if max_pwi + 1 >= (MAX_POOL_WALLETS - 1):
raise ValueError(f"Too many pool wallets ({max_pwi}), cannot create any more on this key.")
owner_sk: PrivateKey = master_sk_to_singleton_owner_sk(
self.service.wallet_state_manager.private_key, uint32(max_pwi + 1)
)
owner_pk: G1Element = owner_sk.get_g1()
initial_target_state = initial_pool_state_from_dict(
request["initial_target_state"], owner_pk, owner_puzzle_hash
)
assert initial_target_state is not None
try:
delayed_address = None
if "p2_singleton_delayed_ph" in request:
delayed_address = bytes32.from_hexstr(request["p2_singleton_delayed_ph"])
tr, p2_singleton_puzzle_hash, launcher_id = await PoolWallet.create_new_pool_wallet_transaction(
wallet_state_manager,
main_wallet,
initial_target_state,
fee,
request.get("p2_singleton_delay_time", None),
delayed_address,
)
except Exception as e:
raise ValueError(str(e))
return {
"total_fee": fee * 2,
"transaction": tr,
"launcher_id": launcher_id.hex(),
"p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(),
}
elif request["mode"] == "recovery":
raise ValueError("Need upgraded singleton for on-chain recovery")
else: # undefined wallet_type
pass
# TODO: rework this function to report detailed errors for each error case
return {"success": False, "error": "invalid request"}
##########################################################################################
# Wallet
##########################################################################################
async def get_wallet_balance(self, request: Dict) -> EndpointResult:
wallet_id = uint32(int(request["wallet_id"]))
wallet = self.service.wallet_state_manager.wallets[wallet_id]
balance = await self.service.get_balance(wallet_id)
wallet_balance = balance.to_json_dict()
wallet_balance["wallet_id"] = wallet_id
wallet_balance["wallet_type"] = wallet.type()
if self.service.logged_in_fingerprint is not None:
wallet_balance["fingerprint"] = self.service.logged_in_fingerprint
if wallet.type() == WalletType.CAT:
assert isinstance(wallet, CATWallet)
wallet_balance["asset_id"] = wallet.get_asset_id()
return {"wallet_balance": wallet_balance}
async def get_transaction(self, request: Dict) -> EndpointResult:
transaction_id: bytes32 = bytes32(hexstr_to_bytes(request["transaction_id"]))
tr: Optional[TransactionRecord] = await self.service.wallet_state_manager.get_transaction(transaction_id)
if tr is None:
raise ValueError(f"Transaction 0x{transaction_id.hex()} not found")
return {
"transaction": (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config),
"transaction_id": tr.name,
}
async def get_transaction_memo(self, request: Dict) -> EndpointResult:
transaction_id: bytes32 = bytes32(hexstr_to_bytes(request["transaction_id"]))
tr: Optional[TransactionRecord] = await self.service.wallet_state_manager.get_transaction(transaction_id)
if tr is None:
raise ValueError(f"Transaction 0x{transaction_id.hex()} not found")
if tr.spend_bundle is None or len(tr.spend_bundle.coin_spends) == 0:
if tr.type == uint32(TransactionType.INCOMING_TX.value):
# Fetch incoming tx coin spend
peer = self.service.get_full_node_peer()
assert len(tr.additions) == 1
coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
[tr.additions[0].parent_coin_info], peer=peer
)
assert len(coin_state_list) == 1
coin_spend: CoinSpend = await self.service.wallet_state_manager.wallet_node.fetch_puzzle_solution(
coin_state_list[0].spent_height, coin_state_list[0].coin, peer
)
tr = dataclasses.replace(tr, spend_bundle=SpendBundle([coin_spend], G2Element()))
else:
raise ValueError(f"Transaction 0x{transaction_id.hex()} doesn't have any coin spend.")
assert tr.spend_bundle is not None
memos: Dict[bytes32, List[bytes]] = compute_memos(tr.spend_bundle)
response = {}
# Convert to hex string
for coin_id, memo_list in memos.items():
response[coin_id.hex()] = [memo.hex() for memo in memo_list]
return {transaction_id.hex(): response}
async def get_transactions(self, request: Dict) -> EndpointResult:
wallet_id = int(request["wallet_id"])
start = request.get("start", 0)
end = request.get("end", 50)
sort_key = request.get("sort_key", None)
reverse = request.get("reverse", False)
to_address = request.get("to_address", None)
to_puzzle_hash: Optional[bytes32] = None
if to_address is not None:
to_puzzle_hash = decode_puzzle_hash(to_address)
transactions = await self.service.wallet_state_manager.tx_store.get_transactions_between(
wallet_id, start, end, sort_key=sort_key, reverse=reverse, to_puzzle_hash=to_puzzle_hash
)
return {
"transactions": [
(await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config)
for tr in transactions
],
"wallet_id": wallet_id,
}
async def get_transaction_count(self, request: Dict) -> EndpointResult:
wallet_id = int(request["wallet_id"])
count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id)
return {
"count": count,
"wallet_id": wallet_id,
}
# this function is just here for backwards-compatibility. It will probably
# be removed in the future
async def get_initial_freeze_period(self, _: Dict) -> EndpointResult:
# Mon May 03 2021 17:00:00 GMT+0000
return {"INITIAL_FREEZE_END_TIMESTAMP": 1620061200}
async def get_next_address(self, request: Dict) -> EndpointResult:
"""
Returns a new address
"""
if request["new_address"] is True:
create_new = True
else:
create_new = False
wallet_id = uint32(int(request["wallet_id"]))
wallet = self.service.wallet_state_manager.wallets[wallet_id]
selected = self.service.config["selected_network"]
prefix = self.service.config["network_overrides"]["config"][selected]["address_prefix"]
if wallet.type() == WalletType.STANDARD_WALLET:
assert isinstance(wallet, Wallet)
raw_puzzle_hash = await wallet.get_puzzle_hash(create_new)
address = encode_puzzle_hash(raw_puzzle_hash, prefix)
elif wallet.type() == WalletType.CAT:
assert isinstance(wallet, CATWallet)
raw_puzzle_hash = await wallet.standard_wallet.get_puzzle_hash(create_new)
address = encode_puzzle_hash(raw_puzzle_hash, prefix)
else:
raise ValueError(f"Wallet type {wallet.type()} cannot create puzzle hashes")
return {
"wallet_id": wallet_id,
"address": address,
}
async def send_transaction(self, request) -> EndpointResult:
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced before sending transactions")
wallet_id = uint32(request["wallet_id"])
wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=Wallet)
if not isinstance(request["amount"], int) or not isinstance(request["fee"], int):
raise ValueError("An integer amount or fee is required (too many decimals)")
amount: uint64 = uint64(request["amount"])
address = request["address"]
selected_network = self.service.config["selected_network"]
expected_prefix = self.service.config["network_overrides"]["config"][selected_network]["address_prefix"]
if address[0 : len(expected_prefix)] != expected_prefix:
raise ValueError("Unexpected Address Prefix")
puzzle_hash: bytes32 = decode_puzzle_hash(address)
memos: List[bytes] = []
if "memos" in request:
memos = [mem.encode("utf-8") for mem in request["memos"]]
fee: uint64 = uint64(request.get("fee", 0))
min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0))
max_coin_amount: uint64 = uint64(request.get("max_coin_amount", 0))
if max_coin_amount == 0:
max_coin_amount = uint64(self.service.wallet_state_manager.constants.MAX_COIN_AMOUNT)
exclude_coin_amounts: Optional[List[uint64]] = request.get("exclude_coin_amounts")
if exclude_coin_amounts is not None:
exclude_coin_amounts = [uint64(a) for a in exclude_coin_amounts]
exclude_coin_ids: Optional[List] = request.get("exclude_coin_ids")
if exclude_coin_ids is not None:
coin_records = await self.service.wallet_state_manager.coin_store.get_coin_records(
[bytes32.from_hexstr(hex_id) for hex_id in exclude_coin_ids]
)
exclude_coins = {wr.coin for wr in coin_records.values()}
else:
exclude_coins = set()
async with self.service.wallet_state_manager.lock:
tx: TransactionRecord = await wallet.generate_signed_transaction(
amount,
puzzle_hash,
fee,
memos=memos,
min_coin_amount=min_coin_amount,
max_coin_amount=max_coin_amount,
exclude_coin_amounts=exclude_coin_amounts,
exclude_coins=exclude_coins,
reuse_puzhash=request.get("reuse_puzhash", None),
)
await wallet.push_transaction(tx)
# Transaction may not have been included in the mempool yet. Use get_transaction to check.
return {
"transaction": tx.to_json_dict_convenience(self.service.config),
"transaction_id": tx.name,
}
async def send_transaction_multi(self, request) -> EndpointResult:
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced before sending transactions")
wallet_id = uint32(request["wallet_id"])
wallet = self.service.wallet_state_manager.wallets[wallet_id]
async with self.service.wallet_state_manager.lock:
if wallet.type() == WalletType.CAT:
assert isinstance(wallet, CATWallet)
transaction: Dict = (await self.cat_spend(request, hold_lock=False))["transaction"]
else:
transaction = (await self.create_signed_transaction(request, hold_lock=False))["signed_tx"]
tr = TransactionRecord.from_json_dict_convenience(transaction)
if wallet.type() != WalletType.CAT:
assert isinstance(wallet, Wallet)
await wallet.push_transaction(tr)
# Transaction may not have been included in the mempool yet. Use get_transaction to check.
return {"transaction": transaction, "transaction_id": tr.name}
async def delete_unconfirmed_transactions(self, request) -> EndpointResult:
wallet_id = uint32(request["wallet_id"])
if wallet_id not in self.service.wallet_state_manager.wallets:
raise ValueError(f"Wallet id {wallet_id} does not exist")
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced.")
async with self.service.wallet_state_manager.db_wrapper.writer():
await self.service.wallet_state_manager.tx_store.delete_unconfirmed_transactions(wallet_id)
wallet = self.service.wallet_state_manager.wallets[wallet_id]
if wallet.type() == WalletType.POOLING_WALLET.value:
assert isinstance(wallet, PoolWallet)
wallet.target_state = None
return {}
async def select_coins(self, request) -> EndpointResult:
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced before selecting coins")