-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Expand file tree
/
Copy pathwallet.py
More file actions
555 lines (494 loc) · 23.6 KB
/
Copy pathwallet.py
File metadata and controls
555 lines (494 loc) · 23.6 KB
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
from __future__ import annotations
import logging
import time
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Set, Tuple, cast
from blspy import AugSchemeMPL, G1Element, G2Element
from typing_extensions import Unpack
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.serialized_program import SerializedProgram
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.types.spend_bundle import SpendBundle
from chia.util.hash import std_hash
from chia.util.ints import uint32, uint64, uint128
from chia.util.streamable import Streamable
from chia.wallet.coin_selection import select_coins
from chia.wallet.conditions import Condition, parse_timelock_info
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.payment import Payment
from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
puzzle_for_pk,
puzzle_hash_for_pk,
solution_for_conditions,
)
from chia.wallet.puzzles.puzzle_utils import (
make_assert_coin_announcement,
make_assert_puzzle_announcement,
make_create_coin_announcement,
make_create_coin_condition,
make_create_puzzle_announcement,
make_reserve_fee_condition,
)
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig
from chia.wallet.util.wallet_types import WalletIdentifier, WalletType
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol
if TYPE_CHECKING:
from chia.server.ws_connection import WSChiaConnection
from chia.wallet.wallet_state_manager import WalletStateManager
# https://github.com/Chia-Network/chips/blob/80e4611fe52b174bf1a0382b9dff73805b18b8c6/CHIPs/chip-0002.md#signmessage
CHIP_0002_SIGN_MESSAGE_PREFIX = "Chia Signed Message"
class Wallet:
if TYPE_CHECKING:
_protocol_check: ClassVar[WalletProtocol[ClawbackMetadata]] = cast("Wallet", None)
wallet_info: WalletInfo
wallet_state_manager: WalletStateManager
log: logging.Logger
wallet_id: uint32
@staticmethod
async def create(
wallet_state_manager: Any,
info: WalletInfo,
name: str = __name__,
) -> Wallet:
self = Wallet()
self.log = logging.getLogger(name)
self.wallet_state_manager = wallet_state_manager
self.wallet_id = info.id
return self
@property
def cost_of_single_tx(self) -> int:
return 11000000 # Estimate
async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128:
spendable: List[WalletCoinRecord] = list(
await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records)
)
if len(spendable) == 0:
return uint128(0)
spendable.sort(reverse=True, key=lambda record: record.coin.amount)
max_cost = self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 5 # avoid full block TXs
current_cost = 0
total_amount = 0
total_coin_count = 0
for record in spendable:
current_cost += self.cost_of_single_tx
total_amount += record.coin.amount
total_coin_count += 1
if current_cost + self.cost_of_single_tx > max_cost:
break
return uint128(total_amount)
@classmethod
def type(cls) -> WalletType:
return WalletType.STANDARD_WALLET
def id(self) -> uint32:
return self.wallet_id
async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint128:
return await self.wallet_state_manager.get_confirmed_balance_for_wallet(self.id(), record_list)
async def get_unconfirmed_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128:
return await self.wallet_state_manager.get_unconfirmed_balance(self.id(), unspent_records)
async def get_spendable_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128:
spendable = await self.wallet_state_manager.get_confirmed_spendable_balance_for_wallet(
self.id(), unspent_records
)
return spendable
async def get_pending_change_balance(self) -> uint64:
unconfirmed_tx: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
self.id()
)
addition_amount = 0
for record in unconfirmed_tx:
if not record.is_in_mempool():
if record.spend_bundle is not None:
self.log.warning(
f"TransactionRecord SpendBundle ID: {record.spend_bundle.name()} not in mempool. "
f"(peer, included, error) list: {record.sent_to}"
)
continue
our_spend = False
for coin in record.removals:
if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()):
our_spend = True
break
if our_spend is not True:
continue
for coin in record.additions:
if await self.wallet_state_manager.does_coin_belong_to_wallet(coin, self.id()):
addition_amount += coin.amount
return uint64(addition_amount)
def require_derivation_paths(self) -> bool:
return True
def puzzle_for_pk(self, pubkey: G1Element) -> Program:
return puzzle_for_pk(pubkey)
def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32:
return puzzle_hash_for_pk(pubkey)
async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32:
return puzzle_hash # Looks unimpressive, but it's more complicated in other wallets
async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program:
secret_key = await self.wallet_state_manager.get_private_key(puzzle_hash)
return puzzle_for_pk(secret_key.get_g1())
async def get_new_puzzle(self) -> Program:
dr = await self.wallet_state_manager.get_unused_derivation_record(self.id())
puzzle = puzzle_for_pk(dr.pubkey)
return puzzle
async def get_puzzle(self, new: bool) -> Program:
if new:
return await self.get_new_puzzle()
else:
record: Optional[
DerivationRecord
] = await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id())
if record is None:
return await self.get_new_puzzle() # pragma: no cover
puzzle = puzzle_for_pk(record.pubkey)
return puzzle
async def get_puzzle_hash(self, new: bool) -> bytes32:
if new:
return await self.get_new_puzzlehash()
else:
record: Optional[
DerivationRecord
] = await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id())
if record is None:
return await self.get_new_puzzlehash()
return record.puzzle_hash
async def get_new_puzzlehash(self) -> bytes32:
puzhash = (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash
return puzhash
def make_solution(
self,
primaries: List[Payment],
coin_announcements: Optional[Set[bytes]] = None,
coin_announcements_to_assert: Optional[Set[bytes32]] = None,
puzzle_announcements: Optional[Set[bytes]] = None,
puzzle_announcements_to_assert: Optional[Set[bytes32]] = None,
conditions: Tuple[Condition, ...] = tuple(),
fee: uint64 = uint64(0),
) -> Program:
assert fee >= 0
condition_list: List[Any] = [condition.to_program() for condition in conditions]
if len(primaries) > 0:
for primary in primaries:
condition_list.append(make_create_coin_condition(primary.puzzle_hash, primary.amount, primary.memos))
if fee:
condition_list.append(make_reserve_fee_condition(fee))
if coin_announcements:
for announcement in coin_announcements:
condition_list.append(make_create_coin_announcement(announcement))
if coin_announcements_to_assert:
for announcement_hash in coin_announcements_to_assert:
condition_list.append(make_assert_coin_announcement(announcement_hash))
if puzzle_announcements:
for announcement in puzzle_announcements:
condition_list.append(make_create_puzzle_announcement(announcement))
if puzzle_announcements_to_assert:
for announcement_hash in puzzle_announcements_to_assert:
condition_list.append(make_assert_puzzle_announcement(announcement_hash))
return solution_for_conditions(condition_list)
def add_condition_to_solution(self, condition: Program, solution: Program) -> Program:
python_program = solution.as_python()
python_program[1].append(condition)
return cast(Program, Program.to(python_program))
async def select_coins(
self,
amount: uint64,
coin_selection_config: CoinSelectionConfig,
) -> Set[Coin]:
"""
Returns a set of coins that can be used for generating a new transaction.
Note: Must be called under wallet state manager lock
"""
spendable_amount: uint128 = await self.get_spendable_balance()
spendable_coins: List[WalletCoinRecord] = list(
await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id())
)
# Try to use coins from the store, if there isn't enough of "unused"
# coins use change coins that are not confirmed yet
unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet(
self.id()
)
coins = await select_coins(
spendable_amount,
coin_selection_config,
spendable_coins,
unconfirmed_removals,
self.log,
uint128(amount),
)
assert sum(c.amount for c in coins) >= amount
return coins
async def _generate_unsigned_transaction(
self,
amount: uint64,
newpuzzlehash: bytes32,
tx_config: TXConfig,
fee: uint64 = uint64(0),
origin_id: Optional[bytes32] = None,
coins: Optional[Set[Coin]] = None,
primaries_input: Optional[List[Payment]] = None,
ignore_max_send_amount: bool = False,
coin_announcements_to_consume: Optional[Set[Announcement]] = None,
puzzle_announcements_to_consume: Optional[Set[Announcement]] = None,
memos: Optional[List[bytes]] = None,
negative_change_allowed: bool = False,
puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None,
extra_conditions: Tuple[Condition, ...] = tuple(),
) -> List[CoinSpend]:
"""
Generates a unsigned transaction in form of List(Puzzle, Solutions)
Note: this must be called under a wallet state manager lock
"""
decorator_manager: PuzzleDecoratorManager = self.wallet_state_manager.decorator_manager
if puzzle_decorator_override is not None:
decorator_manager = PuzzleDecoratorManager.create(puzzle_decorator_override)
primaries = []
if primaries_input is not None:
primaries.extend(primaries_input)
total_amount = amount + sum(primary.amount for primary in primaries) + fee
total_balance = await self.get_spendable_balance()
if not ignore_max_send_amount:
max_send = await self.get_max_send_amount()
if total_amount > max_send:
raise ValueError(f"Can't send more than {max_send} mojos in a single transaction, got {total_amount}")
self.log.debug("Got back max send amount: %s", max_send)
if coins is None:
if total_amount > total_balance:
raise ValueError(
f"Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {total_amount} mojos"
)
coins = await self.select_coins(
uint64(total_amount),
tx_config.coin_selection_config,
)
assert len(coins) > 0
self.log.info(f"coins is not None {coins}")
spend_value = sum([coin.amount for coin in coins])
self.log.info(f"spend_value is {spend_value} and total_amount is {total_amount}")
change = spend_value - total_amount
if negative_change_allowed:
change = max(0, change)
assert change >= 0
if coin_announcements_to_consume is not None:
coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume}
else:
coin_announcements_bytes = None
if puzzle_announcements_to_consume is not None:
puzzle_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in puzzle_announcements_to_consume}
else:
puzzle_announcements_bytes = None
spends: List[CoinSpend] = []
primary_announcement_hash: Optional[bytes32] = None
# Check for duplicates
all_primaries_list = [(p.puzzle_hash, p.amount) for p in primaries]
if len(set(all_primaries_list)) != len(all_primaries_list):
raise ValueError("Cannot create two identical coins")
for coin in coins:
# Only one coin creates outputs
if origin_id in (None, coin.name()):
origin_id = coin.name()
inner_puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash)
decorated_target_puzzle_hash = decorator_manager.decorate_target_puzzle_hash(
inner_puzzle, newpuzzlehash
)
target_primary: List[Payment] = []
if memos is None:
memos = []
memos = decorator_manager.decorate_memos(inner_puzzle, newpuzzlehash, memos)
if (primaries_input is None and amount > 0) or primaries_input is not None:
primaries.append(Payment(decorated_target_puzzle_hash, amount, memos))
target_primary.append(Payment(newpuzzlehash, amount, memos))
if change > 0:
if tx_config.reuse_puzhash:
change_puzzle_hash: bytes32 = coin.puzzle_hash
for primary in primaries:
if change_puzzle_hash == primary.puzzle_hash and change == primary.amount:
# We cannot create two coins has same id, create a new puzhash for the change:
change_puzzle_hash = await self.get_new_puzzlehash()
break
else:
change_puzzle_hash = await self.get_new_puzzlehash()
primaries.append(Payment(change_puzzle_hash, uint64(change)))
message_list: List[bytes32] = [c.name() for c in coins]
for primary in primaries:
message_list.append(Coin(coin.name(), primary.puzzle_hash, primary.amount).name())
message: bytes32 = std_hash(b"".join(message_list))
puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash)
solution: Program = self.make_solution(
primaries=primaries,
fee=fee,
coin_announcements={message},
coin_announcements_to_assert=coin_announcements_bytes,
puzzle_announcements_to_assert=puzzle_announcements_bytes,
conditions=extra_conditions,
)
solution = decorator_manager.solve(inner_puzzle, target_primary, solution)
primary_announcement_hash = Announcement(coin.name(), message).name()
spends.append(
CoinSpend(
coin, SerializedProgram.from_bytes(bytes(puzzle)), SerializedProgram.from_bytes(bytes(solution))
)
)
break
else:
raise ValueError("origin_id is not in the set of selected coins")
# Process the non-origin coins now that we have the primary announcement hash
for coin in coins:
if coin.name() == origin_id:
continue
puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash)
solution = self.make_solution(primaries=[], coin_announcements_to_assert={primary_announcement_hash})
solution = decorator_manager.solve(puzzle, [], solution)
spends.append(
CoinSpend(
coin, SerializedProgram.from_bytes(bytes(puzzle)), SerializedProgram.from_bytes(bytes(solution))
)
)
self.log.debug(f"Spends is {spends}")
return spends
async def sign_message(
self, message: str, puzzle_hash: bytes32, is_hex: bool = False
) -> Tuple[G1Element, G2Element]:
# CHIP-0002 message signing as documented at:
# https://github.com/Chia-Network/chips/blob/80e4611fe52b174bf1a0382b9dff73805b18b8c6/CHIPs/chip-0002.md#signmessage
private = await self.wallet_state_manager.get_private_key(puzzle_hash)
synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH)
synthetic_pk = synthetic_secret_key.get_g1()
if is_hex:
puzzle: Program = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, bytes.fromhex(message)))
else:
puzzle = Program.to((CHIP_0002_SIGN_MESSAGE_PREFIX, message))
return synthetic_pk, AugSchemeMPL.sign(synthetic_secret_key, puzzle.get_tree_hash())
async def generate_signed_transaction(
self,
amount: uint64,
puzzle_hash: bytes32,
tx_config: TXConfig,
fee: uint64 = uint64(0),
coins: Optional[Set[Coin]] = None,
primaries: Optional[List[Payment]] = None,
ignore_max_send_amount: bool = False,
coin_announcements_to_consume: Optional[Set[Announcement]] = None,
puzzle_announcements_to_consume: Optional[Set[Announcement]] = None,
memos: Optional[List[bytes]] = None,
puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None,
extra_conditions: Tuple[Condition, ...] = tuple(),
**kwargs: Unpack[GSTOptionalArgs],
) -> List[TransactionRecord]:
origin_id: Optional[bytes32] = kwargs.get("origin_id", None)
negative_change_allowed: bool = kwargs.get("negative_change_allowed", False)
"""
Use this to generate transaction.
Note: this must be called under a wallet state manager lock
The first output is (amount, puzzle_hash, memos), and the rest of the outputs are in primaries.
"""
if primaries is None:
non_change_amount = amount
else:
non_change_amount = uint64(amount + sum(p.amount for p in primaries))
self.log.debug("Generating transaction for: %s %s %s", puzzle_hash, amount, repr(coins))
transaction = await self._generate_unsigned_transaction(
amount,
puzzle_hash,
tx_config,
fee,
origin_id,
coins,
primaries,
ignore_max_send_amount,
coin_announcements_to_consume,
puzzle_announcements_to_consume,
memos,
negative_change_allowed,
puzzle_decorator_override=puzzle_decorator_override,
extra_conditions=extra_conditions,
)
assert len(transaction) > 0
self.log.info("About to sign a transaction: %s", transaction)
spend_bundle: SpendBundle = await self.wallet_state_manager.sign_transaction(transaction)
now = uint64(int(time.time()))
add_list: List[Coin] = list(spend_bundle.additions())
rem_list: List[Coin] = list(spend_bundle.removals())
output_amount = sum(a.amount for a in add_list) + fee
input_amount = sum(r.amount for r in rem_list)
if negative_change_allowed:
assert output_amount >= input_amount
else:
assert output_amount == input_amount
return [
TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=now,
to_puzzle_hash=puzzle_hash,
amount=uint64(non_change_amount),
fee_amount=uint64(fee),
confirmed=False,
sent=uint32(0),
spend_bundle=spend_bundle,
additions=add_list,
removals=rem_list,
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=spend_bundle.name(),
memos=list(compute_memos(spend_bundle).items()),
valid_times=parse_timelock_info(extra_conditions),
)
]
async def create_tandem_xch_tx(
self,
fee: uint64,
tx_config: TXConfig,
announcement_to_assert: Optional[Announcement] = None,
) -> TransactionRecord:
chia_coins = await self.select_coins(fee, tx_config.coin_selection_config)
[chia_tx] = await self.generate_signed_transaction(
uint64(0),
(await self.get_puzzle_hash(not tx_config.reuse_puzhash)),
tx_config,
fee=fee,
coins=chia_coins,
coin_announcements_to_consume={announcement_to_assert} if announcement_to_assert is not None else None,
)
assert chia_tx.spend_bundle is not None
return chia_tx
async def push_transaction(self, tx: TransactionRecord) -> None:
"""Use this API to send transactions."""
await self.wallet_state_manager.add_pending_transaction(tx)
await self.wallet_state_manager.wallet_node.update_ui()
async def get_coins_to_offer(
self,
asset_id: Optional[bytes32],
amount: uint64,
coin_selection_config: CoinSelectionConfig,
) -> Set[Coin]:
if asset_id is not None:
raise ValueError(f"The standard wallet cannot offer coins with asset id {asset_id}")
balance = await self.get_spendable_balance()
if balance < amount:
raise Exception(f"insufficient funds in wallet {self.id()}")
return await self.select_coins(amount, coin_selection_config)
# WSChiaConnection is only imported for type checking
async def coin_added(
self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: Optional[Streamable]
) -> None: # pylint: disable=used-before-assignment
pass
def get_name(self) -> str:
return "Standard Wallet"
async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool:
if hint == coin.puzzle_hash:
wallet_identifier: Optional[
WalletIdentifier
] = await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(coin.puzzle_hash)
if wallet_identifier is not None and wallet_identifier.id == self.id():
return True
return False