From 75555f50e812a6ac9c3d8a98efc80f65fad7afca Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 11 Oct 2023 06:44:04 -0700 Subject: [PATCH 001/274] Don't autopush transactions by default anywhere --- chia/pools/pool_wallet.py | 22 +- chia/rpc/util.py | 12 +- chia/rpc/wallet_rpc_api.py | 425 ++++++++++++++----- chia/rpc/wallet_rpc_client.py | 43 ++ chia/wallet/did_wallet/did_wallet.py | 54 ++- chia/wallet/nft_wallet/nft_wallet.py | 32 +- chia/wallet/trade_manager.py | 9 +- chia/wallet/wallet_state_manager.py | 17 +- tests/wallet/cat_wallet/test_trades.py | 28 ++ tests/wallet/db_wallet/test_dl_offers.py | 6 + tests/wallet/did_wallet/test_did.py | 69 ++- tests/wallet/nft_wallet/test_nft_1_offers.py | 141 ++++-- tests/wallet/nft_wallet/test_nft_offers.py | 89 ++-- tests/wallet/nft_wallet/test_nft_wallet.py | 61 ++- tests/wallet/rpc/test_wallet_rpc.py | 31 +- tests/wallet/sync/test_wallet_sync.py | 14 +- tests/wallet/vc_wallet/test_vc_wallet.py | 4 +- 17 files changed, 739 insertions(+), 318 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 00201d58a6de..83cb38047ffc 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -462,7 +462,6 @@ async def create_new_pool_wallet_transaction( name=spend_bundle.name(), valid_times=parse_timelock_info(extra_conditions), ) - await standard_wallet.push_transaction(standard_wallet_record) p2_singleton_puzzle_hash: bytes32 = launcher_id_to_p2_puzzle_hash( launcher_coin_id, p2_singleton_delay_time, p2_singleton_delayed_ph ) @@ -521,17 +520,6 @@ async def generate_fee_transaction( ) return fee_tx - async def publish_transactions(self, travel_tx: TransactionRecord, fee_tx: Optional[TransactionRecord]) -> None: - # We create two transaction records, one for the pool wallet to keep track of the travel TX, and another - # for the standard wallet to keep track of the fee. However, we will only submit the first one to the - # blockchain, and this one has the fee inside it as well. - # The fee tx, if present, will be added to the DB with no spend_bundle set, which has the effect that it - # will not be sent to full nodes. - - await self.wallet_state_manager.add_pending_transaction(travel_tx) - if fee_tx is not None: - await self.wallet_state_manager.add_pending_transaction(dataclasses.replace(fee_tx, spend_bundle=None)) - async def generate_travel_transactions( self, fee: uint64, tx_config: TXConfig ) -> Tuple[TransactionRecord, Optional[TransactionRecord]]: @@ -615,6 +603,7 @@ async def generate_travel_transactions( fee_tx = await self.generate_fee_transaction(fee, tx_config) assert fee_tx.spend_bundle is not None signed_spend_bundle = SpendBundle.aggregate([signed_spend_bundle, fee_tx.spend_bundle]) + fee_tx = dataclasses.replace(fee_tx, spend_bundle=None) tx_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -636,7 +625,6 @@ async def generate_travel_transactions( valid_times=ConditionValidTimes(), ) - await self.publish_transactions(tx_record, fee_tx) return tx_record, fee_tx @staticmethod @@ -918,7 +906,6 @@ async def claim_pool_rewards( valid_times=ConditionValidTimes(), ) - await self.publish_transactions(absorb_transaction, fee_tx) return absorb_transaction, fee_tx async def new_peak(self, peak_height: uint32) -> None: @@ -963,7 +950,12 @@ async def new_peak(self, peak_height: uint32) -> None: assert self.target_state.relative_lock_height >= self.MINIMUM_RELATIVE_LOCK_HEIGHT assert self.target_state.pool_url is not None - await self.generate_travel_transactions(self.next_transaction_fee, self.next_tx_config) + travel_tx, fee_tx = await self.generate_travel_transactions( + self.next_transaction_fee, self.next_tx_config + ) + await self.wallet_state_manager.add_pending_transaction(travel_tx) + if fee_tx is not None: + await self.wallet_state_manager.add_pending_transaction(fee_tx) async def have_unconfirmed_transaction(self) -> bool: unconfirmed: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( diff --git a/chia/rpc/util.py b/chia/rpc/util.py index edee869ba40e..0b71ca25c5fe 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -79,6 +79,16 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s ): raise ValueError("Relative timelocks are not currently supported in the RPC") - return await func(self, request, *args, tx_config=tx_config, extra_conditions=extra_conditions, **kwargs) + push: Optional[bool] = request.get("push") + + return await func( + self, + request, + *args, + tx_config=tx_config, + extra_conditions=extra_conditions, + **({"push": push} if push is not None else {}), + **kwargs, + ) return rpc_endpoint diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 1b39fbcb907d..6b2a8b5da9f2 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -554,9 +554,16 @@ async def push_transactions(self, request: Dict[str, Any]) -> 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) + for transaction_hexstr_or_json in request["transactions"]: + if isinstance(transaction_hexstr_or_json, str): + tx = TransactionRecord.from_bytes(hexstr_to_bytes(transaction_hexstr_or_json)) + txs.append(tx) + else: + try: + tx = TransactionRecord.from_json_dict_convenience(transaction_hexstr_or_json) + except AttributeError: + tx = TransactionRecord.from_json_dict(transaction_hexstr_or_json) + txs.append(tx) async with self.service.wallet_state_manager.lock: for tx in txs: @@ -635,6 +642,7 @@ async def create_new_wallet( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wallet_state_manager = self.service.wallet_state_manager @@ -648,6 +656,8 @@ async def create_new_wallet( name = request.get("name", None) if request["mode"] == "new": if request.get("test", False): + if not push: + raise ValueError("Test CAT minting must be pushed automatically") # pragma: no cover async with self.service.wallet_state_manager.lock: cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( wallet_state_manager, @@ -826,11 +836,14 @@ async def create_new_wallet( delayed_address, extra_conditions=extra_conditions, ) + if push: + await self.service.wallet_state_manager.add_pending_transaction(tr) except Exception as e: raise ValueError(str(e)) return { "total_fee": fee * 2, "transaction": tr, + "transactions": [tr], "launcher_id": launcher_id.hex(), "p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(), } @@ -1018,6 +1031,7 @@ async def send_transaction( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before sending transactions") @@ -1051,11 +1065,14 @@ async def send_transaction( puzzle_decorator_override=request.get("puzzle_decorator", None), extra_conditions=extra_conditions, ) - await wallet.push_transaction(tx) + if push: + await self.service.wallet_state_manager.add_pending_transaction(tx) # Transaction may not have been included in the mempool yet. Use get_transaction to check. + json_tx = tx.to_json_dict_convenience(self.service.config) return { - "transaction": tx.to_json_dict_convenience(self.service.config), + "transaction": json_tx, + "transactions": [json_tx], "transaction_id": tx.name, } @@ -1069,16 +1086,20 @@ async def send_transaction_multi(self, request: Dict[str, Any]) -> EndpointResul async with self.service.wallet_state_manager.lock: if wallet.type() in {WalletType.CAT, WalletType.CRCAT}: assert isinstance(wallet, CATWallet) - transaction = (await self.cat_spend(request, hold_lock=False))["transaction"] + response = await self.cat_spend(request, hold_lock=False) + transaction = response["transaction"] + transactions = response["transactions"] else: - transaction = (await self.create_signed_transaction(request, hold_lock=False))["signed_tx"] - tr = TransactionRecord.from_json_dict_convenience(transaction) - if wallet.type() not in {WalletType.CAT, WalletType.CRCAT}: - assert isinstance(wallet, Wallet) - await wallet.push_transaction(tr) + response = await self.create_signed_transaction(request, hold_lock=False) + transaction = response["signed_tx"] + transactions = response["transactions"] # Transaction may not have been included in the mempool yet. Use get_transaction to check. - return {"transaction": transaction, "transaction_id": tr.name} + return { + "transaction": transaction, + "transaction_id": TransactionRecord.from_json_dict_convenience(transaction).name, + "transactions": transactions, + } @tx_endpoint async def spend_clawback_coins( @@ -1086,6 +1107,7 @@ async def spend_clawback_coins( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Spend clawback coins that were sent (to claw them back) or received (to claim them). @@ -1110,34 +1132,36 @@ async def spend_clawback_coins( batch_size = request.get( "batch_size", self.service.wallet_state_manager.config.get("auto_claim", {}).get("batch_size", 50) ) - tx_id_list: List[bytes] = [] + tx_list: List[TransactionRecord] = [] for coin_id, coin_record in coin_records.coin_id_to_record.items(): try: metadata = coin_record.parsed_metadata() assert isinstance(metadata, ClawbackMetadata) coins[coin_record.coin] = metadata if len(coins) >= batch_size: - tx_id_list.extend( - ( - await self.service.wallet_state_manager.spend_clawback_coins( - coins, tx_fee, tx_config, request.get("force", False), extra_conditions=extra_conditions - ) - ) + new_txs = await self.service.wallet_state_manager.spend_clawback_coins( + coins, tx_fee, tx_config, request.get("force", False), extra_conditions=extra_conditions ) + if push: + for tx in new_txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + tx_list.extend(new_txs) coins = {} except Exception as e: log.error(f"Failed to spend clawback coin {coin_id.hex()}: %s", e) if len(coins) > 0: - tx_id_list.extend( - ( - await self.service.wallet_state_manager.spend_clawback_coins( - coins, tx_fee, tx_config, request.get("force", False), extra_conditions=extra_conditions - ) - ) + new_txs = await self.service.wallet_state_manager.spend_clawback_coins( + coins, tx_fee, tx_config, request.get("force", False), extra_conditions=extra_conditions ) + if push: + for tx in new_txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + tx_list.extend(new_txs) + return { "success": True, - "transaction_ids": [tx.hex() for tx in tx_id_list], + "transaction_ids": [tx.name.hex() for tx in tx_list if tx.type == TransactionType.OUTGOING_CLAWBACK.value], + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in tx_list], } async def delete_unconfirmed_transactions(self, request: Dict[str, Any]) -> EndpointResult: @@ -1389,6 +1413,7 @@ async def send_notification( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: tx: TransactionRecord = await self.service.wallet_state_manager.notification_manager.send_new_notification( bytes32.from_hexstr(request["target"]), @@ -1398,8 +1423,11 @@ async def send_notification( request.get("fee", uint64(0)), extra_conditions=extra_conditions, ) - await self.service.wallet_state_manager.add_pending_transaction(tx) - return {"tx": tx.to_json_dict_convenience(self.service.config)} + if push: + await self.service.wallet_state_manager.add_pending_transaction(tx) + + json_tx = tx.to_json_dict_convenience(self.service.config) + return {"tx": json_tx, "transactions": [json_tx]} async def verify_signature(self, request: Dict[str, Any]) -> EndpointResult: """ @@ -1578,6 +1606,7 @@ async def cat_spend( tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), hold_lock: bool = True, + push: bool = True, ) -> EndpointResult: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced.") @@ -1642,8 +1671,9 @@ async def cat_spend( memos=memos if memos else None, extra_conditions=extra_conditions, ) - for tx in txs: - await wallet.standard_wallet.push_transaction(tx) + if push: + for tx in txs: + await wallet.standard_wallet.push_transaction(tx) else: txs = await wallet.generate_signed_transaction( amounts, @@ -1655,13 +1685,15 @@ async def cat_spend( memos=memos if memos else None, extra_conditions=extra_conditions, ) - for tx in txs: - await wallet.standard_wallet.push_transaction(tx) + if push: + for tx in txs: + await wallet.standard_wallet.push_transaction(tx) # Return the first transaction, which is expected to be the CAT spend. If a fee is # included, it is currently ordered after the CAT spend. return { "transaction": txs[0].to_json_dict_convenience(self.service.config), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], "transaction_id": txs[0].name, } @@ -1687,7 +1719,11 @@ async def create_offer_for_ids( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = False, ) -> EndpointResult: + if push: + raise ValueError("Cannot push an incomplete spend") # pragma: no cover + offer: Dict[str, int] = request["offer"] fee: uint64 = uint64(request.get("fee", 0)) validate_only: bool = request.get("validate_only", False) @@ -1851,6 +1887,7 @@ async def take_offer( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: offer_hex: str = request["offer"] @@ -1892,7 +1929,14 @@ async def take_offer( solver=solver, extra_conditions=extra_conditions, ) - return {"trade_record": trade_record.to_json_dict_convenience()} + if push: + for tx in tx_records: + await self.service.wallet_state_manager.add_pending_transaction(tx) + + return { + "trade_record": trade_record.to_json_dict_convenience(), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in tx_records], + } async def get_offer(self, request: Dict[str, Any]) -> EndpointResult: trade_mgr = self.service.wallet_state_manager.trade_manager @@ -1951,16 +1995,21 @@ async def cancel_offer( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wsm = self.service.wallet_state_manager secure = request["secure"] trade_id = bytes32.from_hexstr(request["trade_id"]) fee: uint64 = uint64(request.get("fee", 0)) async with self.service.wallet_state_manager.lock: - await wsm.trade_manager.cancel_pending_offers( + txs = await wsm.trade_manager.cancel_pending_offers( [bytes32(trade_id)], tx_config, fee=fee, secure=secure, extra_conditions=extra_conditions ) - return {} + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + + return {"transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs]} @tx_endpoint async def cancel_offers( @@ -1968,6 +2017,7 @@ async def cancel_offers( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: secure = request["secure"] batch_fee: uint64 = uint64(request.get("batch_fee", 0)) @@ -1978,6 +2028,8 @@ async def cancel_offers( else: asset_id = request.get("asset_id", "xch") + all_txs: List[TransactionRecord] = [] + start: int = 0 end: int = start + batch_size trade_mgr = self.service.wallet_state_manager.trade_manager @@ -2007,8 +2059,10 @@ async def cancel_offers( continue async with self.service.wallet_state_manager.lock: - await trade_mgr.cancel_pending_offers( - list(records.keys()), tx_config, batch_fee, secure, records, extra_conditions=extra_conditions + all_txs.extend( + await trade_mgr.cancel_pending_offers( + list(records.keys()), tx_config, batch_fee, secure, records, extra_conditions=extra_conditions + ) ) log.info(f"Cancelled offers {start} to {end} ...") # If fewer records were returned than requested, we're done @@ -2016,7 +2070,12 @@ async def cancel_offers( break start = end end += batch_size - return {"success": True} + + if push: + for tx in all_txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + + return {"success": True, "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in all_txs]} ########################################################################################## # Distributed Identities @@ -2040,11 +2099,11 @@ async def did_update_recovery_ids( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wallet_id = uint32(request["wallet_id"]) wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DIDWallet) recovery_list = [] - success: bool = False for _ in request["new_list"]: recovery_list.append(decode_puzzle_hash(_)) if "num_verifications_required" in request: @@ -2055,12 +2114,18 @@ async def did_update_recovery_ids( update_success = await wallet.update_recovery_list(recovery_list, new_amount_verifications_required) # Update coin with new ID info if update_success: - spend_bundle = await wallet.create_update_spend( + txs = await wallet.create_update_spend( tx_config, fee=uint64(request.get("fee", 0)), extra_conditions=extra_conditions ) - if spend_bundle is not None: - success = True - return {"success": success} + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { + "success": True, + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } + else: + return {"success": False, "transactions": []} # pragma: no cover @tx_endpoint async def did_message_spend( @@ -2068,6 +2133,7 @@ async def did_message_spend( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = False, ) -> EndpointResult: wallet_id = uint32(request["wallet_id"]) wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DIDWallet) @@ -2078,12 +2144,16 @@ async def did_message_spend( for pa in request.get("puzzle_announcements", []): puzzle_announcements.add(bytes.fromhex(pa)) - spend_bundle = ( - await wallet.create_message_spend( - tx_config, coin_announcements, puzzle_announcements, extra_conditions=extra_conditions - ) - ).spend_bundle - return {"success": True, "spend_bundle": spend_bundle} + tx = await wallet.create_message_spend( + tx_config, coin_announcements, puzzle_announcements, extra_conditions=extra_conditions + ) + if push: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { + "success": True, + "spend_bundle": tx.spend_bundle, + "transactions": [tx.to_json_dict_convenience(self.service.config)], + } async def did_get_info(self, request: Dict[str, Any]) -> EndpointResult: if "coin_id" not in request: @@ -2329,6 +2399,7 @@ async def did_update_metadata( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wallet_id = uint32(request["wallet_id"]) wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DIDWallet) @@ -2339,13 +2410,20 @@ async def did_update_metadata( update_success = await wallet.update_metadata(metadata) # Update coin with new ID info if update_success: - spend_bundle = await wallet.create_update_spend( + txs = await wallet.create_update_spend( tx_config, uint64(request.get("fee", 0)), extra_conditions=extra_conditions ) - if spend_bundle is not None: - return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} - else: - return {"success": False, "error": "Couldn't create an update spend bundle."} + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { + "wallet_id": wallet_id, + "success": True, + "spend_bundle": SpendBundle.aggregate( + [tx.spend_bundle for tx in txs if tx.spend_bundle is not None] + ), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } else: return {"success": False, "error": f"Couldn't update metadata with input: {metadata}"} @@ -2384,7 +2462,9 @@ async def did_get_metadata(self, request: Dict[str, Any]) -> EndpointResult: "metadata": metadata, } - async def did_recovery_spend(self, request: Dict[str, Any]) -> EndpointResult: + # TODO: this needs a test + # Don't need full @tx_endpoint decorator here, but "push" is still a valid option + async def did_recovery_spend(self, request: Dict[str, Any]) -> EndpointResult: # pragma: no cover wallet_id = uint32(request["wallet_id"]) wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DIDWallet) if len(request["attest_data"]) < wallet.did_info.num_of_backup_ids_needed: @@ -2409,15 +2489,23 @@ async def did_recovery_spend(self, request: Dict[str, Any]) -> EndpointResult: puzhash = wallet.did_info.temp_puzhash assert wallet.did_info.temp_coin is not None - spend_bundle = await wallet.recovery_spend( - wallet.did_info.temp_coin, - puzhash, - info_list, - pubkey, - message_spend_bundle, - ) + tx = ( + await wallet.recovery_spend( + wallet.did_info.temp_coin, + puzhash, + info_list, + pubkey, + message_spend_bundle, + ) + )[0] + if request.get("push", True): + await self.service.wallet_state_manager.add_pending_transaction(tx) if spend_bundle: - return {"success": True, "spend_bundle": spend_bundle} + return { + "success": True, + "spend_bundle": tx.spend_bundle, + "transactions": [tx.to_json_dict_convenience(self.service.config)], + } else: return {"success": False} @@ -2427,32 +2515,36 @@ async def did_get_pubkey(self, request: Dict[str, Any]) -> EndpointResult: pubkey = bytes((await wallet.wallet_state_manager.get_unused_derivation_record(wallet_id)).pubkey).hex() return {"success": True, "pubkey": pubkey} + # TODO: this needs a test @tx_endpoint async def did_create_attest( self, request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> EndpointResult: + push: bool = True, + ) -> EndpointResult: # pragma: no cover wallet_id = uint32(request["wallet_id"]) wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DIDWallet) async with self.service.wallet_state_manager.lock: info = await wallet.get_info_for_recovery() coin = bytes32.from_hexstr(request["coin_name"]) pubkey = G1Element.from_bytes(hexstr_to_bytes(request["pubkey"])) - spend_bundle, attest_data = await wallet.create_attestment( + tx, message_spend_bundle, attest_data = await wallet.create_attestment( coin, bytes32.from_hexstr(request["puzhash"]), pubkey, tx_config, extra_conditions=extra_conditions, ) - if info is not None and spend_bundle is not None: + if info is not None: + assert tx.spend_bundle is not None return { "success": True, - "message_spend_bundle": bytes(spend_bundle).hex(), + "message_spend_bundle": bytes(message_spend_bundle).hex(), "info": [info[0].hex(), info[1].hex(), info[2]], "attest_data": attest_data, + "transactions": [tx.to_json_dict_convenience(self.service.config)], } else: return {"success": False} @@ -2505,6 +2597,7 @@ async def did_transfer_did( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced.") @@ -2512,18 +2605,22 @@ async def did_transfer_did( did_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DIDWallet) puzzle_hash: bytes32 = decode_puzzle_hash(request["inner_address"]) async with self.service.wallet_state_manager.lock: - txs: TransactionRecord = await did_wallet.transfer_did( + txs: List[TransactionRecord] = await did_wallet.transfer_did( puzzle_hash, uint64(request.get("fee", 0)), request.get("with_recovery_info", True), tx_config, extra_conditions=extra_conditions, ) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "success": True, - "transaction": txs.to_json_dict_convenience(self.service.config), - "transaction_id": txs.name, + "transaction": txs[0].to_json_dict_convenience(self.service.config), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + "transaction_id": txs[0].name, } ########################################################################################## @@ -2535,6 +2632,7 @@ async def nft_mint_nft( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: log.debug("Got minting RPC request: %s", request) wallet_id = uint32(request["wallet_id"]) @@ -2586,7 +2684,7 @@ async def nft_mint_nft( else: did_id = decode_puzzle_hash(did_id) - spend_bundle = await nft_wallet.generate_new_nft( + txs = await nft_wallet.generate_new_nft( metadata, tx_config, target_puzhash, @@ -2597,11 +2695,20 @@ async def nft_mint_nft( extra_conditions=extra_conditions, ) nft_id = None - assert spend_bundle is not None + spend_bundle = SpendBundle.aggregate([tx.spend_bundle for tx in txs if tx.spend_bundle is not None]) for cs in spend_bundle.coin_spends: if cs.coin.puzzle_hash == nft_puzzles.LAUNCHER_PUZZLE_HASH: nft_id = encode_puzzle_hash(cs.coin.name(), AddressType.NFT.hrp(self.service.config)) - return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle, "nft_id": nft_id} + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { + "wallet_id": wallet_id, + "success": True, + "spend_bundle": spend_bundle, + "nft_id": nft_id, + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } async def nft_count_nfts(self, request: Dict[str, Any]) -> EndpointResult: wallet_id = request.get("wallet_id", None) @@ -2652,6 +2759,7 @@ async def nft_set_nft_did( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wallet_id = uint32(request["wallet_id"]) nft_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=NFTWallet) @@ -2665,14 +2773,19 @@ async def nft_set_nft_did( return {"success": False, "error": "The NFT doesn't support setting a DID."} fee = uint64(request.get("fee", 0)) - spend_bundle = await nft_wallet.set_nft_did( + txs = await nft_wallet.set_nft_did( nft_coin_info, did_id, tx_config, fee=fee, extra_conditions=extra_conditions, ) - return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} + return { + "wallet_id": wallet_id, + "success": True, + "spend_bundle": SpendBundle.aggregate([tx.spend_bundle for tx in txs if tx.spend_bundle is not None]), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } @tx_endpoint async def nft_set_did_bulk( @@ -2680,6 +2793,7 @@ async def nft_set_did_bulk( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """ Bulk set DID for NFTs across different wallets. @@ -2754,8 +2868,9 @@ async def nft_set_did_bulk( # Add all spend bundles to the first tx refined_tx_list[0] = dataclasses.replace(refined_tx_list[0], spend_bundle=spend_bundle) - for tx in refined_tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in refined_tx_list: + await self.service.wallet_state_manager.add_pending_transaction(tx) for id in coin_ids: await nft_wallet.update_coin_status(id, True) for wallet_id in nft_dict.keys(): @@ -2765,6 +2880,7 @@ async def nft_set_did_bulk( "success": True, "spend_bundle": spend_bundle, "tx_num": len(refined_tx_list), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in refined_tx_list], } else: raise ValueError("Couldn't set DID on given NFT") @@ -2775,6 +2891,7 @@ async def nft_transfer_bulk( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """ Bulk transfer NFTs to an address. @@ -2845,8 +2962,9 @@ async def nft_transfer_bulk( spend_bundle = SpendBundle.aggregate(spend_bundles) # Add all spend bundles to the first tx refined_tx_list[0] = dataclasses.replace(refined_tx_list[0], spend_bundle=spend_bundle) - for tx in refined_tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in refined_tx_list: + await self.service.wallet_state_manager.add_pending_transaction(tx) for id in coin_ids: await nft_wallet.update_coin_status(id, True) for wallet_id in nft_dict.keys(): @@ -2856,6 +2974,7 @@ async def nft_transfer_bulk( "success": True, "spend_bundle": spend_bundle, "tx_num": len(refined_tx_list), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in refined_tx_list], } else: raise ValueError("Couldn't transfer given NFTs") @@ -2921,6 +3040,7 @@ async def nft_transfer_nft( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wallet_id = uint32(request["wallet_id"]) address = request["target_address"] @@ -2954,9 +3074,16 @@ async def nft_transfer_nft( for tx in txs: if tx.spend_bundle is not None: spend_bundle = tx.spend_bundle - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) await nft_wallet.update_coin_status(nft_coin_info.coin.name(), True) - return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} + return { + "wallet_id": wallet_id, + "success": True, + "spend_bundle": spend_bundle, + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } except Exception as e: log.exception(f"Failed to transfer NFT: {e}") return {"success": False, "error": str(e)} @@ -3034,6 +3161,7 @@ async def nft_add_uri( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: wallet_id = uint32(request["wallet_id"]) # Note metadata updater can only add one uri for one field per spend. @@ -3049,10 +3177,18 @@ async def nft_add_uri( nft_coin_info = await nft_wallet.get_nft_coin_by_id(nft_coin_id) fee = uint64(request.get("fee", 0)) - spend_bundle = await nft_wallet.update_metadata( + txs = await nft_wallet.update_metadata( nft_coin_info, key, uri, tx_config, fee=fee, extra_conditions=extra_conditions ) - return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { + "wallet_id": wallet_id, + "success": True, + "spend_bundle": SpendBundle.aggregate([tx.spend_bundle for tx in txs if tx.spend_bundle is not None]), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } async def nft_calculate_royalties(self, request: Dict[str, Any]) -> EndpointResult: return NFTWallet.royalty_calculation( @@ -3069,7 +3205,10 @@ async def nft_mint_bulk( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = False, ) -> EndpointResult: + if push: + raise ValueError("Automatic pushing of nft minting transactions not yet available") # pragma: no cover if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced.") wallet_id = uint32(request["wallet_id"]) @@ -3280,6 +3419,7 @@ async def create_signed_transaction( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = False, hold_lock: bool = True, ) -> EndpointResult: if "wallet_id" in request: @@ -3371,8 +3511,10 @@ async def _generate_signed_transaction() -> EndpointResult: extra_conditions=extra_conditions, ) signed_tx = tx.to_json_dict_convenience(self.service.config) + if push: + await self.service.wallet_state_manager.add_pending_transaction(tx) - return {"signed_txs": [signed_tx], "signed_tx": signed_tx} + return {"signed_txs": [signed_tx], "signed_tx": signed_tx, "transactions": [signed_tx]} else: assert isinstance(wallet, CATWallet) @@ -3390,8 +3532,11 @@ async def _generate_signed_transaction() -> EndpointResult: extra_conditions=extra_conditions, ) signed_txs = [tx.to_json_dict_convenience(self.service.config) for tx in txs] + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) - return {"signed_txs": signed_txs, "signed_tx": signed_txs[0]} + return {"signed_txs": signed_txs, "signed_tx": signed_txs[0], "transactions": signed_txs} if hold_lock: async with self.service.wallet_state_manager.lock: @@ -3408,6 +3553,7 @@ async def pw_join_pool( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: fee = uint64(request.get("fee", 0)) wallet_id = uint32(request["wallet_id"]) @@ -3433,7 +3579,20 @@ async def pw_join_pool( async with self.service.wallet_state_manager.lock: total_fee, tx, fee_tx = await wallet.join_pool(new_target_state, fee, tx_config) - return {"total_fee": total_fee, "transaction": tx, "fee_transaction": fee_tx} + if push: + await self.service.wallet_state_manager.add_pending_transaction(tx) + if fee_tx is not None: + await self.service.wallet_state_manager.add_pending_transaction(fee_tx) + return { + "total_fee": total_fee, + "transaction": tx, + "fee_transaction": fee_tx, + "transactions": [ + transaction.to_json_dict_convenience(self.service.config) + for transaction in (tx, fee_tx) + if transaction is not None + ], + } @tx_endpoint async def pw_self_pool( @@ -3441,6 +3600,7 @@ async def pw_self_pool( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: # Leaving a pool requires two state transitions. # First we transition to PoolSingletonState.LEAVING_POOL @@ -3454,7 +3614,20 @@ async def pw_self_pool( async with self.service.wallet_state_manager.lock: total_fee, tx, fee_tx = await wallet.self_pool(fee, tx_config) - return {"total_fee": total_fee, "transaction": tx, "fee_transaction": fee_tx} + if push: + await self.service.wallet_state_manager.add_pending_transaction(tx) + if fee_tx is not None: + await self.service.wallet_state_manager.add_pending_transaction(fee_tx) + return { + "total_fee": total_fee, + "transaction": tx, + "fee_transaction": fee_tx, + "transactions": [ + transaction.to_json_dict_convenience(self.service.config) + for transaction in (tx, fee_tx) + if transaction is not None + ], + } @tx_endpoint async def pw_absorb_rewards( @@ -3462,6 +3635,7 @@ async def pw_absorb_rewards( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Perform a sweep of the p2_singleton rewards controlled by the pool wallet singleton""" if await self.service.wallet_state_manager.synced() is False: @@ -3475,7 +3649,18 @@ async def pw_absorb_rewards( async with self.service.wallet_state_manager.lock: transaction, fee_tx = await wallet.claim_pool_rewards(fee, max_spends_in_tx, tx_config) state: PoolWalletInfo = await wallet.get_current_state() - return {"state": state.to_json_dict(), "transaction": transaction, "fee_transaction": fee_tx} + if push: + await self.service.wallet_state_manager.add_pending_transaction(transaction) + if fee_tx is not None: + await self.service.wallet_state_manager.add_pending_transaction(fee_tx) + return { + "state": state.to_json_dict(), + "transaction": transaction, + "fee_transaction": fee_tx, + "transactions": [ + tx.to_json_dict_convenience(self.service.config) for tx in (transaction, fee_tx) if tx is not None + ], + } async def pw_status(self, request: Dict[str, Any]) -> EndpointResult: """Return the complete state of the Pool wallet with id `request["wallet_id"]`""" @@ -3499,6 +3684,7 @@ async def create_new_dl( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Initialize the DataLayer Wallet (only one can exist)""" if self.service.wallet_state_manager is None: @@ -3518,8 +3704,9 @@ async def create_new_dl( fee=request.get("fee", uint64(0)), extra_conditions=extra_conditions, ) - await self.service.wallet_state_manager.add_pending_transaction(dl_tx) - await self.service.wallet_state_manager.add_pending_transaction(std_tx) + if push: + await self.service.wallet_state_manager.add_pending_transaction(dl_tx) + await self.service.wallet_state_manager.add_pending_transaction(std_tx) except ValueError as e: log.error(f"Error while generating new reporter {e}") return {"success": False, "error": str(e)} @@ -3594,6 +3781,7 @@ async def dl_update_root( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Get the singleton record for the latest singleton of a launcher ID""" if self.service.wallet_state_manager is None: @@ -3608,9 +3796,13 @@ async def dl_update_root( fee=uint64(request.get("fee", 0)), extra_conditions=extra_conditions, ) - for record in records: - await self.service.wallet_state_manager.add_pending_transaction(record) - return {"tx_record": records[0].to_json_dict_convenience(self.service.config)} + if push: + for record in records: + await self.service.wallet_state_manager.add_pending_transaction(record) + return { + "tx_record": records[0].to_json_dict_convenience(self.service.config), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in records], + } @tx_endpoint async def dl_update_multiple( @@ -3618,6 +3810,7 @@ async def dl_update_multiple( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Update multiple singletons with new merkle roots""" if self.service.wallet_state_manager is None: @@ -3644,9 +3837,13 @@ async def dl_update_multiple( aggregate_spend = SpendBundle.aggregate([aggregate_spend, tx.spend_bundle]) modified_txs.append(dataclasses.replace(tx, spend_bundle=None)) modified_txs[0] = dataclasses.replace(modified_txs[0], spend_bundle=aggregate_spend) - for tx in modified_txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) - return {"tx_records": [rec.to_json_dict_convenience(self.service.config) for rec in modified_txs]} + if push: + for tx in modified_txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { + "tx_records": [rec.to_json_dict_convenience(self.service.config) for rec in modified_txs], + "transactions": [rec.to_json_dict_convenience(self.service.config) for rec in modified_txs], + } async def dl_history(self, request: Dict[str, Any]) -> EndpointResult: """Get the singleton record for the latest singleton of a launcher ID""" @@ -3696,6 +3893,7 @@ async def dl_new_mirror( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Add a new on chain message for a specific singleton""" if self.service.wallet_state_manager is None: @@ -3711,8 +3909,9 @@ async def dl_new_mirror( fee=request.get("fee", uint64(0)), extra_conditions=extra_conditions, ) - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -3724,6 +3923,7 @@ async def dl_delete_mirror( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """Remove an existing mirror for a specific singleton""" if self.service.wallet_state_manager is None: @@ -3739,8 +3939,9 @@ async def dl_delete_mirror( fee=request.get("fee", uint64(0)), extra_conditions=extra_conditions, ) - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -3755,6 +3956,7 @@ async def vc_mint( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """ Mint a verified credential using the assigned DID @@ -3782,8 +3984,9 @@ class VCMint(Streamable): vc_record, tx_list = await vc_wallet.launch_new_vc( did_id, tx_config, puzhash, parsed_request.fee, extra_conditions=extra_conditions ) - for tx in tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in tx_list: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "vc_record": vc_record.to_json_dict(), "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in tx_list], @@ -3842,6 +4045,7 @@ async def vc_spend( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """ Spend a verified credential @@ -3873,8 +4077,9 @@ class VCSpend(Streamable): provider_inner_puzhash=parsed_request.provider_inner_puzhash, extra_conditions=extra_conditions, ) - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -3919,6 +4124,7 @@ async def vc_revoke( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """ Revoke an on chain VC provided the correct DID is available @@ -3942,8 +4148,9 @@ class VCRevoke(Streamable): parsed_request.fee, extra_conditions=extra_conditions, ) - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -3955,6 +4162,7 @@ async def crcat_approve_pending( request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> EndpointResult: """ Moving any "pending approval" CR-CATs into the spendable balance of the wallet @@ -3981,8 +4189,9 @@ class CRCATApprovePending(Streamable): fee=parsed_request.fee, extra_conditions=extra_conditions, ) - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + if push: + for tx in txs: + await self.service.wallet_state_manager.add_pending_transaction(tx) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index c14a26d414cc..546c77c597f8 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -192,6 +192,7 @@ async def send_transaction( puzzle_decorator_override: Optional[List[Dict[str, Union[str, int, bool]]]] = None, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> TransactionRecord: if memos is None: send_dict: Dict = { @@ -201,6 +202,7 @@ async def send_transaction( "fee": fee, "puzzle_decorator": puzzle_decorator_override, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **timelock_info.to_json_dict(), } else: @@ -212,6 +214,7 @@ async def send_transaction( "memos": memos, "puzzle_decorator": puzzle_decorator_override, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **timelock_info.to_json_dict(), } send_dict.update(tx_config.to_json_dict()) @@ -225,6 +228,7 @@ async def send_transaction_multi( tx_config: TXConfig, coins: List[Coin] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes additions_hex = [] @@ -241,6 +245,7 @@ async def send_transaction_multi( "additions": additions_hex, "coins": coins_json, "fee": fee, + "push": push, **tx_config.to_json_dict(), }, ) @@ -299,6 +304,7 @@ async def create_signed_transactions( wallet_id: Optional[int] = None, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = False, ) -> List[TransactionRecord]: # Converts bytes to hex for puzzle hashes additions_hex = [] @@ -311,6 +317,7 @@ async def create_signed_transactions( "additions": additions_hex, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -354,6 +361,7 @@ async def create_signed_transaction( coin_announcements: Optional[List[Announcement]] = None, puzzle_announcements: Optional[List[Announcement]] = None, wallet_id: Optional[int] = None, + push: bool = False, ) -> TransactionRecord: txs: List[TransactionRecord] = await self.create_signed_transactions( additions=additions, @@ -363,6 +371,7 @@ async def create_signed_transaction( coin_announcements=coin_announcements, puzzle_announcements=puzzle_announcements, wallet_id=wallet_id, + push=push, ) if len(txs) == 0: raise ValueError("`create_signed_transaction` returned empty list!") @@ -473,12 +482,14 @@ async def update_did_recovery_list( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> Dict: request: Dict[str, Any] = { "wallet_id": wallet_id, "new_list": recovery_list, "num_verifications_required": num_verification, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -500,12 +511,14 @@ async def did_message_spend( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = False, ) -> Dict: request: Dict[str, Any] = { "wallet_id": wallet_id, "coin_announcements": coin_announcements, "puzzle_announcements": puzzle_announcements, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -519,11 +532,13 @@ async def update_did_metadata( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> Dict: request: Dict[str, Any] = { "wallet_id": wallet_id, "metadata": metadata, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -600,6 +615,7 @@ async def did_transfer_did( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> Dict: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -607,6 +623,7 @@ async def did_transfer_did( "fee": fee, "with_recovery_info": with_recovery, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -762,12 +779,14 @@ async def cat_spend( cat_discrepancy: Optional[Tuple[int, Program, Program]] = None, # (extra_delta, tail_reveal, tail_solution) extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> TransactionRecord: send_dict: Dict[str, Any] = { "wallet_id": wallet_id, "fee": fee, "memos": memos if memos else [], "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -841,11 +860,13 @@ async def take_offer( fee=uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> TradeRecord: req = { "offer": offer.to_bech32(), "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -902,6 +923,7 @@ async def cancel_offer( secure: bool = True, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ): await self.fetch( "cancel_offer", @@ -910,6 +932,7 @@ async def cancel_offer( "secure": secure, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), }, @@ -925,6 +948,7 @@ async def cancel_offers( asset_id: Optional[bytes32] = None, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> None: await self.fetch( "cancel_offers", @@ -936,6 +960,7 @@ async def cancel_offers( "cancel_all": cancel_all, "asset_id": None if asset_id is None else asset_id.hex(), "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), }, @@ -970,6 +995,7 @@ async def mint_nft( did_id=None, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ): request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -987,6 +1013,7 @@ async def mint_nft( "did_id": did_id, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -1003,6 +1030,7 @@ async def add_uri_to_nft( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ): request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1011,6 +1039,7 @@ async def add_uri_to_nft( "key": key, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -1047,6 +1076,7 @@ async def transfer_nft( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ): request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1054,6 +1084,7 @@ async def transfer_nft( "target_address": target_address, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -1079,6 +1110,7 @@ async def set_nft_did( tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ): request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1086,6 +1118,7 @@ async def set_nft_did( "nft_coin_id": nft_coin_id, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -1116,6 +1149,7 @@ async def nft_mint_bulk( fee: Optional[int] = 0, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = False, ) -> Dict: request = { "wallet_id": wallet_id, @@ -1133,6 +1167,7 @@ async def nft_mint_bulk( "mint_from_did": mint_from_did, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), } @@ -1351,6 +1386,7 @@ async def vc_mint( fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> Tuple[VCRecord, List[TransactionRecord]]: response = await self.fetch( "vc_mint", @@ -1359,6 +1395,7 @@ async def vc_mint( "target_address": encode_puzzle_hash(target_address, "rpc") if target_address is not None else None, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), }, @@ -1385,6 +1422,7 @@ async def vc_spend( fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> List[TransactionRecord]: response = await self.fetch( "vc_spend", @@ -1397,6 +1435,7 @@ async def vc_spend( else provider_inner_puzhash, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), }, @@ -1417,6 +1456,7 @@ async def vc_revoke( fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> List[TransactionRecord]: response = await self.fetch( "vc_revoke", @@ -1424,6 +1464,7 @@ async def vc_revoke( "vc_parent_id": vc_parent_id.hex(), "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **tx_config.to_json_dict(), **timelock_info.to_json_dict(), }, @@ -1436,6 +1477,7 @@ async def crcat_approve_pending( min_amount_to_claim: uint64, tx_config: TXConfig, fee: uint64 = uint64(0), + push: bool = True, ) -> List[TransactionRecord]: response = await self.fetch( "crcat_approve_pending", @@ -1443,6 +1485,7 @@ async def crcat_approve_pending( "wallet_id": wallet_id, "min_amount_to_claim": min_amount_to_claim, "fee": fee, + "push": push, **tx_config.to_json_dict(), }, ) diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 5bcbc1469018..6aa8680fb816 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -124,14 +124,14 @@ async def create_new_did_wallet( raise ValueError("Not enough balance") try: - spend_bundle = await self.generate_new_decentralised_id(amount, DEFAULT_TX_CONFIG, fee) + txs = await self.generate_new_decentralised_id(amount, DEFAULT_TX_CONFIG, fee) except Exception: await wallet_state_manager.user_store.delete_wallet(self.id()) raise - if spend_bundle is None: - await wallet_state_manager.user_store.delete_wallet(self.id()) - raise ValueError("Failed to create spend.") + for tx in txs: + await self.wallet_state_manager.add_pending_transaction(tx) + await self.wallet_state_manager.add_new_wallet(self) return self @@ -534,7 +534,7 @@ def get_name(self): async def create_update_spend( self, tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple() - ): + ) -> List[TransactionRecord]: assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None coin = await self.get_coin() @@ -600,7 +600,6 @@ async def create_update_spend( if chia_tx is not None and chia_tx.spend_bundle is not None: spend_bundle = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle]) chia_tx = dataclasses.replace(chia_tx, spend_bundle=None) - await self.wallet_state_manager.add_pending_transaction(chia_tx) did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -620,9 +619,12 @@ async def create_update_spend( memos=list(compute_memos(spend_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ) - await self.wallet_state_manager.add_pending_transaction(did_record) - return spend_bundle + txs = [did_record] + if chia_tx is not None: + txs.append(chia_tx) + + return txs async def transfer_did( self, @@ -631,7 +633,7 @@ async def transfer_did( with_recovery: bool, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> TransactionRecord: + ) -> List[TransactionRecord]: """ Transfer the current DID to another owner :param new_puzhash: New owner's p2_puzzle @@ -697,7 +699,6 @@ async def transfer_did( if chia_tx is not None and chia_tx.spend_bundle is not None: spend_bundle = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle]) chia_tx = dataclasses.replace(chia_tx, spend_bundle=None) - await self.wallet_state_manager.add_pending_transaction(chia_tx) did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -717,8 +718,10 @@ async def transfer_did( memos=list(compute_memos(spend_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ) - await self.wallet_state_manager.add_pending_transaction(did_record) - return did_record + txs = [did_record] + if chia_tx is not None: + txs.append(chia_tx) + return txs # The message spend can tests\wallet\rpc\test_wallet_rpc.py send messages and also change your innerpuz async def create_message_spend( @@ -806,7 +809,7 @@ async def create_message_spend( ) # This is used to cash out, or update the id_list - async def create_exit_spend(self, puzhash: bytes32, tx_config: TXConfig): + async def create_exit_spend(self, puzhash: bytes32, tx_config: TXConfig) -> List[TransactionRecord]: assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None coin = await self.get_coin() @@ -857,8 +860,7 @@ async def create_exit_spend(self, puzhash: bytes32, tx_config: TXConfig): memos=list(compute_memos(spend_bundle).items()), valid_times=ConditionValidTimes(), ) - await self.wallet_state_manager.add_pending_transaction(did_record) - return spend_bundle + return [did_record] # Pushes a SpendBundle to create a message coin on the blockchain # Returns a SpendBundle for the recoverer to spend the message coin @@ -869,7 +871,7 @@ async def create_attestment( pubkey: G1Element, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Tuple[SpendBundle, str]: + ) -> Tuple[TransactionRecord, SpendBundle, str]: """ Create an attestment :param recovering_coin_name: Coin ID of the DID @@ -941,8 +943,7 @@ async def create_attestment( ) attest_str: str = f"{self.get_my_DID()}:{bytes(message_spend_bundle).hex()}:{coin.parent_coin_info.hex()}:" attest_str += f"{self.did_info.current_inner.get_tree_hash().hex()}:{coin.amount}" - await self.wallet_state_manager.add_pending_transaction(did_record) - return message_spend_bundle, attest_str + return did_record, message_spend_bundle, attest_str async def get_info_for_recovery(self) -> Optional[Tuple[bytes32, bytes32, uint64]]: assert self.did_info.current_inner is not None @@ -997,7 +998,7 @@ async def recovery_spend( parent_innerpuzhash_amounts_for_recovery_ids: List[Tuple[bytes, bytes, int]], pubkey: G1Element, spend_bundle: SpendBundle, - ) -> SpendBundle: + ) -> List[TransactionRecord]: assert self.did_info.origin_coin is not None # innersol is mode new_amount_or_p2_solution new_inner_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey recovery_list_reveal my_id) # noqa @@ -1068,7 +1069,6 @@ async def recovery_spend( memos=list(compute_memos(spend_bundle).items()), valid_times=ConditionValidTimes(), ) - await self.wallet_state_manager.add_pending_transaction(did_record) new_did_info = DIDInfo( self.did_info.origin_coin, self.did_info.backup_ids, @@ -1082,7 +1082,7 @@ async def recovery_spend( self.did_info.metadata, ) await self.save_info(new_did_info) - return spend_bundle + return [did_record] async def get_new_p2_inner_hash(self) -> bytes32: puzzle = await self.get_new_p2_inner_puzzle() @@ -1221,14 +1221,12 @@ async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: async def generate_new_decentralised_id( self, amount: uint64, tx_config: TXConfig, fee: uint64 = uint64(0) - ) -> Optional[SpendBundle]: + ) -> List[TransactionRecord]: """ This must be called under the wallet state manager lock """ coins = await self.standard_wallet.select_coins(uint64(amount + fee), tx_config.coin_selection_config) - if coins is None: - return None origin = coins.copy().pop() genesis_launcher_puz = SINGLETON_LAUNCHER_PUZZLE @@ -1273,9 +1271,6 @@ async def generate_new_decentralised_id( await self.add_parent(eve_coin.parent_coin_info, eve_parent) await self.add_parent(eve_coin.name(), future_parent) - if tx_record.spend_bundle is None: - return None - # Only want to save this information if the transaction is valid did_info: DIDInfo = DIDInfo( launcher_coin, @@ -1291,6 +1286,7 @@ async def generate_new_decentralised_id( ) await self.save_info(did_info) eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner) + assert tx_record.spend_bundle is not None full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend, launcher_sb]) assert self.did_info.origin_coin is not None assert self.did_info.current_inner is not None @@ -1315,9 +1311,7 @@ async def generate_new_decentralised_id( valid_times=ConditionValidTimes(), ) regular_record = dataclasses.replace(tx_record, spend_bundle=None) - await self.wallet_state_manager.add_pending_transaction(regular_record) - await self.wallet_state_manager.add_pending_transaction(did_record) - return full_spend + return [did_record, regular_record] async def generate_eve_spend( self, diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index d211c5c5e7a8..48d3c9ff01dd 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -328,9 +328,8 @@ async def generate_new_nft( percentage: uint16 = uint16(0), did_id: Optional[bytes] = None, fee: uint64 = uint64(0), - push_tx: bool = True, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Optional[SpendBundle]: + ) -> List[TransactionRecord]: """ This must be called under the wallet state manager lock """ @@ -404,8 +403,7 @@ async def generate_new_nft( eve_coin = Coin(launcher_coin.name(), eve_fullpuz_hash, uint64(amount)) if tx_record.spend_bundle is None: - self.log.error("Couldn't produce a launcher spend") - return None + raise ValueError("Couldn't produce a launcher spend") # pragma: no cover bundles_to_agg = [tx_record.spend_bundle, launcher_sb] @@ -435,10 +433,7 @@ async def generate_new_nft( memos=[[target_puzzle_hash]], ) txs.append(dataclasses.replace(tx_record, spend_bundle=None)) - if push_tx: - for tx in txs: - await self.wallet_state_manager.add_pending_transaction(tx) - return SpendBundle.aggregate([x.spend_bundle for x in txs if x.spend_bundle is not None]) + return txs async def sign(self, spend_bundle: SpendBundle, puzzle_hashes: Optional[List[bytes32]] = None) -> SpendBundle: if puzzle_hashes is None: @@ -486,7 +481,7 @@ async def update_metadata( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Optional[SpendBundle]: + ) -> List[TransactionRecord]: uncurried_nft = UncurriedNFT.uncurry(*nft_coin_info.full_puzzle.uncurry()) assert uncurried_nft is not None puzzle_hash = uncurried_nft.p2_puzzle.get_tree_hash() @@ -505,11 +500,9 @@ async def update_metadata( metadata_update=(key, uri), extra_conditions=extra_conditions, ) - for tx in txs: - await self.wallet_state_manager.add_pending_transaction(tx) await self.update_coin_status(nft_coin_info.coin.name(), True) self.wallet_state_manager.state_changed("nft_coin_updated", self.wallet_info.id) - return SpendBundle.aggregate([x.spend_bundle for x in txs if x.spend_bundle is not None]) + return txs async def get_current_nfts(self, start_index: int = 0, count: int = 50) -> List[NFTCoinInfo]: return await self.nft_store.get_nft_list(wallet_id=self.id(), start_index=start_index, count=count) @@ -1211,7 +1204,7 @@ async def set_nft_did( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> SpendBundle: + ) -> List[TransactionRecord]: self.log.debug("Setting NFT DID with parameters: nft=%s did=%s", nft_coin_info, did_id) unft = UncurriedNFT.uncurry(*nft_coin_info.full_puzzle.uncurry()) assert unft is not None @@ -1234,15 +1227,10 @@ async def set_nft_did( additional_bundles=additional_bundles, extra_conditions=extra_conditions, ) - spend_bundle = SpendBundle.aggregate([x.spend_bundle for x in nft_tx_record if x.spend_bundle is not None]) - if spend_bundle: - for tx in nft_tx_record: - await self.wallet_state_manager.add_pending_transaction(tx) - await self.update_coin_status(nft_coin_info.coin.name(), True) - self.wallet_state_manager.state_changed("nft_coin_did_set", self.wallet_info.id) - return spend_bundle - else: - raise ValueError("Couldn't set DID on given NFT") + + await self.update_coin_status(nft_coin_info.coin.name(), True) + self.wallet_state_manager.state_changed("nft_coin_did_set", self.wallet_info.id) + return nft_tx_record async def mint_from_did( self, diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 5af9a4b3c820..f3272fb3ba74 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -230,7 +230,7 @@ async def cancel_pending_offers( secure: bool = True, # Cancel with a transaction on chain trade_cache: Dict[bytes32, TradeRecord] = {}, # Optional pre-fetched trade records for optimization extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Optional[List[TransactionRecord]]: + ) -> List[TransactionRecord]: """This will create a transaction that includes coins that were offered""" all_txs: List[TransactionRecord] = [] @@ -338,8 +338,8 @@ async def cancel_pending_offers( # Aggregate spend bundles to the first tx if len(all_txs) > 0: all_txs[0] = dataclasses.replace(all_txs[0], spend_bundle=SpendBundle.aggregate(bundles)) - for tx in all_txs: - await self.wallet_state_manager.add_pending_transaction(tx_record=dataclasses.replace(tx, fee_amount=fee)) + + all_txs = [dataclasses.replace(tx, fee_amount=fee) for tx in all_txs] return all_txs @@ -814,9 +814,6 @@ async def respond_to_offer( memos=[], valid_times=ConditionValidTimes(), ) - await self.wallet_state_manager.add_pending_transaction(push_tx) - for tx in tx_records: - await self.wallet_state_manager.add_transaction(tx) return trade_record, [push_tx, *tx_records] diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index aee0e1eaff66..dc95c4d47c58 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import dataclasses import logging import multiprocessing.context import time @@ -808,12 +809,16 @@ async def auto_claim_coins(self) -> None: 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) + txs = await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config) + for tx in txs: + await self.add_pending_transaction(tx) 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) + txs = await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config) + for tx in txs: + await self.add_pending_transaction(tx) async def spend_clawback_coins( self, @@ -822,7 +827,7 @@ async def spend_clawback_coins( tx_config: TXConfig, force: bool = False, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> List[bytes32]: + ) -> List[TransactionRecord]: assert len(clawback_coins) > 0 coin_spends: List[CoinSpend] = [] message: bytes32 = std_hash(b"".join([c.name() for c in clawback_coins.keys()])) @@ -871,12 +876,14 @@ async def spend_clawback_coins( if len(coin_spends) == 0: return [] spend_bundle: SpendBundle = await self.sign_transaction(coin_spends) + tx_list: List[TransactionRecord] = [] 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]) + tx_list.append(dataclasses.replace(chia_tx, spend_bundle=None)) assert derivation_record is not None tx_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -897,11 +904,11 @@ async def spend_clawback_coins( memos=list(compute_memos(spend_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ) - await self.add_pending_transaction(tx_record) + tx_list.append(tx_record) # Update incoming tx to prevent double spend and mark it is pending for coin_spend in coin_spends: await self.tx_store.increment_sent(coin_spend.coin.name(), "", MempoolInclusionStatus.PENDING, None) - return [tx_record.name] + return tx_list async def filter_spam(self, new_coin_state: List[CoinState]) -> List[CoinState]: xch_spam_amount = self.config.get("xch_spam_amount", 1000000) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index fb7621738197..78f5057e0e6c 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -352,6 +352,8 @@ async def test_cat_trades( tx_config, fee=uint64(1), ) + for tx in tx_records: + await wallet_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -446,6 +448,8 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) + for tx in tx_records: + await wallet_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -497,6 +501,8 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) + for tx in tx_records: + await wallet_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -577,6 +583,8 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) + for tx in tx_records: + await wallet_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -638,6 +646,8 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) + for tx in tx_records: + await wallet_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -683,6 +693,8 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) + for tx in tx_records: + await wallet_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -803,6 +815,8 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: # trade_take, tx_records = await trade_manager_taker.respond_to_offer( # Offer.from_bytes(trade_make.offer), # ) + # for tx in tx_records: + # await wallet_taker.wallet_state_manager.add_pending_transaction(tx) # await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) # assert trade_take is not None # assert tx_records is not None @@ -819,6 +833,8 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=fee, secure=True ) + for tx in txs: + await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -859,6 +875,8 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=uint64(0), secure=True ) + for tx in txs: + await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -912,6 +930,8 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=uint64(0), secure=True ) + for tx in txs: + await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -970,12 +990,16 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: peer = wallet_node_taker.get_full_node_peer() offer = Offer.from_bytes(trade_make.offer) tr1, txs1 = await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) + for tx in txs1: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) # we shouldn't be able to respond to a duplicate offer with pytest.raises(ValueError): await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CONFIRM, trade_manager_taker, tr1) # pushing into mempool while already in it should fail tr2, txs2 = await trade_manager_trader.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) + for tx in txs2: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert await trade_manager_trader.get_coins_of_interest() offer_tx_records: List[TransactionRecord] = await wallet_node_maker.wallet_state_manager.tx_store.get_not_sent() await full_node.process_transaction_records(records=offer_tx_records) @@ -1033,6 +1057,8 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: bundle = dataclasses.replace(offer._bundle, aggregated_signature=G2Element()) offer = dataclasses.replace(offer, _bundle=bundle) tr1, txs1 = await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) + for tx in txs1: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) wallet_node_taker.wallet_tx_resend_timeout_secs = 0 # don't wait for resend for _ in range(10): print(await wallet_node_taker._resend_queue()) @@ -1092,5 +1118,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: tr1, txs1 = await trade_manager_taker.respond_to_offer( offer, peer, DEFAULT_TX_CONFIG, fee=uint64(1000000000000) ) + for tx in txs1: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) await full_node.process_transaction_records(records=txs1) await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, tr1) diff --git a/tests/wallet/db_wallet/test_dl_offers.py b/tests/wallet/db_wallet/test_dl_offers.py index fc4d0d99e6e8..6397ffe5312c 100644 --- a/tests/wallet/db_wallet/test_dl_offers.py +++ b/tests/wallet/db_wallet/test_dl_offers.py @@ -174,6 +174,8 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ), fee=fee, ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert offer_taker is not None assert tx_records is not None @@ -295,6 +297,8 @@ async def test_dl_offer_cancellation(wallets_prefarm: Any, trusted: bool) -> Non cancellation_txs = await trade_manager.cancel_pending_offers( [offer.trade_id], DEFAULT_TX_CONFIG, fee=uint64(2_000_000_000_000), secure=True ) + for tx in cancellation_txs: + await trade_manager.wallet_state_manager.add_pending_transaction(tx) assert len(cancellation_txs) == 2 await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager, offer) await full_node_api.process_transaction_records(records=cancellation_txs) @@ -472,6 +476,8 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ), fee=fee, ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert offer_taker is not None assert tx_records is not None diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index a5dbfae115ac..6c2b53eb94e1 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -206,9 +206,11 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes pubkey = bytes( (await did_wallet_2.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id)).pubkey ) - message_spend_bundle, attest_data = await did_wallet_0.create_attestment( + message_tx, message_spend_bundle, attest_data = await did_wallet_0.create_attestment( did_wallet_2.did_info.temp_coin.name(), newpuzhash, pubkey, DEFAULT_TX_CONFIG ) + await did_wallet_0.wallet_state_manager.add_pending_transaction(message_tx) + assert message_spend_bundle is not None spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_0.id() ) @@ -224,15 +226,19 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes ) = await did_wallet_2.load_attest_files_for_recovery_spend([attest_data]) assert message_spend_bundle == test_message_spend_bundle - spend_bundle = await did_wallet_2.recovery_spend( + txs = await did_wallet_2.recovery_spend( did_wallet_2.did_info.temp_coin, newpuzhash, test_info_list, pubkey, test_message_spend_bundle, ) + assert txs[0].spend_bundle is not None + await did_wallet_2.wallet_state_manager.add_pending_transaction(txs[0]) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + await time_out_assert_not_none( + 5, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() + ) await full_node_api.farm_blocks_to_wallet(1, wallet_0) @@ -243,7 +249,9 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes assert wallet.wallet_state_manager.wallets[wallet.id()] == wallet some_ph = 32 * b"\2" - await did_wallet_2.create_exit_spend(some_ph, DEFAULT_TX_CONFIG) + txs = await did_wallet_2.create_exit_spend(some_ph, DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet_2.wallet_state_manager.add_pending_transaction(tx) spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_2.id() @@ -367,16 +375,20 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w await did_wallet_4.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id) ).pubkey new_ph = did_wallet_4.did_info.temp_puzhash - message_spend_bundle, attest1 = await did_wallet.create_attestment( + message_tx, message_spend_bundle, attest1 = await did_wallet.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) + await did_wallet.wallet_state_manager.add_pending_transaction(message_tx) + assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) - message_spend_bundle2, attest2 = await did_wallet_2.create_attestment( + message_tx2, message_spend_bundle2, attest2 = await did_wallet_2.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) + await did_wallet_2.wallet_state_manager.add_pending_transaction(message_tx2) + assert message_spend_bundle2 is not None spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_2.id() ) @@ -394,7 +406,8 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w await full_node_api.farm_blocks_to_wallet(1, wallet) await time_out_assert(15, did_wallet_4.get_confirmed_balance, 0) await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 0) - await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, message_spend_bundle) + txs = await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, message_spend_bundle) + await did_wallet_4.wallet_state_manager.add_pending_transaction(txs[0]) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_4.id() ) @@ -459,9 +472,7 @@ async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes info = Program.to([]) pubkey = (await did_wallet.wallet_state_manager.get_unused_derivation_record(did_wallet.wallet_info.id)).pubkey try: - spend_bundle = await did_wallet.recovery_spend( - coin, ph, info, pubkey, SpendBundle([], AugSchemeMPL.aggregate([])) - ) + await did_wallet.recovery_spend(coin, ph, info, pubkey, SpendBundle([], AugSchemeMPL.aggregate([]))) except Exception: # We expect a CLVM 80 error for this test pass @@ -531,7 +542,9 @@ async def test_did_find_lost_did(self, self_hostname, two_wallet_nodes, trusted) recovery_list = [bytes32.fromhex(did_wallet.get_my_DID())] await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list - await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) + txs = await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet.wallet_state_manager.add_pending_transaction(tx) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) @@ -611,7 +624,9 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, recovery_list = [bytes.fromhex(did_wallet_2.get_my_DID())] await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list - await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) + txs = await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet.wallet_state_manager.add_pending_transaction(tx) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -638,7 +653,11 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, await did_wallet_3.wallet_state_manager.get_unused_derivation_record(did_wallet_3.wallet_info.id) ).pubkey await time_out_assert(15, did_wallet.get_confirmed_balance, 101) - attest_data = (await did_wallet.create_attestment(coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG))[1] + message_tx, message_spend_bundle, attest_data = await did_wallet.create_attestment( + coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG + ) + await did_wallet.wallet_state_manager.add_pending_transaction(message_tx) + assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle @@ -649,7 +668,8 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, info, message_spend_bundle, ) = await did_wallet_3.load_attest_files_for_recovery_spend([attest_data]) - await did_wallet_3.recovery_spend(coin, new_ph, info, pubkey, message_spend_bundle) + txs = await did_wallet_3.recovery_spend(coin, new_ph, info, pubkey, message_spend_bundle) + await did_wallet_3.wallet_state_manager.add_pending_transaction(txs[0]) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_3.id() ) @@ -676,7 +696,11 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, pubkey = ( await did_wallet_4.wallet_state_manager.get_unused_derivation_record(did_wallet_4.wallet_info.id) ).pubkey - attest1 = (await did_wallet_3.create_attestment(coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG))[1] + message_tx, message_spend_bundle, attest1 = await did_wallet_3.create_attestment( + coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG + ) + await did_wallet_3.wallet_state_manager.add_pending_transaction(message_tx) + assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_3.id() ) @@ -689,7 +713,8 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, test_info_list, test_message_spend_bundle, ) = await did_wallet_4.load_attest_files_for_recovery_spend([attest1]) - await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, test_message_spend_bundle) + txs = await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, test_message_spend_bundle) + await did_wallet_2.wallet_state_manager.add_pending_transaction(txs[0]) spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_4.id() @@ -764,7 +789,9 @@ async def test_did_transfer(self, self_hostname, two_wallet_nodes, with_recovery await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) # Transfer DID new_puzhash = await wallet2.get_new_puzzlehash() - await did_wallet_1.transfer_did(new_puzhash, fee, with_recovery, DEFAULT_TX_CONFIG) + txs = await did_wallet_1.transfer_did(new_puzhash, fee, with_recovery, DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet_1.wallet_state_manager.add_pending_transaction(tx) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) @@ -840,7 +867,9 @@ async def test_update_recovery_list(self, self_hostname, two_wallet_nodes, trust await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) await did_wallet_1.update_recovery_list([bytes(ph)], 1) - await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG) + txs = await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet_1.wallet_state_manager.add_pending_transaction(tx) await full_node_api.farm_blocks_to_wallet(1, wallet) await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) @@ -1046,7 +1075,9 @@ async def test_update_metadata(self, self_hostname, two_wallet_nodes, trusted): metadata = {} metadata["Twitter"] = "http://www.twitter.com" await did_wallet_1.update_metadata(metadata) - await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG, fee) + txs = await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG, fee) + for tx in txs: + await did_wallet_1.wallet_state_manager.add_pending_transaction(tx) transaction_records = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) diff --git a/tests/wallet/nft_wallet/test_nft_1_offers.py b/tests/wallet/nft_wallet/test_nft_1_offers.py index e132dd53d86e..401bdce138d6 100644 --- a/tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/tests/wallet/nft_wallet/test_nft_1_offers.py @@ -129,7 +129,7 @@ async def test_nft_offer_sell_nft( ] ) - sb = await nft_wallet_maker.generate_new_nft( + txs = await nft_wallet_maker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -137,10 +137,13 @@ async def test_nft_offer_sell_nft( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -182,6 +185,8 @@ async def test_nft_offer_sell_nft( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(20, mempool_not_empty, True, full_node_api) @@ -283,7 +288,7 @@ async def test_nft_offer_request_nft( await time_out_assert(20, wallet_taker.get_unconfirmed_balance, funds - 1) await time_out_assert(20, wallet_taker.get_confirmed_balance, funds - 1) - sb = await nft_wallet_taker.generate_new_nft( + txs = await nft_wallet_taker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -291,10 +296,13 @@ async def test_nft_offer_request_nft( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -338,6 +346,8 @@ async def test_nft_offer_request_nft( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None @@ -436,7 +446,7 @@ async def test_nft_offer_sell_did_to_did( await time_out_assert(20, wallet_maker.get_unconfirmed_balance, funds - 1) await time_out_assert(20, wallet_maker.get_confirmed_balance, funds - 1) - sb = await nft_wallet_maker.generate_new_nft( + txs = await nft_wallet_maker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -444,10 +454,13 @@ async def test_nft_offer_sell_did_to_did( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -506,6 +519,8 @@ async def test_nft_offer_sell_did_to_did( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -610,7 +625,7 @@ async def test_nft_offer_sell_nft_for_cat( ] ) - sb = await nft_wallet_maker.generate_new_nft( + txs = await nft_wallet_maker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -618,10 +633,13 @@ async def test_nft_offer_sell_nft_for_cat( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -706,6 +724,8 @@ async def test_nft_offer_sell_nft_for_cat( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -807,7 +827,7 @@ async def test_nft_offer_request_nft_for_cat( ] ) - sb = await nft_wallet_taker.generate_new_nft( + txs = await nft_wallet_taker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -815,10 +835,13 @@ async def test_nft_offer_request_nft_for_cat( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -912,6 +935,8 @@ async def test_nft_offer_request_nft_for_cat( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -1005,7 +1030,7 @@ async def test_nft_offer_sell_cancel( ] ) - sb = await nft_wallet_maker.generate_new_nft( + txs = await nft_wallet_maker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -1013,10 +1038,13 @@ async def test_nft_offer_sell_cancel( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker], timeout=20) @@ -1044,6 +1072,8 @@ async def test_nft_offer_sell_cancel( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=FEE, secure=True ) + for tx in txs: + await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) @@ -1127,7 +1157,7 @@ async def test_nft_offer_sell_cancel_in_batch( ] ) - sb = await nft_wallet_maker.generate_new_nft( + txs = await nft_wallet_maker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash, @@ -1135,10 +1165,13 @@ async def test_nft_offer_sell_cancel_in_batch( royalty_basis_pts, did_id, ) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) @@ -1167,6 +1200,8 @@ async def test_nft_offer_sell_cancel_in_batch( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=FEE, secure=True ) + for tx in txs: + await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) @@ -1346,7 +1381,7 @@ async def test_complex_nft_offer( ) return else: - sb_maker = await nft_wallet_maker.generate_new_nft( + txs = await nft_wallet_maker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash_maker, @@ -1354,8 +1389,14 @@ async def test_complex_nft_offer( uint16(royalty_basis_pts_maker), did_id_maker, ) - - sb_taker_1 = await nft_wallet_taker.generate_new_nft( + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) + + txs = await nft_wallet_taker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash_taker, @@ -1363,10 +1404,12 @@ async def test_complex_nft_offer( royalty_basis_pts_taker_1, did_id_taker, ) - assert sb_maker is not None - assert sb_taker_1 is not None - await time_out_assert_not_none(10, full_node_api.full_node.mempool_manager.get_spendbundle, sb_maker.name()) - await time_out_assert_not_none(10, full_node_api.full_node.mempool_manager.get_spendbundle, sb_taker_1.name()) + for tx in txs: + await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) funds_maker -= 1 @@ -1380,7 +1423,7 @@ async def test_complex_nft_offer( await time_out_assert(30, get_nft_count, 1, nft_wallet_taker) # MAke one more NFT for the taker - sb_taker_2 = await nft_wallet_taker.generate_new_nft( + txs = await nft_wallet_taker.generate_new_nft( metadata, DEFAULT_TX_CONFIG, target_puzhash_taker, @@ -1388,8 +1431,12 @@ async def test_complex_nft_offer( royalty_basis_pts_taker_2, did_id_taker, ) - assert sb_taker_2 is not None - await time_out_assert_not_none(10, full_node_api.full_node.mempool_manager.get_spendbundle, sb_taker_2.name()) + for tx in txs: + await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) funds_taker -= 1 @@ -1458,6 +1505,8 @@ async def test_complex_nft_offer( DEFAULT_TX_CONFIG, fee=FEE, ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None await full_node_api.process_transaction_records(records=tx_records) @@ -1559,6 +1608,8 @@ async def get_cat_wallet_and_check_balance(asset_id: str, wsm: Any) -> uint128: DEFAULT_TX_CONFIG, fee=uint64(0), ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None await time_out_assert(20, mempool_not_empty, True, full_node_api) diff --git a/tests/wallet/nft_wallet/test_nft_offers.py b/tests/wallet/nft_wallet/test_nft_offers.py index a636d89ae994..98bfd00f891f 100644 --- a/tests/wallet/nft_wallet/test_nft_offers.py +++ b/tests/wallet/nft_wallet/test_nft_offers.py @@ -102,9 +102,13 @@ async def test_nft_offer_with_fee( ] ) - sb = await nft_wallet_maker.generate_new_nft(metadata, tx_config) - assert sb - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_maker.generate_new_nft(metadata, tx_config) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) @@ -147,6 +151,8 @@ async def test_nft_offer_with_fee( tx_config, fee=taker_fee, ) + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -214,7 +220,8 @@ async def test_nft_offer_with_fee( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, tx_config, fee=taker_fee ) - + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -294,9 +301,13 @@ async def test_nft_offer_cancellations( ] ) - sb = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert sb - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) @@ -331,6 +342,8 @@ async def test_nft_offer_cancellations( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=cancel_fee, secure=True ) + for tx in txs: + await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) await time_out_assert(20, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node_api.process_transaction_records(records=txs) @@ -411,9 +424,13 @@ async def test_nft_offer_with_metadata_update( ] ) - sb = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert sb - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) @@ -426,11 +443,12 @@ async def test_nft_offer_with_metadata_update( url_to_add = "https://new_url.com" key = "mu" fee_for_update = uint64(10) - update_sb = await nft_wallet_maker.update_metadata( - nft_to_update, key, url_to_add, DEFAULT_TX_CONFIG, fee=fee_for_update - ) + txs = await nft_wallet_maker.update_metadata(nft_to_update, key, url_to_add, DEFAULT_TX_CONFIG, fee=fee_for_update) mempool_mgr = full_node_api.full_node.mempool_manager - await time_out_assert_not_none(20, mempool_mgr.get_spendbundle, update_sb.name()) # type: ignore + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none(20, mempool_mgr.get_spendbundle, tx.spend_bundle.name()) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) @@ -467,7 +485,8 @@ async def test_nft_offer_with_metadata_update( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=taker_fee ) - + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -553,9 +572,13 @@ async def test_nft_offer_nft_for_cat( ] ) - sb = await nft_wallet_maker.generate_new_nft(metadata, tx_config) - assert sb - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_maker.generate_new_nft(metadata, tx_config) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) @@ -641,7 +664,8 @@ async def test_nft_offer_nft_for_cat( tx_config, fee=taker_fee, ) - + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -721,7 +745,8 @@ async def test_nft_offer_nft_for_cat( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, tx_config, fee=taker_fee ) - + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None @@ -809,9 +834,13 @@ async def test_nft_offer_nft_for_nft( ] ) - sb = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert sb - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) metadata_2 = Program.to( [ @@ -819,9 +848,14 @@ async def test_nft_offer_nft_for_nft( ("h", "0xD4584AD463139FA8C0D9F68F4B59F183"), ] ) - sb_2 = await nft_wallet_taker.generate_new_nft(metadata_2, DEFAULT_TX_CONFIG) - assert sb_2 - await time_out_assert_not_none(20, full_node_api.full_node.mempool_manager.get_spendbundle, sb_2.name()) + + txs = await nft_wallet_taker.generate_new_nft(metadata_2, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) @@ -863,7 +897,8 @@ async def test_nft_offer_nft_for_nft( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=taker_fee ) - + for tx in tx_records: + await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) assert trade_take is not None assert tx_records is not None diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py index 390308157315..c3773aa47368 100644 --- a/tests/wallet/nft_wallet/test_nft_wallet.py +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -23,6 +23,7 @@ from chia.util.ints import uint16, uint32, uint64 from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.nft_wallet.nft_wallet import NFTWallet +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.address_type import AddressType from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG @@ -139,9 +140,13 @@ async def test_nft_wallet_creation_automatically(self_hostname: str, two_wallet_ ] ) - sb = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert sb - await time_out_assert_not_none(30, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_0.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) @@ -234,11 +239,14 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n await time_out_assert(30, wallet_0.get_unconfirmed_balance, 2000000000000) await time_out_assert(30, wallet_0.get_confirmed_balance, 2000000000000) - sb = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(30, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_0.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -268,11 +276,14 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n await time_out_assert(10, wallet_0.get_unconfirmed_balance, 4000000000000 - 1) await time_out_assert(10, wallet_0.get_confirmed_balance, 4000000000000) - sb = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert sb - # ensure hints are generated - assert compute_memos(sb) - await time_out_assert_not_none(10, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await nft_wallet_0.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await time_out_assert(30, wallet_node_0.wallet_state_manager.lock.locked, False) for i in range(1, num_blocks * 2): @@ -1001,10 +1012,13 @@ async def test_nft_transfer_nft_with_did(self_hostname: str, two_wallet_nodes: A await full_node_api.wait_for_wallet_synced(wallet_node_0, 20) await full_node_api.wait_for_wallet_synced(wallet_node_1, 20) # transfer DID to the other wallet - tx = await did_wallet.transfer_did(ph1, uint64(0), True, DEFAULT_TX_CONFIG) - assert tx - assert tx.spend_bundle is not None - await time_out_assert_not_none(30, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name()) + txs = await did_wallet.transfer_did(ph1, uint64(0), True, DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 30, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) await full_node_api.wait_for_wallet_synced(wallet_node_0, 20) @@ -1050,6 +1064,9 @@ async def test_nft_transfer_nft_with_did(self_hostname: str, two_wallet_nodes: A resp = await api_1.nft_set_nft_did( dict(wallet_id=nft_wallet_id_1, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex(), fee=fee) ) + txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] + for tx in txs: + await did_wallet.wallet_state_manager.add_pending_transaction(tx) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( @@ -1619,6 +1636,9 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A resp = await api_0.nft_set_nft_did( dict(wallet_id=nft_wallet_0_id, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex()) ) + txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] + for tx in txs: + await did_wallet1.wallet_state_manager.add_pending_transaction(tx) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( 30, api_0.nft_get_by_did, [dict(did_id=hmr_did_id)], lambda x: x.get("wallet_id", 0) > 0 @@ -1649,7 +1669,9 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A resp = await api_0.nft_set_nft_did( dict(wallet_id=nft_wallet_1_id, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex()) ) - + txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] + for tx in txs: + await did_wallet1.wallet_state_manager.add_pending_transaction(tx) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( 30, api_0.nft_get_by_did, [dict(did_id=hmr_did_id)], lambda x: x.get("wallet_id") is not None @@ -1673,6 +1695,9 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A assert coins[0] == resp["nft_info"] # Test set DID2 -> None resp = await api_0.nft_set_nft_did(dict(wallet_id=nft_wallet_2_id, nft_coin_id=nft_coin_id.hex())) + txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] + for tx in txs: + await did_wallet1.wallet_state_manager.add_pending_transaction(tx) await make_new_block_with(resp, full_node_api, ph) # Check NFT DID diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 8e2f1e055527..dcdea34320c6 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -355,6 +355,10 @@ async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironmen ) await client.push_transactions([tx]) + resp = await client.fetch("push_transactions", {"transactions": [tx.to_json_dict_convenience(wallet_node.config)]}) + assert resp["success"] + resp = await client.fetch("push_transactions", {"transactions": [tx.to_json_dict()]}) + assert resp["success"] spend_bundle = tx.spend_bundle assert spend_bundle is not None @@ -523,6 +527,7 @@ async def test_create_signed_transaction( tx_config=DEFAULT_TX_CONFIG.override( excluded_coin_amounts=[uint64(selected_coin[0].amount)] if selected_coin is not None else [], ), + push=True, ) change_expected = not selected_coin or selected_coin[0].amount - amount_total > 0 assert_tx_amounts(tx, outputs, amount_fee=amount_fee, change_expected=change_expected, is_cat=is_cat) @@ -530,8 +535,6 @@ async def test_create_signed_transaction( # Farm the transaction and make sure the wallet balance reflects it correct spend_bundle = tx.spend_bundle assert spend_bundle is not None - push_res = await wallet_1_rpc.push_transactions([tx]) - assert push_res["success"] await farm_transaction(full_node_api, wallet_1_node, spend_bundle) await time_out_assert(20, get_confirmed_balance, generated_funds - amount_total, wallet_1_rpc, wallet_id) @@ -758,12 +761,12 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron resp = await wallet_2_rpc.spend_clawback_coins([clawback_coin_id_1, clawback_coin_id_2], 100) assert resp["success"] assert len(resp["transaction_ids"]) == 2 - await time_out_assert_not_none( - 10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0]) - ) - await time_out_assert_not_none( - 10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][1]) - ) + for _tx in resp["transactions"]: + tx = TransactionRecord.from_json_dict_convenience(_tx) + if tx.spend_bundle is not None: + await time_out_assert_not_none( + 10, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) await farm_transaction_block(full_node_api, wallet_2_node) await time_out_assert(20, get_confirmed_balance, generated_funds + 300, wallet_2_rpc, 1) # Test spent coin @@ -1471,10 +1474,9 @@ async def num_wallets() -> int: assert metadata["Twitter"] == "Https://test" last_did_coin = await did_wallet_2.get_coin() - bundle = SpendBundle.from_json_dict( - (await wallet_2_rpc.did_message_spend(did_wallet_2.id(), [], [], DEFAULT_TX_CONFIG))["spend_bundle"] + SpendBundle.from_json_dict( + (await wallet_2_rpc.did_message_spend(did_wallet_2.id(), [], [], DEFAULT_TX_CONFIG, push=True))["spend_bundle"] ) - await env.full_node.rpc_client.push_tx(bundle) await wallet_2_node.wallet_state_manager.add_interested_coin_ids([last_did_coin.name()]) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) @@ -1484,14 +1486,13 @@ async def num_wallets() -> int: assert next_did_coin.parent_coin_info == last_did_coin.name() last_did_coin = next_did_coin - bundle = SpendBundle.from_json_dict( + SpendBundle.from_json_dict( ( await wallet_2_rpc.did_message_spend( - did_wallet_2.id(), [], [], DEFAULT_TX_CONFIG.override(reuse_puzhash=True) + did_wallet_2.id(), [], [], DEFAULT_TX_CONFIG.override(reuse_puzhash=True), push=True ) - )["spend_bundle"] + )["spend_bundle"], ) - await env.full_node.rpc_client.push_tx(bundle) await wallet_2_node.wallet_state_manager.add_interested_coin_ids([last_did_coin.name()]) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index 2342f2793d91..fc343a2707e4 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -1125,14 +1125,16 @@ async def test_dusted_wallet( ("h", "0xD4584AD463139FA8C0D9F68F4B59F185"), ] ) - farm_sb = await farm_nft_wallet.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - assert farm_sb - - # ensure hints are generated - assert compute_memos(farm_sb) + txs = await farm_nft_wallet.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + for tx in txs: + await farm_nft_wallet.wallet_state_manager.add_pending_transaction(tx) + if tx.spend_bundle is not None: + assert compute_memos(tx.spend_bundle) + await time_out_assert_not_none( + 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + ) # Farm a new block - await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, farm_sb.name()) await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(farm_ph)) await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20) diff --git a/tests/wallet/vc_wallet/test_vc_wallet.py b/tests/wallet/vc_wallet/test_vc_wallet.py index b8d9e3323970..8a79aff98092 100644 --- a/tests/wallet/vc_wallet/test_vc_wallet.py +++ b/tests/wallet/vc_wallet/test_vc_wallet.py @@ -451,7 +451,9 @@ async def test_self_revoke( new_vc_record.vc.launcher_id, DEFAULT_TX_CONFIG, new_proof_hash=bytes32([0] * 32), self_revoke=True ) - await did_wallet.transfer_did(bytes32([0] * 32), uint64(0), False, DEFAULT_TX_CONFIG) + txs = await did_wallet.transfer_did(bytes32([0] * 32), uint64(0), False, DEFAULT_TX_CONFIG) + for tx in txs: + await did_wallet.wallet_state_manager.add_pending_transaction(tx) spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) From 55f519df24470885457883e13eacc3a71ceee16f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 10 Oct 2023 14:31:48 -0700 Subject: [PATCH 002/274] Remove ignore_max_send_amount --- chia/data_layer/data_layer_wallet.py | 3 --- chia/pools/pool_wallet.py | 2 -- chia/rpc/wallet_rpc_api.py | 2 -- chia/wallet/cat_wallet/cat_wallet.py | 5 ----- chia/wallet/did_wallet/did_wallet.py | 1 - chia/wallet/nft_wallet/nft_wallet.py | 2 -- chia/wallet/trade_manager.py | 2 -- chia/wallet/vc_wallet/cr_cat_wallet.py | 6 ------ chia/wallet/wallet.py | 8 -------- 9 files changed, 31 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index ec38ee91e700..542fa20099f8 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -322,7 +322,6 @@ async def generate_new_reporter( origin_id=launcher_parent.name(), coins=coins, primaries=None, - ignore_max_send_amount=False, coin_announcements_to_consume={announcement}, extra_conditions=extra_conditions, ) @@ -645,7 +644,6 @@ async def generate_signed_transaction( memos: Optional[List[List[bytes]]] = None, # ignored coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, - ignore_max_send_amount: bool = False, # ignored extra_conditions: Tuple[Condition, ...] = tuple(), **kwargs: Unpack[GSTOptionalArgs], ) -> List[TransactionRecord]: @@ -758,7 +756,6 @@ async def create_new_mirror( fee=fee, primaries=[], memos=[launcher_id, *(url for url in urls)], - ignore_max_send_amount=False, extra_conditions=extra_conditions, ) assert create_mirror_tx_record.spend_bundle is not None diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 00201d58a6de..e30dc53ffe50 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -516,7 +516,6 @@ async def generate_fee_transaction( origin_id=None, coins=None, primaries=None, - ignore_max_send_amount=False, coin_announcements_to_consume=coin_announcements, ) return fee_tx @@ -705,7 +704,6 @@ async def generate_launcher_spend( fee, coins, None, - False, announcement_set, origin_id=launcher_parent.name(), extra_conditions=extra_conditions, diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 1b39fbcb907d..04d642d8b6c2 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -3363,7 +3363,6 @@ async def _generate_signed_transaction() -> EndpointResult: tx_config, fee, coins=coins, - ignore_max_send_amount=True, primaries=additional_outputs, memos=memos_0, coin_announcements_to_consume=coin_announcements, @@ -3383,7 +3382,6 @@ async def _generate_signed_transaction() -> EndpointResult: tx_config, fee, coins=coins, - ignore_max_send_amount=True, memos=[memos_0] + [output.memos for output in additional_outputs], coin_announcements_to_consume=coin_announcements, puzzle_announcements_to_consume=puzzle_announcements, diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index f90c9ce13b23..b3e753c9b61f 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -788,7 +788,6 @@ async def generate_signed_transaction( tx_config: TXConfig, fee: uint64 = uint64(0), coins: Optional[Set[Coin]] = None, - ignore_max_send_amount: bool = False, memos: Optional[List[List[bytes]]] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, @@ -810,10 +809,6 @@ async def generate_signed_transaction( payments.append(Payment(puzhash, amount, memos_with_hint)) payment_sum = sum([p.amount for p in payments]) - if not ignore_max_send_amount: - max_send = await self.get_max_send_amount() - if payment_sum > max_send: - raise ValueError(f"Can't send more than {max_send} mojos in a single transaction") unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( payments, tx_config, diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 5bcbc1469018..c769308e1cc6 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -1250,7 +1250,6 @@ async def generate_new_decentralised_id( fee, coins, None, - False, announcement_set, origin_id=origin.name(), ) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index d211c5c5e7a8..71fa63bee4b0 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -390,7 +390,6 @@ async def generate_new_nft( fee, coins, None, - False, announcement_set, origin_id=origin.name(), extra_conditions=extra_conditions, @@ -618,7 +617,6 @@ async def generate_signed_transaction( memos: Optional[List[List[bytes]]] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, - ignore_max_send_amount: bool = False, extra_conditions: Tuple[Condition, ...] = tuple(), **kwargs: Unpack[GSTOptionalArgs], ) -> List[TransactionRecord]: diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 5af9a4b3c820..fd7f222fde26 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -284,7 +284,6 @@ async def cancel_pending_offers( ), fee=fee_to_pay, coins=selected_coins, - ignore_max_send_amount=True, extra_conditions=extra_conditions, ) if tx is not None and tx.spend_bundle is not None: @@ -301,7 +300,6 @@ async def cancel_pending_offers( ), fee=fee_to_pay, coins={coin}, - ignore_max_send_amount=True, extra_conditions=extra_conditions, ) for tx in txs: diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index aba001173984..ef575ded3609 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -628,7 +628,6 @@ async def generate_signed_transaction( tx_config: TXConfig, fee: uint64 = uint64(0), coins: Optional[Set[Coin]] = None, - ignore_max_send_amount: bool = False, memos: Optional[List[List[bytes]]] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, @@ -660,11 +659,6 @@ async def generate_signed_transaction( ) ) - payment_sum = sum([p.amount for p in payments]) - if not ignore_max_send_amount: - max_send = await self.get_max_send_amount() - if payment_sum > max_send: - raise ValueError(f"Can't send more than {max_send} mojos in a single transaction") # pragma: no cover unsigned_spend_bundle, other_txs = await self._generate_unsigned_spendbundle( payments, tx_config, diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 96dff41b5198..cbe6a93f249e 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -271,7 +271,6 @@ async def _generate_unsigned_transaction( 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, @@ -293,11 +292,6 @@ async def _generate_unsigned_transaction( 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( @@ -426,7 +420,6 @@ async def generate_signed_transaction( 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, @@ -455,7 +448,6 @@ async def generate_signed_transaction( origin_id, coins, primaries, - ignore_max_send_amount, coin_announcements_to_consume, puzzle_announcements_to_consume, memos, From 5102b47846b1369c6e0a52e3cd002ccdc4684bd8 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 10 Oct 2023 14:17:13 -0700 Subject: [PATCH 003/274] Use TXConfig instead of pending_transactions for coin exclusion in spend_clawback_coins --- chia/rpc/wallet_rpc_api.py | 17 +++++++++++------ chia/wallet/wallet_state_manager.py | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 6b2a8b5da9f2..4d98c8b26fb5 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1142,10 +1142,14 @@ async def spend_clawback_coins( new_txs = await self.service.wallet_state_manager.spend_clawback_coins( coins, tx_fee, tx_config, request.get("force", False), extra_conditions=extra_conditions ) - if push: - for tx in new_txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) tx_list.extend(new_txs) + tx_config = dataclasses.replace( + tx_config, + excluded_coin_ids=[ + *tx_config.excluded_coin_ids, + *(c.name() for tx in new_txs for c in tx.removals), + ], + ) coins = {} except Exception as e: log.error(f"Failed to spend clawback coin {coin_id.hex()}: %s", e) @@ -1153,11 +1157,12 @@ async def spend_clawback_coins( new_txs = await self.service.wallet_state_manager.spend_clawback_coins( coins, tx_fee, tx_config, request.get("force", False), extra_conditions=extra_conditions ) - if push: - for tx in new_txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) tx_list.extend(new_txs) + if push: + for tx in tx_list: + await self.service.wallet_state_manager.add_pending_transaction(tx) + return { "success": True, "transaction_ids": [tx.name.hex() for tx in tx_list if tx.type == TransactionType.OUTGOING_CLAWBACK.value], diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index dc95c4d47c58..1aebb70bb16e 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -800,6 +800,7 @@ async def auto_claim_coins(self) -> None: stop=tx_config.coin_selection_config.max_coin_amount, ), ) + all_txs: List[TransactionRecord] = [] for coin in unspent_coins.records: try: metadata: MetadataTypes = coin.parsed_metadata() @@ -810,15 +811,22 @@ async def auto_claim_coins(self) -> None: clawback_coins[coin.coin] = metadata if len(clawback_coins) >= self.config.get("auto_claim", {}).get("batch_size", 50): txs = await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config) - for tx in txs: - await self.add_pending_transaction(tx) + all_txs.extend(txs) + tx_config = dataclasses.replace( + tx_config, + excluded_coin_ids=[ + *tx_config.excluded_coin_ids, + *(c.name() for tx in txs for c in tx.removals), + ], + ) 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: - txs = await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config) - for tx in txs: - await self.add_pending_transaction(tx) + all_txs.extend(await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config)) + + for tx in all_txs: + await self.add_pending_transaction(tx) async def spend_clawback_coins( self, From 1bfe54ea859ad5f0128460b4f771ed98aa6b3480 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 11 Oct 2023 14:21:41 -0700 Subject: [PATCH 004/274] Unify transaction pushing into a single method --- chia/data_layer/data_layer_wallet.py | 3 +- chia/pools/pool_wallet.py | 6 +- chia/rpc/wallet_rpc_api.py | 114 +++++++----------- chia/simulator/full_node_simulator.py | 2 +- chia/wallet/cat_wallet/cat_wallet.py | 3 +- chia/wallet/did_wallet/did_wallet.py | 3 +- chia/wallet/wallet.py | 5 - chia/wallet/wallet_state_manager.py | 33 +++-- tests/core/full_node/test_full_node.py | 16 +-- tests/core/full_node/test_transactions.py | 4 +- tests/simulation/test_simulation.py | 6 +- tests/simulation/test_simulator.py | 4 +- tests/wallet/cat_wallet/test_cat_wallet.py | 39 +++--- tests/wallet/cat_wallet/test_trades.py | 42 +++---- tests/wallet/db_wallet/test_dl_offers.py | 36 ++---- tests/wallet/db_wallet/test_dl_wallet.py | 47 +++----- tests/wallet/did_wallet/test_did.py | 37 +++--- tests/wallet/nft_wallet/test_nft_1_offers.py | 53 ++++---- tests/wallet/nft_wallet/test_nft_offers.py | 35 +++--- tests/wallet/nft_wallet/test_nft_wallet.py | 26 ++-- tests/wallet/rpc/test_wallet_rpc.py | 2 +- .../simple_sync/test_simple_sync_protocol.py | 10 +- tests/wallet/sync/test_wallet_sync.py | 6 +- tests/wallet/test_notifications.py | 2 +- tests/wallet/test_wallet.py | 42 +++---- tests/wallet/test_wallet_retry.py | 2 +- tests/wallet/vc_wallet/test_vc_wallet.py | 7 +- 27 files changed, 243 insertions(+), 342 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index ec38ee91e700..4c7ddcf424e2 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -1029,8 +1029,7 @@ async def potentially_handle_resubmit(self, launcher_id: bytes32) -> None: # pr fee=fee, ) ) - for tx in all_txs: - await self.wallet_state_manager.add_pending_transaction(tx) + await self.wallet_state_manager.add_pending_transactions(all_txs) except Exception as e: self.log.warning(f"Something went wrong during attempted DL resubmit: {str(e)}") # Something went wrong so let's delete anything pending that was created diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 83cb38047ffc..45733d227fcc 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -882,6 +882,7 @@ async def claim_pool_rewards( fee_tx = await self.generate_fee_transaction(fee, tx_config, coin_announcements={absorb_announce}) assert fee_tx.spend_bundle is not None full_spend = SpendBundle.aggregate([fee_tx.spend_bundle, claim_spend]) + fee_tx = dataclasses.replace(fee_tx, spend_bundle=None) assert full_spend.fees() == fee current_time = uint64(int(time.time())) @@ -953,9 +954,10 @@ async def new_peak(self, peak_height: uint32) -> None: travel_tx, fee_tx = await self.generate_travel_transactions( self.next_transaction_fee, self.next_tx_config ) - await self.wallet_state_manager.add_pending_transaction(travel_tx) + txs = [travel_tx] if fee_tx is not None: - await self.wallet_state_manager.add_pending_transaction(fee_tx) + txs.append(fee_tx) + await self.wallet_state_manager.add_pending_transactions(txs) async def have_unconfirmed_transaction(self) -> bool: unconfirmed: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 4d98c8b26fb5..933b98d579fb 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -551,8 +551,6 @@ async def push_tx(self, request: Dict[str, Any]) -> EndpointResult: return {} async def push_transactions(self, request: Dict[str, Any]) -> EndpointResult: - wallet = self.service.wallet_state_manager.main_wallet - txs: List[TransactionRecord] = [] for transaction_hexstr_or_json in request["transactions"]: if isinstance(transaction_hexstr_or_json, str): @@ -566,8 +564,7 @@ async def push_transactions(self, request: Dict[str, Any]) -> EndpointResult: txs.append(tx) async with self.service.wallet_state_manager.lock: - for tx in txs: - await wallet.push_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return {} @@ -836,8 +833,7 @@ async def create_new_wallet( delayed_address, extra_conditions=extra_conditions, ) - if push: - await self.service.wallet_state_manager.add_pending_transaction(tr) + await self.service.wallet_state_manager.add_pending_transactions([tr]) except Exception as e: raise ValueError(str(e)) return { @@ -1065,8 +1061,7 @@ async def send_transaction( puzzle_decorator_override=request.get("puzzle_decorator", None), extra_conditions=extra_conditions, ) - if push: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions([tx]) # Transaction may not have been included in the mempool yet. Use get_transaction to check. json_tx = tx.to_json_dict_convenience(self.service.config) @@ -1160,8 +1155,7 @@ async def spend_clawback_coins( tx_list.extend(new_txs) if push: - for tx in tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(tx_list, merge_spends=False) return { "success": True, @@ -1429,7 +1423,7 @@ async def send_notification( extra_conditions=extra_conditions, ) if push: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions([tx]) json_tx = tx.to_json_dict_convenience(self.service.config) return {"tx": json_tx, "transactions": [json_tx]} @@ -1677,8 +1671,7 @@ async def cat_spend( extra_conditions=extra_conditions, ) if push: - for tx in txs: - await wallet.standard_wallet.push_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) else: txs = await wallet.generate_signed_transaction( amounts, @@ -1691,8 +1684,7 @@ async def cat_spend( extra_conditions=extra_conditions, ) if push: - for tx in txs: - await wallet.standard_wallet.push_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) # Return the first transaction, which is expected to be the CAT spend. If a fee is # included, it is currently ordered after the CAT spend. @@ -1935,8 +1927,7 @@ async def take_offer( extra_conditions=extra_conditions, ) if push: - for tx in tx_records: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(tx_records) return { "trade_record": trade_record.to_json_dict_convenience(), @@ -2011,8 +2002,7 @@ async def cancel_offer( [bytes32(trade_id)], tx_config, fee=fee, secure=secure, extra_conditions=extra_conditions ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return {"transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs]} @@ -2064,11 +2054,11 @@ async def cancel_offers( continue async with self.service.wallet_state_manager.lock: - all_txs.extend( - await trade_mgr.cancel_pending_offers( - list(records.keys()), tx_config, batch_fee, secure, records, extra_conditions=extra_conditions - ) + batch_txs: List[TransactionRecord] = await trade_mgr.cancel_pending_offers( + list(records.keys()), tx_config, batch_fee, secure, records, extra_conditions=extra_conditions ) + all_txs.extend(batch_txs) + log.info(f"Cancelled offers {start} to {end} ...") # If fewer records were returned than requested, we're done if len(trades) < batch_size: @@ -2077,8 +2067,7 @@ async def cancel_offers( end += batch_size if push: - for tx in all_txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(all_txs, merge_spends=False) return {"success": True, "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in all_txs]} @@ -2123,8 +2112,7 @@ async def did_update_recovery_ids( tx_config, fee=uint64(request.get("fee", 0)), extra_conditions=extra_conditions ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "success": True, "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -2153,7 +2141,7 @@ async def did_message_spend( tx_config, coin_announcements, puzzle_announcements, extra_conditions=extra_conditions ) if push: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions([tx]) return { "success": True, "spend_bundle": tx.spend_bundle, @@ -2419,8 +2407,7 @@ async def did_update_metadata( tx_config, uint64(request.get("fee", 0)), extra_conditions=extra_conditions ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "wallet_id": wallet_id, "success": True, @@ -2504,7 +2491,7 @@ async def did_recovery_spend(self, request: Dict[str, Any]) -> EndpointResult: ) )[0] if request.get("push", True): - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions([tx]) if spend_bundle: return { "success": True, @@ -2618,8 +2605,7 @@ async def did_transfer_did( extra_conditions=extra_conditions, ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "success": True, @@ -2705,8 +2691,7 @@ async def nft_mint_nft( if cs.coin.puzzle_hash == nft_puzzles.LAUNCHER_PUZZLE_HASH: nft_id = encode_puzzle_hash(cs.coin.name(), AddressType.NFT.hrp(self.service.config)) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "wallet_id": wallet_id, "success": True, @@ -2874,8 +2859,7 @@ async def nft_set_did_bulk( refined_tx_list[0] = dataclasses.replace(refined_tx_list[0], spend_bundle=spend_bundle) if push: - for tx in refined_tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(refined_tx_list) for id in coin_ids: await nft_wallet.update_coin_status(id, True) for wallet_id in nft_dict.keys(): @@ -2968,8 +2952,7 @@ async def nft_transfer_bulk( # Add all spend bundles to the first tx refined_tx_list[0] = dataclasses.replace(refined_tx_list[0], spend_bundle=spend_bundle) if push: - for tx in refined_tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(refined_tx_list) for id in coin_ids: await nft_wallet.update_coin_status(id, True) for wallet_id in nft_dict.keys(): @@ -3080,8 +3063,7 @@ async def nft_transfer_nft( if tx.spend_bundle is not None: spend_bundle = tx.spend_bundle if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) await nft_wallet.update_coin_status(nft_coin_info.coin.name(), True) return { "wallet_id": wallet_id, @@ -3186,8 +3168,7 @@ async def nft_add_uri( nft_coin_info, key, uri, tx_config, fee=fee, extra_conditions=extra_conditions ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "wallet_id": wallet_id, "success": True, @@ -3517,7 +3498,7 @@ async def _generate_signed_transaction() -> EndpointResult: ) signed_tx = tx.to_json_dict_convenience(self.service.config) if push: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions([tx]) return {"signed_txs": [signed_tx], "signed_tx": signed_tx, "transactions": [signed_tx]} @@ -3538,8 +3519,7 @@ async def _generate_signed_transaction() -> EndpointResult: ) signed_txs = [tx.to_json_dict_convenience(self.service.config) for tx in txs] if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return {"signed_txs": signed_txs, "signed_tx": signed_txs[0], "transactions": signed_txs} @@ -3585,9 +3565,10 @@ async def pw_join_pool( async with self.service.wallet_state_manager.lock: total_fee, tx, fee_tx = await wallet.join_pool(new_target_state, fee, tx_config) if push: - await self.service.wallet_state_manager.add_pending_transaction(tx) + txs = [tx] if fee_tx is not None: - await self.service.wallet_state_manager.add_pending_transaction(fee_tx) + txs.append(fee_tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "total_fee": total_fee, "transaction": tx, @@ -3620,9 +3601,10 @@ async def pw_self_pool( async with self.service.wallet_state_manager.lock: total_fee, tx, fee_tx = await wallet.self_pool(fee, tx_config) if push: - await self.service.wallet_state_manager.add_pending_transaction(tx) + txs = [tx] if fee_tx is not None: - await self.service.wallet_state_manager.add_pending_transaction(fee_tx) + txs.append(fee_tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "total_fee": total_fee, "transaction": tx, @@ -3655,9 +3637,10 @@ async def pw_absorb_rewards( transaction, fee_tx = await wallet.claim_pool_rewards(fee, max_spends_in_tx, tx_config) state: PoolWalletInfo = await wallet.get_current_state() if push: - await self.service.wallet_state_manager.add_pending_transaction(transaction) + txs = [transaction] if fee_tx is not None: - await self.service.wallet_state_manager.add_pending_transaction(fee_tx) + txs.append(fee_tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "state": state.to_json_dict(), "transaction": transaction, @@ -3710,8 +3693,7 @@ async def create_new_dl( extra_conditions=extra_conditions, ) if push: - await self.service.wallet_state_manager.add_pending_transaction(dl_tx) - await self.service.wallet_state_manager.add_pending_transaction(std_tx) + await self.service.wallet_state_manager.add_pending_transactions([dl_tx, std_tx]) except ValueError as e: log.error(f"Error while generating new reporter {e}") return {"success": False, "error": str(e)} @@ -3802,8 +3784,7 @@ async def dl_update_root( extra_conditions=extra_conditions, ) if push: - for record in records: - await self.service.wallet_state_manager.add_pending_transaction(record) + await self.service.wallet_state_manager.add_pending_transactions(records) return { "tx_record": records[0].to_json_dict_convenience(self.service.config), "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in records], @@ -3843,8 +3824,7 @@ async def dl_update_multiple( modified_txs.append(dataclasses.replace(tx, spend_bundle=None)) modified_txs[0] = dataclasses.replace(modified_txs[0], spend_bundle=aggregate_spend) if push: - for tx in modified_txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(modified_txs) return { "tx_records": [rec.to_json_dict_convenience(self.service.config) for rec in modified_txs], "transactions": [rec.to_json_dict_convenience(self.service.config) for rec in modified_txs], @@ -3915,8 +3895,7 @@ async def dl_new_mirror( extra_conditions=extra_conditions, ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -3945,8 +3924,7 @@ async def dl_delete_mirror( extra_conditions=extra_conditions, ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -3990,8 +3968,7 @@ class VCMint(Streamable): did_id, tx_config, puzhash, parsed_request.fee, extra_conditions=extra_conditions ) if push: - for tx in tx_list: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(tx_list) return { "vc_record": vc_record.to_json_dict(), "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in tx_list], @@ -4083,8 +4060,7 @@ class VCSpend(Streamable): extra_conditions=extra_conditions, ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -4154,8 +4130,7 @@ class VCRevoke(Streamable): extra_conditions=extra_conditions, ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], @@ -4195,8 +4170,7 @@ class CRCATApprovePending(Streamable): extra_conditions=extra_conditions, ) if push: - for tx in txs: - await self.service.wallet_state_manager.add_pending_transaction(tx) + await self.service.wallet_state_manager.add_pending_transactions(txs) return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], diff --git a/chia/simulator/full_node_simulator.py b/chia/simulator/full_node_simulator.py index 5a0763bd11c1..7396312c6c9c 100644 --- a/chia/simulator/full_node_simulator.py +++ b/chia/simulator/full_node_simulator.py @@ -620,7 +620,7 @@ async def create_coins_with_amounts( tx_config=DEFAULT_TX_CONFIG, primaries=outputs_group[1:], ) - await wallet.push_transaction(tx=tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) transaction_records.append(tx) else: break diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index f90c9ce13b23..6340aeff4e95 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -173,8 +173,7 @@ async def create_new_cat_wallet( valid_times=ConditionValidTimes(), ) chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle) - await self.standard_wallet.push_transaction(chia_tx) - await self.standard_wallet.push_transaction(cat_record) + await self.wallet_state_manager.add_pending_transactions([chia_tx, cat_record]) return self @staticmethod diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 6aa8680fb816..b04abd8e6bf5 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -129,8 +129,7 @@ async def create_new_did_wallet( await wallet_state_manager.user_store.delete_wallet(self.id()) raise - for tx in txs: - await self.wallet_state_manager.add_pending_transaction(tx) + await self.wallet_state_manager.add_pending_transactions(txs) await self.wallet_state_manager.add_new_wallet(self) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 96dff41b5198..a6ae7a62130c 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -518,11 +518,6 @@ async def create_tandem_xch_tx( 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], diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 1aebb70bb16e..fb43b02c4a7f 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -825,8 +825,7 @@ async def auto_claim_coins(self) -> None: if len(clawback_coins) > 0: all_txs.extend(await self.spend_clawback_coins(clawback_coins, tx_fee, tx_config)) - for tx in all_txs: - await self.add_pending_transaction(tx) + await self.add_pending_transactions(all_txs) async def spend_clawback_coins( self, @@ -1992,20 +1991,34 @@ async def coin_added( await self.create_more_puzzle_hashes() - async def add_pending_transaction(self, tx_record: TransactionRecord) -> None: + async def add_pending_transactions(self, tx_records: List[TransactionRecord], merge_spends: bool = True) -> None: """ - Called from wallet before new transaction is sent to the full_node + Add a list of transactions to be submitted to the full node. + Aggregates the `spend_bundle` property for each transaction onto the first transaction in the list. """ - # Wallet node will use this queue to retry sending this transaction until full nodes receives it - await self.tx_store.add_transaction_record(tx_record) + agg_spend: SpendBundle = SpendBundle.aggregate( + [tx.spend_bundle for tx in tx_records if tx.spend_bundle is not None] + ) + actual_spend_involved: bool = agg_spend != SpendBundle([], G2Element()) + if merge_spends and actual_spend_involved: + tx_records = [ + dataclasses.replace(tx, spend_bundle=agg_spend if i == 0 else None) for i, tx in enumerate(tx_records) + ] all_coins_names = [] - all_coins_names.extend([coin.name() for coin in tx_record.additions]) - all_coins_names.extend([coin.name() for coin in tx_record.removals]) + async with self.db_wrapper.writer_maybe_transaction(): + for tx_record in tx_records: + # Wallet node will use this queue to retry sending this transaction until full nodes receives it + await self.tx_store.add_transaction_record(tx_record) + all_coins_names.extend([coin.name() for coin in tx_record.additions]) + all_coins_names.extend([coin.name() for coin in tx_record.removals]) await self.add_interested_coin_ids(all_coins_names) - if tx_record.spend_bundle is not None: + + if actual_spend_involved: self.tx_pending_changed() - self.state_changed("pending_transaction", tx_record.wallet_id) + for wallet_id in set(tx.wallet_id for tx in tx_records): + self.state_changed("pending_transaction", wallet_id) + await self.wallet_node.update_ui() async def add_transaction(self, tx_record: TransactionRecord) -> None: """ diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 144b146c3878..130e4af96c37 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -147,7 +147,7 @@ async def test_block_compression( ph, DEFAULT_TX_CONFIG, ) - await wallet.push_transaction(tx=tr) + await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -186,7 +186,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.push_transaction(tx=tr) + await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -229,7 +229,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.push_transaction(tx=tr) + await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -241,7 +241,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.push_transaction(tx=tr) + await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -254,7 +254,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.push_transaction(tx=tr) + await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -267,7 +267,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.push_transaction(tx=tr) + await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -319,7 +319,7 @@ async def check_transaction_confirmed(transaction) -> bool: additions=new_spend_bundle.additions(), removals=new_spend_bundle.removals(), ) - await wallet.push_transaction(tx=new_tr) + await wallet.wallet_state_manager.add_pending_transactions([new_tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -365,7 +365,7 @@ async def check_transaction_confirmed(transaction) -> bool: additions=new_spend_bundle.additions(), removals=new_spend_bundle.removals(), ) - await wallet.push_transaction(tx=new_tr) + await wallet.wallet_state_manager.add_pending_transactions([new_tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index e8b08c9bdcbf..45acbe37d843 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -86,7 +86,7 @@ async def peak_height(fna: FullNodeAPI): [tx] = await wallet_0.wallet_state_manager.main_wallet.generate_signed_transaction( 10, ph1, DEFAULT_TX_CONFIG, 0 ) - await wallet_0.wallet_state_manager.main_wallet.push_transaction(tx) + await wallet_0.wallet_state_manager.add_pending_transactions([tx]) await time_out_assert( 10, @@ -160,7 +160,7 @@ async def test_mempool_tx_sync(self, three_nodes_two_wallets, self_hostname, see [tx] = await wallet_0.wallet_state_manager.main_wallet.generate_signed_transaction( 10, bytes32.random(seeded_random), DEFAULT_TX_CONFIG, 0 ) - await wallet_0.wallet_state_manager.main_wallet.push_transaction(tx) + await wallet_0.wallet_state_manager.add_pending_transactions([tx]) await time_out_assert( 10, diff --git a/tests/simulation/test_simulation.py b/tests/simulation/test_simulation.py index 7e34b824d832..aaf201a5ab2a 100644 --- a/tests/simulation/test_simulation.py +++ b/tests/simulation/test_simulation.py @@ -171,7 +171,7 @@ async def test_simulator_auto_farm_and_get_coins( DEFAULT_TX_CONFIG, uint64(0), ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) # wait till out of mempool await time_out_assert(10, full_node_api.full_node.mempool_manager.get_spendbundle, None, tx.name) # wait until the transaction is confirmed @@ -345,7 +345,7 @@ async def test_wait_transaction_records_entered_mempool( tx_config=DEFAULT_TX_CONFIG, coins={coin}, ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert tx.spend_bundle is not None @@ -397,7 +397,7 @@ async def test_process_transactions( ] for tx in transactions: assert tx.spend_bundle is not None, "the above created transaction is missing the expected spend bundle" - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) if records_or_bundles_or_coins == "records": await full_node_api.process_transaction_records(records=transactions) diff --git a/tests/simulation/test_simulator.py b/tests/simulation/test_simulator.py index c3b90faa8690..be36cef79926 100644 --- a/tests/simulation/test_simulator.py +++ b/tests/simulation/test_simulator.py @@ -165,7 +165,7 @@ async def test_wait_transaction_records_entered_mempool( tx_config=DEFAULT_TX_CONFIG, coins={coin}, ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert tx.spend_bundle is not None @@ -200,7 +200,7 @@ async def test_process_transaction_records( tx_config=DEFAULT_TX_CONFIG, coins={coin}, ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.process_transaction_records(records=[tx]) assert full_node_api.full_node.coin_store.get_coin_record(coin.name()) is not None diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 1a67f26037be..528d8ab15511 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -234,8 +234,8 @@ async def test_cat_spend(self, self_hostname, two_wallet_nodes, trusted): [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG, fee=uint64(1) ) tx_id = None + await wallet.wallet_state_manager.add_pending_transactions(tx_records) for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) if tx_record.wallet_id is cat_wallet.id(): tx_id = tx_record.name.hex() assert tx_record.to_puzzle_hash == cat_2_hash @@ -266,8 +266,7 @@ async def test_cat_spend(self, self_hostname, two_wallet_nodes, trusted): assert list(memos[tx_id].values())[0][0] == cat_2_hash.hex() cat_hash = await cat_wallet.get_new_inner_hash() tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash], DEFAULT_TX_CONFIG) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node_api.txs_in_mempool, True, tx_records) @@ -346,8 +345,8 @@ async def test_cat_reuse_address(self, self_hostname, two_wallet_nodes, trusted) tx_records = await cat_wallet.generate_signed_transaction( [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG.override(reuse_puzhash=True), fee=uint64(1) ) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) if tx_record.wallet_id is cat_wallet.id(): assert tx_record.to_puzzle_hash == cat_2_hash assert len(tx_record.spend_bundle.coin_spends) == 2 @@ -374,8 +373,7 @@ async def test_cat_reuse_address(self, self_hostname, two_wallet_nodes, trusted) cat_hash = await cat_wallet.get_new_inner_hash() tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash], DEFAULT_TX_CONFIG) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node_api.txs_in_mempool, True, tx_records) @@ -510,8 +508,7 @@ async def test_cat_doesnt_see_eve(self, self_hostname, two_wallet_nodes, trusted tx_records = await cat_wallet.generate_signed_transaction( [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG, fee=uint64(1) ) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) await time_out_assert(30, wallet.get_confirmed_balance, funds - 101) @@ -527,7 +524,7 @@ async def test_cat_doesnt_see_eve(self, self_hostname, two_wallet_nodes, trusted [tx_record] = await wallet.wallet_state_manager.main_wallet.generate_signed_transaction( 10, cc2_ph, DEFAULT_TX_CONFIG, 0 ) - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) id = cat_wallet_2.id() @@ -615,8 +612,7 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust tx_records = await cat_wallet_0.generate_signed_transaction( [uint64(60), uint64(20)], [cat_1_hash, cat_2_hash], DEFAULT_TX_CONFIG ) - for tx_record in tx_records: - await wallet_0.wallet_state_manager.add_pending_transaction(tx_record) + await wallet_0.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) await time_out_assert(20, cat_wallet_0.get_confirmed_balance, 20) @@ -631,12 +627,10 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust cat_hash = await cat_wallet_0.get_new_inner_hash() tx_records = await cat_wallet_1.generate_signed_transaction([uint64(15)], [cat_hash], DEFAULT_TX_CONFIG) - for tx_record in tx_records: - await wallet_1.wallet_state_manager.add_pending_transaction(tx_record) + await wallet_1.wallet_state_manager.add_pending_transactions(tx_records) tx_records_2 = await cat_wallet_2.generate_signed_transaction([uint64(20)], [cat_hash], DEFAULT_TX_CONFIG) - for tx_record in tx_records_2: - await wallet_2.wallet_state_manager.add_pending_transaction(tx_record) + await wallet_2.wallet_state_manager.add_pending_transactions(tx_records_2) await full_node_api.process_transaction_records(records=[*tx_records, *tx_records_2]) @@ -660,8 +654,7 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust [uint64(30)], [cat_hash], DEFAULT_TX_CONFIG, memos=[[b"too"], [b"many"], [b"memos"]] ) - for tx_record in tx_records_3: - await wallet_1.wallet_state_manager.add_pending_transaction(tx_record) + await wallet_1.wallet_state_manager.add_pending_transactions(tx_records_3) await time_out_assert(15, full_node_api.txs_in_mempool, True, tx_records_3) txs = await wallet_1.wallet_state_manager.tx_store.get_transactions_between(cat_wallet_1.id(), 0, 100000) for tx in txs: @@ -733,8 +726,7 @@ async def test_cat_max_amount_send(self, self_hostname, two_wallet_nodes, truste tx_records = await cat_wallet.generate_signed_transaction( amounts, puzzle_hashes, DEFAULT_TX_CONFIG, coins={spent_coint} ) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) await asyncio.sleep(2) @@ -842,8 +834,7 @@ async def test_cat_hint(self, self_hostname, two_wallet_nodes, trusted, autodisc [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG, memos=[[cat_2_hash]] ) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) @@ -874,8 +865,7 @@ async def check_wallets(node): [uint64(10)], [cat_2_hash], DEFAULT_TX_CONFIG, memos=[[cat_2_hash]] ) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) @@ -892,8 +882,7 @@ async def check_wallets(node): cat_hash = await cat_wallet.get_new_inner_hash() tx_records = await cat_wallet_2.generate_signed_transaction([uint64(5)], [cat_hash], DEFAULT_TX_CONFIG) - for tx_record in tx_records: - await wallet.wallet_state_manager.add_pending_transaction(tx_record) + await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 78f5057e0e6c..246976728616 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -352,8 +352,7 @@ async def test_cat_trades( tx_config, fee=uint64(1), ) - for tx in tx_records: - await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -448,8 +447,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) - for tx in tx_records: - await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -501,8 +499,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) - for tx in tx_records: - await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -583,8 +580,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) - for tx in tx_records: - await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -646,8 +642,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) - for tx in tx_records: - await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -693,8 +688,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): peer, tx_config, ) - for tx in tx_records: - await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -815,8 +809,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: # trade_take, tx_records = await trade_manager_taker.respond_to_offer( # Offer.from_bytes(trade_make.offer), # ) - # for tx in tx_records: - # await wallet_taker.wallet_state_manager.add_pending_transaction(tx) + # await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) # await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) # assert trade_take is not None # assert tx_records is not None @@ -833,8 +826,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=fee, secure=True ) - for tx in txs: - await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -875,8 +867,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=uint64(0), secure=True ) - for tx in txs: - await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) + await wallet_taker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -930,8 +921,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=uint64(0), secure=True ) - for tx in txs: - await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -990,16 +980,14 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: peer = wallet_node_taker.get_full_node_peer() offer = Offer.from_bytes(trade_make.offer) tr1, txs1 = await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) - for tx in txs1: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1) # we shouldn't be able to respond to a duplicate offer with pytest.raises(ValueError): await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CONFIRM, trade_manager_taker, tr1) # pushing into mempool while already in it should fail tr2, txs2 = await trade_manager_trader.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) - for tx in txs2: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs2) assert await trade_manager_trader.get_coins_of_interest() offer_tx_records: List[TransactionRecord] = await wallet_node_maker.wallet_state_manager.tx_store.get_not_sent() await full_node.process_transaction_records(records=offer_tx_records) @@ -1057,8 +1045,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: bundle = dataclasses.replace(offer._bundle, aggregated_signature=G2Element()) offer = dataclasses.replace(offer, _bundle=bundle) tr1, txs1 = await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) - for tx in txs1: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1) wallet_node_taker.wallet_tx_resend_timeout_secs = 0 # don't wait for resend for _ in range(10): print(await wallet_node_taker._resend_queue()) @@ -1118,7 +1105,6 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: tr1, txs1 = await trade_manager_taker.respond_to_offer( offer, peer, DEFAULT_TX_CONFIG, fee=uint64(1000000000000) ) - for tx in txs1: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1) await full_node.process_transaction_records(records=txs1) await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, tr1) diff --git a/tests/wallet/db_wallet/test_dl_offers.py b/tests/wallet/db_wallet/test_dl_offers.py index 6397ffe5312c..b7b157ca45c5 100644 --- a/tests/wallet/db_wallet/test_dl_offers.py +++ b/tests/wallet/db_wallet/test_dl_offers.py @@ -68,8 +68,7 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: maker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_maker.get_latest_singleton(launcher_id_maker) is not None - await wsm_maker.add_pending_transaction(dl_record) - await wsm_maker.add_pending_transaction(std_record) + await wsm_maker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) maker_funds -= fee maker_funds -= 1 @@ -79,8 +78,7 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: taker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_taker.get_latest_singleton(launcher_id_taker) is not None - await wsm_taker.add_pending_transaction(dl_record) - await wsm_taker.add_pending_transaction(std_record) + await wsm_taker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) taker_funds -= fee taker_funds -= 1 @@ -174,8 +172,7 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ), fee=fee, ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert offer_taker is not None assert tx_records is not None @@ -234,8 +231,7 @@ async def is_singleton_generation(wallet: DataLayerWallet, launcher_id: bytes32, await time_out_assert(15, is_singleton_generation, True, dl_wallet_taker, launcher_id_taker, 2) txs = await dl_wallet_taker.create_update_state_spend(launcher_id_taker, bytes32([2] * 32), DEFAULT_TX_CONFIG) - for tx in txs: - await wallet_node_taker.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_taker.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) @@ -257,13 +253,11 @@ async def test_dl_offer_cancellation(wallets_prefarm: Any, trusted: bool) -> Non dl_record, std_record, launcher_id = await dl_wallet.generate_new_reporter(root, DEFAULT_TX_CONFIG) assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wsm.add_pending_transaction(dl_record) - await wsm.add_pending_transaction(std_record) + await wsm.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed_and_root, True, dl_wallet, launcher_id, root) dl_record_2, std_record_2, launcher_id_2 = await dl_wallet.generate_new_reporter(root, DEFAULT_TX_CONFIG) - await wsm.add_pending_transaction(dl_record_2) - await wsm.add_pending_transaction(std_record_2) + await wsm.add_pending_transactions([dl_record_2, std_record_2]) await full_node_api.process_transaction_records(records=[dl_record_2, std_record_2]) trade_manager = wsm.trade_manager @@ -297,8 +291,7 @@ async def test_dl_offer_cancellation(wallets_prefarm: Any, trusted: bool) -> Non cancellation_txs = await trade_manager.cancel_pending_offers( [offer.trade_id], DEFAULT_TX_CONFIG, fee=uint64(2_000_000_000_000), secure=True ) - for tx in cancellation_txs: - await trade_manager.wallet_state_manager.add_pending_transaction(tx) + await trade_manager.wallet_state_manager.add_pending_transactions(cancellation_txs) assert len(cancellation_txs) == 2 await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager, offer) await full_node_api.process_transaction_records(records=cancellation_txs) @@ -335,8 +328,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: maker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_maker.get_latest_singleton(launcher_id_maker_1) is not None - await wsm_maker.add_pending_transaction(dl_record) - await wsm_maker.add_pending_transaction(std_record) + await wsm_maker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) maker_funds -= fee maker_funds -= 1 @@ -345,8 +337,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: maker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_maker.get_latest_singleton(launcher_id_maker_2) is not None - await wsm_maker.add_pending_transaction(dl_record) - await wsm_maker.add_pending_transaction(std_record) + await wsm_maker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) maker_funds -= fee maker_funds -= 1 @@ -356,8 +347,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: taker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_taker.get_latest_singleton(launcher_id_taker_1) is not None - await wsm_taker.add_pending_transaction(dl_record) - await wsm_taker.add_pending_transaction(std_record) + await wsm_taker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) taker_funds -= fee taker_funds -= 1 @@ -366,8 +356,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: taker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_taker.get_latest_singleton(launcher_id_taker_2) is not None - await wsm_taker.add_pending_transaction(dl_record) - await wsm_taker.add_pending_transaction(std_record) + await wsm_taker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) taker_funds -= fee taker_funds -= 1 @@ -476,8 +465,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ), fee=fee, ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert offer_taker is not None assert tx_records is not None diff --git a/tests/wallet/db_wallet/test_dl_wallet.py b/tests/wallet/db_wallet/test_dl_wallet.py index cea97783043b..1b26b53938c0 100644 --- a/tests/wallet/db_wallet/test_dl_wallet.py +++ b/tests/wallet/db_wallet/test_dl_wallet.py @@ -79,8 +79,7 @@ async def test_initial_creation( assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(dl_record) - await wallet_node_0.wallet_state_manager.add_pending_transaction(std_record) + await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -132,8 +131,7 @@ async def test_get_owned_singletons( assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(dl_record) - await wallet_node_0.wallet_state_manager.add_pending_transaction(std_record) + await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -191,8 +189,7 @@ async def test_tracking_non_owned( assert await dl_wallet_0.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(dl_record) - await wallet_node_0.wallet_state_manager.add_pending_transaction(std_record) + await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet_0, launcher_id) @@ -206,8 +203,7 @@ async def test_tracking_non_owned( new_root = MerkleTree([Program.to("root").get_tree_hash()]).calculate_root() txs = await dl_wallet_0.create_update_state_spend(launcher_id, new_root, DEFAULT_TX_CONFIG) - for tx in txs: - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet_0, launcher_id) @@ -263,8 +259,7 @@ async def test_lifecycle( assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(dl_record) - await wallet_node_0.wallet_state_manager.add_pending_transaction(std_record) + await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -299,8 +294,7 @@ async def test_lifecycle( assert new_record != previous_record assert not new_record.confirmed - for tx in txs: - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -321,8 +315,7 @@ async def test_lifecycle( assert new_record != previous_record assert not new_record.confirmed - for tx in txs: - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -387,8 +380,7 @@ async def is_singleton_confirmed(wallet: DataLayerWallet, lid: bytes32) -> bool: initial_record = await dl_wallet_0.get_latest_singleton(launcher_id) assert initial_record is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(dl_record) - await wallet_node_0.wallet_state_manager.add_pending_transaction(std_record) + await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) await asyncio.wait_for( full_node_api.process_transaction_records(records=[dl_record, std_record]), timeout=adjusted_timeout(timeout=15), @@ -419,16 +411,14 @@ async def is_singleton_confirmed(wallet: DataLayerWallet, lid: bytes32) -> bool: assert initial_record != record_0 assert record_0 != record_1 - for tx in report_txs: - await wallet_node_1.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_1.wallet_state_manager.add_pending_transactions(report_txs) await asyncio.wait_for( full_node_api.wait_transaction_records_entered_mempool(records=report_txs), timeout=adjusted_timeout(timeout=15), ) - for tx in update_txs: - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs) await asyncio.wait_for( full_node_api.process_transaction_records(records=report_txs), timeout=adjusted_timeout(timeout=15) @@ -473,8 +463,7 @@ async def is_singleton_generation(wallet: DataLayerWallet, launcher_id: bytes32, ) record_1 = await dl_wallet_0.get_latest_singleton(launcher_id) assert record_1 is not None - for tx in update_txs_1: - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs_1) await full_node_api.wait_transaction_records_entered_mempool(update_txs_1) # Delete any trace of that update @@ -487,8 +476,7 @@ async def is_singleton_generation(wallet: DataLayerWallet, launcher_id: bytes32, assert record_0 is not None assert record_0 != record_1 - for tx in update_txs_0: - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs_0) await asyncio.wait_for( full_node_api.process_transaction_records(records=update_txs_1), timeout=adjusted_timeout(timeout=15) @@ -553,15 +541,13 @@ async def test_mirrors(wallets_prefarm: Any, trusted: bool) -> None: dl_record, std_record, launcher_id_1 = await dl_wallet_1.generate_new_reporter(bytes32([0] * 32), DEFAULT_TX_CONFIG) assert await dl_wallet_1.get_latest_singleton(launcher_id_1) is not None - await wsm_1.add_pending_transaction(dl_record) - await wsm_1.add_pending_transaction(std_record) + await wsm_1.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed_and_root, True, dl_wallet_1, launcher_id_1, bytes32([0] * 32)) dl_record, std_record, launcher_id_2 = await dl_wallet_2.generate_new_reporter(bytes32([0] * 32), DEFAULT_TX_CONFIG) assert await dl_wallet_2.get_latest_singleton(launcher_id_2) is not None - await wsm_2.add_pending_transaction(dl_record) - await wsm_2.add_pending_transaction(std_record) + await wsm_2.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed_and_root, True, dl_wallet_2, launcher_id_2, bytes32([0] * 32)) @@ -576,10 +562,10 @@ async def test_mirrors(wallets_prefarm: Any, trusted: bool) -> None: launcher_id_2, uint64(3), [b"foo", b"bar"], DEFAULT_TX_CONFIG, fee=uint64(1_999_999_999_999) ) additions: List[Coin] = [] + await wsm_1.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: additions.extend(tx.spend_bundle.additions()) - await wsm_1.add_pending_transaction(tx) await full_node_api.process_transaction_records(records=txs) mirror_coin: Coin = [c for c in additions if c.puzzle_hash == create_mirror_puzzle().get_tree_hash()][0] @@ -592,8 +578,7 @@ async def test_mirrors(wallets_prefarm: Any, trusted: bool) -> None: ) txs = await dl_wallet_1.delete_mirror(mirror.coin_id, peer_1, DEFAULT_TX_CONFIG, fee=uint64(2_000_000_000_000)) - for tx in txs: - await wsm_1.add_pending_transaction(tx) + await wsm_1.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, dl_wallet_1.get_mirrors_for_launcher, [], launcher_id_2) diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index 6c2b53eb94e1..b3b9b00cd99c 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -209,7 +209,7 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes message_tx, message_spend_bundle, attest_data = await did_wallet_0.create_attestment( did_wallet_2.did_info.temp_coin.name(), newpuzhash, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet_0.wallet_state_manager.add_pending_transaction(message_tx) + await did_wallet_0.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_0.id() @@ -234,7 +234,7 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes test_message_spend_bundle, ) assert txs[0].spend_bundle is not None - await did_wallet_2.wallet_state_manager.add_pending_transaction(txs[0]) + await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 5, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() @@ -250,8 +250,7 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes some_ph = 32 * b"\2" txs = await did_wallet_2.create_exit_spend(some_ph, DEFAULT_TX_CONFIG) - for tx in txs: - await did_wallet_2.wallet_state_manager.add_pending_transaction(tx) + await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_2.id() @@ -378,7 +377,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w message_tx, message_spend_bundle, attest1 = await did_wallet.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet.wallet_state_manager.add_pending_transaction(message_tx) + await did_wallet.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -387,7 +386,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w message_tx2, message_spend_bundle2, attest2 = await did_wallet_2.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet_2.wallet_state_manager.add_pending_transaction(message_tx2) + await did_wallet_2.wallet_state_manager.add_pending_transactions([message_tx2]) assert message_spend_bundle2 is not None spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_2.id() @@ -407,7 +406,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w await time_out_assert(15, did_wallet_4.get_confirmed_balance, 0) await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 0) txs = await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, message_spend_bundle) - await did_wallet_4.wallet_state_manager.add_pending_transaction(txs[0]) + await did_wallet_4.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_4.id() ) @@ -543,8 +542,7 @@ async def test_did_find_lost_did(self, self_hostname, two_wallet_nodes, trusted) await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list txs = await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) - for tx in txs: - await did_wallet.wallet_state_manager.add_pending_transaction(tx) + await did_wallet.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) @@ -625,8 +623,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list txs = await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) - for tx in txs: - await did_wallet.wallet_state_manager.add_pending_transaction(tx) + await did_wallet.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -656,7 +653,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, message_tx, message_spend_bundle, attest_data = await did_wallet.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet.wallet_state_manager.add_pending_transaction(message_tx) + await did_wallet.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -669,7 +666,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, message_spend_bundle, ) = await did_wallet_3.load_attest_files_for_recovery_spend([attest_data]) txs = await did_wallet_3.recovery_spend(coin, new_ph, info, pubkey, message_spend_bundle) - await did_wallet_3.wallet_state_manager.add_pending_transaction(txs[0]) + await did_wallet_3.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_3.id() ) @@ -699,7 +696,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, message_tx, message_spend_bundle, attest1 = await did_wallet_3.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet_3.wallet_state_manager.add_pending_transaction(message_tx) + await did_wallet_3.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_3.id() @@ -714,7 +711,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, test_message_spend_bundle, ) = await did_wallet_4.load_attest_files_for_recovery_spend([attest1]) txs = await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, test_message_spend_bundle) - await did_wallet_2.wallet_state_manager.add_pending_transaction(txs[0]) + await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_4.id() @@ -791,7 +788,7 @@ async def test_did_transfer(self, self_hostname, two_wallet_nodes, with_recovery new_puzhash = await wallet2.get_new_puzzlehash() txs = await did_wallet_1.transfer_did(new_puzhash, fee, with_recovery, DEFAULT_TX_CONFIG) for tx in txs: - await did_wallet_1.wallet_state_manager.add_pending_transaction(tx) + await did_wallet_1.wallet_state_manager.add_pending_transactions([tx]) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) @@ -868,8 +865,7 @@ async def test_update_recovery_list(self, self_hostname, two_wallet_nodes, trust await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) await did_wallet_1.update_recovery_list([bytes(ph)], 1) txs = await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG) - for tx in txs: - await did_wallet_1.wallet_state_manager.add_pending_transaction(tx) + await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) await full_node_api.farm_blocks_to_wallet(1, wallet) await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) @@ -954,7 +950,7 @@ async def test_get_info(self, self_hostname, two_wallet_nodes, trusted): ), fee, ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.process_transaction_records(records=[tx]) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=15) @@ -1076,8 +1072,7 @@ async def test_update_metadata(self, self_hostname, two_wallet_nodes, trusted): metadata["Twitter"] = "http://www.twitter.com" await did_wallet_1.update_metadata(metadata) txs = await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG, fee) - for tx in txs: - await did_wallet_1.wallet_state_manager.add_pending_transaction(tx) + await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) transaction_records = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) diff --git a/tests/wallet/nft_wallet/test_nft_1_offers.py b/tests/wallet/nft_wallet/test_nft_1_offers.py index 401bdce138d6..2467fa555112 100644 --- a/tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/tests/wallet/nft_wallet/test_nft_1_offers.py @@ -137,8 +137,8 @@ async def test_nft_offer_sell_nft( royalty_basis_pts, did_id, ) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -185,8 +185,7 @@ async def test_nft_offer_sell_nft( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) @@ -296,8 +295,8 @@ async def test_nft_offer_request_nft( royalty_basis_pts, did_id, ) + await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -346,8 +345,7 @@ async def test_nft_offer_request_nft( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None @@ -454,8 +452,8 @@ async def test_nft_offer_sell_did_to_did( royalty_basis_pts, did_id, ) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -519,8 +517,7 @@ async def test_nft_offer_sell_did_to_did( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -633,8 +630,8 @@ async def test_nft_offer_sell_nft_for_cat( royalty_basis_pts, did_id, ) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -693,8 +690,7 @@ async def test_nft_offer_sell_nft_for_cat( DEFAULT_TX_CONFIG, memos=[[ph_taker_cat_1], [ph_taker_cat_2]], ) - for tx_record in cat_tx_records: - await wallet_maker.wallet_state_manager.add_pending_transaction(tx_record) + await wallet_maker.wallet_state_manager.add_pending_transactions(cat_tx_records) await full_node_api.process_transaction_records(records=cat_tx_records) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -724,8 +720,7 @@ async def test_nft_offer_sell_nft_for_cat( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -835,8 +830,8 @@ async def test_nft_offer_request_nft_for_cat( royalty_basis_pts, did_id, ) + await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -899,8 +894,7 @@ async def test_nft_offer_request_nft_for_cat( amounts.append(uint64(extra_change)) puzzle_hashes.append(ph_taker_cat_1) cat_tx_records = await cat_wallet_maker.generate_signed_transaction(amounts, puzzle_hashes, DEFAULT_TX_CONFIG) - for tx_record in cat_tx_records: - await wallet_maker.wallet_state_manager.add_pending_transaction(tx_record) + await wallet_maker.wallet_state_manager.add_pending_transactions(cat_tx_records) await full_node_api.process_transaction_records(records=cat_tx_records) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -935,8 +929,7 @@ async def test_nft_offer_request_nft_for_cat( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -1038,8 +1031,8 @@ async def test_nft_offer_sell_cancel( royalty_basis_pts, did_id, ) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -1072,8 +1065,7 @@ async def test_nft_offer_sell_cancel( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=FEE, secure=True ) - for tx in txs: - await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) @@ -1165,8 +1157,8 @@ async def test_nft_offer_sell_cancel_in_batch( royalty_basis_pts, did_id, ) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -1200,8 +1192,7 @@ async def test_nft_offer_sell_cancel_in_batch( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=FEE, secure=True ) - for tx in txs: - await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) @@ -1389,8 +1380,8 @@ async def test_complex_nft_offer( uint16(royalty_basis_pts_maker), did_id_maker, ) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -1404,8 +1395,8 @@ async def test_complex_nft_offer( royalty_basis_pts_taker_1, did_id_taker, ) + await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -1431,8 +1422,8 @@ async def test_complex_nft_offer( royalty_basis_pts_taker_2, did_id_taker, ) + await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_taker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -1505,8 +1496,7 @@ async def test_complex_nft_offer( DEFAULT_TX_CONFIG, fee=FEE, ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None await full_node_api.process_transaction_records(records=tx_records) @@ -1608,8 +1598,7 @@ async def get_cat_wallet_and_check_balance(asset_id: str, wsm: Any) -> uint128: DEFAULT_TX_CONFIG, fee=uint64(0), ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None await time_out_assert(20, mempool_not_empty, True, full_node_api) diff --git a/tests/wallet/nft_wallet/test_nft_offers.py b/tests/wallet/nft_wallet/test_nft_offers.py index 98bfd00f891f..18c595f4349c 100644 --- a/tests/wallet/nft_wallet/test_nft_offers.py +++ b/tests/wallet/nft_wallet/test_nft_offers.py @@ -103,8 +103,8 @@ async def test_nft_offer_with_fee( ) txs = await nft_wallet_maker.generate_new_nft(metadata, tx_config) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -151,8 +151,7 @@ async def test_nft_offer_with_fee( tx_config, fee=taker_fee, ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -220,8 +219,7 @@ async def test_nft_offer_with_fee( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, tx_config, fee=taker_fee ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -302,8 +300,8 @@ async def test_nft_offer_cancellations( ) txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -342,8 +340,7 @@ async def test_nft_offer_cancellations( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=cancel_fee, secure=True ) - for tx in txs: - await trade_manager_maker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(20, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node_api.process_transaction_records(records=txs) @@ -425,8 +422,8 @@ async def test_nft_offer_with_metadata_update( ) txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -445,8 +442,8 @@ async def test_nft_offer_with_metadata_update( fee_for_update = uint64(10) txs = await nft_wallet_maker.update_metadata(nft_to_update, key, url_to_add, DEFAULT_TX_CONFIG, fee=fee_for_update) mempool_mgr = full_node_api.full_node.mempool_manager + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none(20, mempool_mgr.get_spendbundle, tx.spend_bundle.name()) @@ -485,8 +482,7 @@ async def test_nft_offer_with_metadata_update( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=taker_fee ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -573,8 +569,8 @@ async def test_nft_offer_nft_for_cat( ) txs = await nft_wallet_maker.generate_new_nft(metadata, tx_config) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -664,8 +660,7 @@ async def test_nft_offer_nft_for_cat( tx_config, fee=taker_fee, ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -745,8 +740,7 @@ async def test_nft_offer_nft_for_cat( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, tx_config, fee=taker_fee ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -835,8 +829,8 @@ async def test_nft_offer_nft_for_nft( ) txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -850,8 +844,8 @@ async def test_nft_offer_nft_for_nft( ) txs = await nft_wallet_taker.generate_new_nft(metadata_2, DEFAULT_TX_CONFIG) + await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_maker.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -897,8 +891,7 @@ async def test_nft_offer_nft_for_nft( trade_take, tx_records = await trade_manager_taker.respond_to_offer( Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=taker_fee ) - for tx in tx_records: - await trade_manager_taker.wallet_state_manager.add_pending_transaction(tx) + await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py index c3773aa47368..43cb41b8049f 100644 --- a/tests/wallet/nft_wallet/test_nft_wallet.py +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -141,8 +141,8 @@ async def test_nft_wallet_creation_automatically(self_hostname: str, two_wallet_ ) txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_0.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: await time_out_assert_not_none( 20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -160,7 +160,7 @@ async def test_nft_wallet_creation_automatically(self_hostname: str, two_wallet_ ) assert len(txs) == 1 assert txs[0].spend_bundle is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(txs[0]) + await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() ) @@ -240,8 +240,8 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n await time_out_assert(30, wallet_0.get_unconfirmed_balance, 2000000000000) await time_out_assert(30, wallet_0.get_confirmed_balance, 2000000000000) txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_0.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -277,8 +277,8 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n await time_out_assert(10, wallet_0.get_confirmed_balance, 4000000000000) txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await nft_wallet_0.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -301,7 +301,7 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n ) assert len(txs) == 1 assert txs[0].spend_bundle is not None - await wallet_node_0.wallet_state_manager.add_pending_transaction(txs[0]) + await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() ) @@ -323,7 +323,7 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n ) assert len(txs) == 1 assert txs[0].spend_bundle is not None - await wallet_node_1.wallet_state_manager.add_pending_transaction(txs[0]) + await wallet_node_1.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() ) @@ -1014,7 +1014,7 @@ async def test_nft_transfer_nft_with_did(self_hostname: str, two_wallet_nodes: A # transfer DID to the other wallet txs = await did_wallet.transfer_did(ph1, uint64(0), True, DEFAULT_TX_CONFIG) for tx in txs: - await did_wallet.wallet_state_manager.add_pending_transaction(tx) + await did_wallet.wallet_state_manager.add_pending_transactions(txs) if tx.spend_bundle is not None: await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -1065,8 +1065,7 @@ async def test_nft_transfer_nft_with_did(self_hostname: str, two_wallet_nodes: A dict(wallet_id=nft_wallet_id_1, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex(), fee=fee) ) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - for tx in txs: - await did_wallet.wallet_state_manager.add_pending_transaction(tx) + await did_wallet.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( @@ -1637,8 +1636,7 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A dict(wallet_id=nft_wallet_0_id, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex()) ) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - for tx in txs: - await did_wallet1.wallet_state_manager.add_pending_transaction(tx) + await did_wallet1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( 30, api_0.nft_get_by_did, [dict(did_id=hmr_did_id)], lambda x: x.get("wallet_id", 0) > 0 @@ -1670,8 +1668,7 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A dict(wallet_id=nft_wallet_1_id, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex()) ) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - for tx in txs: - await did_wallet1.wallet_state_manager.add_pending_transaction(tx) + await did_wallet1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( 30, api_0.nft_get_by_did, [dict(did_id=hmr_did_id)], lambda x: x.get("wallet_id") is not None @@ -1696,8 +1693,7 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A # Test set DID2 -> None resp = await api_0.nft_set_nft_did(dict(wallet_id=nft_wallet_2_id, nft_coin_id=nft_coin_id.hex())) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - for tx in txs: - await did_wallet1.wallet_state_manager.add_pending_transaction(tx) + await did_wallet1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) # Check NFT DID diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index dcdea34320c6..158ba95723a8 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -428,7 +428,7 @@ async def test_get_farmed_amount_with_fee(wallet_rpc_environment: WalletRpcTestE tx_config=DEFAULT_TX_CONFIG, fee=uint64(fee_amount), ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) our_ph = await wallet.get_new_puzzlehash() await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index 5b160d4a1f6c..9ccd89a5706a 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -191,7 +191,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): spent_coin = tx_record.spend_bundle.removals()[0] assert spent_coin.puzzle_hash == puzzle_hash - await wallet.push_transaction(tx_record) + await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -203,7 +203,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): [tx_record] = await wallet.generate_signed_transaction( uint64(10), SINGLETON_LAUNCHER_HASH, DEFAULT_TX_CONFIG, uint64(0) ) - await wallet.push_transaction(tx_record) + await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -211,7 +211,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): # Send a transaction to make sure the wallet is still running [tx_record] = await wallet.generate_signed_transaction(uint64(10), junk_ph, DEFAULT_TX_CONFIG, uint64(0)) - await wallet.push_transaction(tx_record) + await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -273,7 +273,7 @@ async def test_subscribe_for_coin_id(self, simulator_and_wallet, self_hostname): [tx_record] = await standard_wallet.generate_signed_transaction( uint64(10), puzzle_hash, DEFAULT_TX_CONFIG, uint64(0), coins=coins ) - await standard_wallet.push_transaction(tx_record) + await standard_wallet.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -312,7 +312,7 @@ async def test_subscribe_for_coin_id(self, simulator_and_wallet, self_hostname): data_response: RespondToCoinUpdates = RespondToCoinUpdates.from_bytes(msg_response.data) assert len(data_response.coin_states) == 0 - await standard_wallet.push_transaction(tx_record) + await standard_wallet.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index fc343a2707e4..f1989971beaa 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -1126,8 +1126,8 @@ async def test_dusted_wallet( ] ) txs = await farm_nft_wallet.generate_new_nft(metadata, DEFAULT_TX_CONFIG) + await farm_nft_wallet.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await farm_nft_wallet.wallet_state_manager.add_pending_transaction(tx) if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) await time_out_assert_not_none( @@ -1161,7 +1161,7 @@ async def test_dusted_wallet( ) assert len(txs) == 1 assert txs[0].spend_bundle is not None - await farm_wallet_node.wallet_state_manager.add_pending_transaction(txs[0]) + await farm_wallet_node.wallet_state_manager.add_pending_transactions(txs) assert compute_memos(txs[0].spend_bundle) # Farm a new block. @@ -1296,7 +1296,7 @@ async def assert_coin_state_retry() -> None: [tx] = await wallet.generate_signed_transaction( 1_000_000_000_000, bytes32([0] * 32), DEFAULT_TX_CONFIG, memos=[ph] ) - await wallet_node.wallet_state_manager.add_pending_transaction(tx) + await wallet_node.wallet_state_manager.add_pending_transactions([tx]) async def tx_in_mempool(): return full_node_api.full_node.mempool_manager.get_spendbundle(tx.name) is not None diff --git a/tests/wallet/test_notifications.py b/tests/wallet/test_notifications.py index 88718983e17e..dce43613ed40 100644 --- a/tests/wallet/test_notifications.py +++ b/tests/wallet/test_notifications.py @@ -138,7 +138,7 @@ async def track_coin_state(*args: Any) -> bool: if case == "allow_larger": allow_larger_height = peak.height + 1 tx = await notification_manager_1.send_new_notification(ph_2, msg, AMOUNT, DEFAULT_TX_CONFIG, fee=FEE) - await wsm_1.add_pending_transaction(tx) + await wsm_1.add_pending_transactions([tx]) await time_out_assert_not_none( 5, full_node_api.full_node.mempool_manager.get_spendbundle, diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index c421035bc0e1..ee2bbf528df3 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -120,7 +120,7 @@ async def test_wallet_make_transaction( DEFAULT_TX_CONFIG, uint64(0), ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet.get_confirmed_balance() == expected_confirmed_balance @@ -176,7 +176,7 @@ async def test_wallet_reuse_address( assert len(tx.spend_bundle.coin_spends) == 1 new_puzhash = [c.puzzle_hash.hex() for c in tx.additions] assert tx.spend_bundle.coin_spends[0].coin.puzzle_hash.hex() in new_puzhash - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet.get_confirmed_balance() == expected_confirmed_balance @@ -231,7 +231,7 @@ async def test_wallet_clawback_claim_auto( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 10}], ) - await wallet.push_transaction(tx1) + await wallet.wallet_state_manager.add_pending_transactions([tx1]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx1]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) await time_out_assert( @@ -247,7 +247,7 @@ async def test_wallet_clawback_claim_auto( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 10}], ) - await wallet.push_transaction(tx2) + await wallet.wallet_state_manager.add_pending_transactions([tx2]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx2]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) await time_out_assert( @@ -263,7 +263,7 @@ async def test_wallet_clawback_claim_auto( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 10}], ) - await wallet.push_transaction(tx3) + await wallet.wallet_state_manager.add_pending_transactions([tx3]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx3]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) await time_out_assert( @@ -343,7 +343,7 @@ async def test_wallet_clawback_clawback( memos=[b"Test"], ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -466,7 +466,7 @@ async def test_wallet_clawback_sent_self( memos=[b"Test"], ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -552,7 +552,7 @@ async def test_wallet_clawback_claim_manual( puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -642,7 +642,7 @@ async def test_wallet_clawback_reorg( puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -740,7 +740,7 @@ async def test_get_clawback_coins( puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 500}], ) - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -800,7 +800,7 @@ async def test_clawback_resync( clawback_coin_id_1 = tx1.additions[0].name() assert tx1.spend_bundle is not None - await wallet_1.push_transaction(tx1) + await wallet_1.wallet_state_manager.add_pending_transactions([tx1]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx1]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) # Check merkle coins @@ -819,7 +819,7 @@ async def test_clawback_resync( ) clawback_coin_id_2 = tx2.additions[0].name() assert tx2.spend_bundle is not None - await wallet_1.push_transaction(tx2) + await wallet_1.wallet_state_manager.add_pending_transactions([tx2]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx2]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) # Check merkle coins @@ -1047,7 +1047,7 @@ async def test_wallet_send_to_three_peers( uint64(0), ) assert tx.spend_bundle is not None - await wallet_0.wallet_state_manager.main_wallet.push_transaction(tx) + await wallet_0.wallet_state_manager.main_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api_0.wait_transaction_records_entered_mempool(records=[tx]) # wallet0 <-> sever1 @@ -1104,7 +1104,7 @@ async def test_wallet_make_transaction_hop( uint64(0), ) - await wallet_0.push_transaction(tx) + await wallet_0.wallet_state_manager.add_pending_transactions([tx]) await full_node_api_0.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet_0.get_confirmed_balance() == expected_confirmed_balance @@ -1122,7 +1122,7 @@ async def test_wallet_make_transaction_hop( [tx] = await wallet_1.generate_signed_transaction( uint64(tx_amount), await wallet_0.get_new_puzzlehash(), DEFAULT_TX_CONFIG, uint64(0) ) - await wallet_1.push_transaction(tx) + await wallet_1.wallet_state_manager.add_pending_transactions([tx]) await full_node_api_0.wait_transaction_records_entered_mempool(records=[tx]) await full_node_api_0.farm_blocks_to_puzzlehash(count=4, guarantee_transaction_blocks=True) @@ -1185,7 +1185,7 @@ async def test_wallet_make_transaction_with_fee( fees = tx.spend_bundle.fees() assert fees == tx_fee - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_1.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet.get_confirmed_balance() == expected_confirmed_balance @@ -1252,7 +1252,7 @@ async def test_wallet_make_transaction_with_memo( fees = tx.spend_bundle.fees() assert fees == tx_fee - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_1.wait_transaction_records_entered_mempool(records=[tx]) memos = await api_0.get_transaction_memo(dict(transaction_id=tx_id)) # test json serialization @@ -1314,7 +1314,7 @@ async def test_wallet_create_hit_max_send_amount( ) assert tx_split_coins.spend_bundle is not None - await wallet.push_transaction(tx_split_coins) + await wallet.wallet_state_manager.add_pending_transactions([tx_split_coins]) await full_node_1.process_transaction_records(records=[tx_split_coins]) await wait_for_coins_in_wallet(coins=set(tx_split_coins.additions), wallet=wallet) @@ -1426,7 +1426,7 @@ async def test_wallet_prevent_fee_theft( memos=list(compute_memos(stolen_sb).items()), valid_times=ConditionValidTimes(), ) - await wallet.push_transaction(stolen_tx) + await wallet.wallet_state_manager.add_pending_transactions([stolen_tx]) await time_out_assert(20, wallet.get_confirmed_balance, expected_confirmed_balance) await time_out_assert(20, wallet.get_unconfirmed_balance, expected_confirmed_balance - stolen_cs.coin.amount) @@ -1486,7 +1486,7 @@ async def test_wallet_tx_reorg( [tx] = await wallet.generate_signed_transaction(uint64(tx_amount), ph2, DEFAULT_TX_CONFIG, coins={coin}) assert tx.spend_bundle is not None - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.process_transaction_records(records=[tx]) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node, wallet_node_2], timeout=20) @@ -1739,7 +1739,7 @@ async def test_wallet_transaction_options( assert tx.spend_bundle is not None paid_coin = [coin for coin in tx.spend_bundle.additions() if coin.amount == AMOUNT_TO_SEND][0] assert paid_coin.parent_coin_info == coin_list[2].name() - await wallet.push_transaction(tx) + await wallet.wallet_state_manager.add_pending_transactions([tx]) await time_out_assert(20, wallet.get_confirmed_balance, expected_confirmed_balance) await time_out_assert(20, wallet.get_unconfirmed_balance, expected_confirmed_balance - AMOUNT_TO_SEND) diff --git a/tests/wallet/test_wallet_retry.py b/tests/wallet/test_wallet_retry.py index 18cc3cce8f43..5b0aca0f03e3 100644 --- a/tests/wallet/test_wallet_retry.py +++ b/tests/wallet/test_wallet_retry.py @@ -66,7 +66,7 @@ async def test_wallet_tx_retry( [transaction] = await wallet_1.generate_signed_transaction(uint64(100), reward_ph, DEFAULT_TX_CONFIG) sb1: Optional[SpendBundle] = transaction.spend_bundle assert sb1 is not None - await wallet_1.push_transaction(transaction) + await wallet_1.wallet_state_manager.add_pending_transactions([transaction]) async def sb_in_mempool() -> bool: return full_node_1.full_node.mempool_manager.get_spendbundle(transaction.name) == transaction.spend_bundle diff --git a/tests/wallet/vc_wallet/test_vc_wallet.py b/tests/wallet/vc_wallet/test_vc_wallet.py index 8a79aff98092..d97938fcf133 100644 --- a/tests/wallet/vc_wallet/test_vc_wallet.py +++ b/tests/wallet/vc_wallet/test_vc_wallet.py @@ -253,7 +253,7 @@ async def check_length(length: int, func: Callable[..., Awaitable[Any]], *args: memos=["hey"], ) confirmed_balance -= 2000000000 - await wallet_node_0.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_0.wallet_state_manager.add_pending_transactions([tx]) assert tx.spend_bundle is not None spend_bundle = tx.spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) @@ -359,7 +359,7 @@ async def check_length(length: int, func: Callable[..., Awaitable[Any]], *args: uint64(0), cat_discrepancy=(-50, Program.to(None), Program.to(None)), ) - await wallet_node_1.wallet_state_manager.add_pending_transaction(tx) + await wallet_node_1.wallet_state_manager.add_pending_transactions([tx]) assert tx.spend_bundle is not None spend_bundle = tx.spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) @@ -452,8 +452,7 @@ async def test_self_revoke( ) txs = await did_wallet.transfer_did(bytes32([0] * 32), uint64(0), False, DEFAULT_TX_CONFIG) - for tx in txs: - await did_wallet.wallet_state_manager.add_pending_transaction(tx) + await did_wallet.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) From cd64522ebb13088ac851ab199b1b11539ac11e52 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 31 Oct 2023 13:46:34 -0700 Subject: [PATCH 005/274] Address offline comments by @emlowe --- chia/wallet/cat_wallet/cat_wallet.py | 3 +++ chia/wallet/wallet.py | 4 ++++ tests/wallet/test_wallet.py | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 6e622ac02e65..072f678dd8d1 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -793,6 +793,9 @@ async def generate_signed_transaction( payments.append(Payment(puzhash, amount, memos_with_hint)) payment_sum = sum([p.amount for p in payments]) + max_send = await self.get_max_send_amount() + if payment_sum > max_send: + raise ValueError(f" Insufficient funds. Your max amount is {max_send} mojos in a single transaction.") unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( payments, tx_config, diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 6c994158ca1a..13c123f532ce 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -267,6 +267,10 @@ async def _generate_unsigned_transaction( total_amount = amount + sum(primary.amount for primary in primaries) + fee total_balance = await self.get_spendable_balance() + 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( diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 78df9d474dfe..d1ff6522627c 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -1308,7 +1308,12 @@ async def test_wallet_create_hit_max_send_amount( await time_out_assert(20, wallet.get_confirmed_balance, expected_confirmed_balance) - primaries = [Payment(ph, uint64(1000000000 + i)) for i in range(60)] + primaries = [ + Payment(ph, uint64(1000000000 + i)) + for i in range( + int(wallet.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 5 / wallet.cost_of_single_tx) + 1 + ) + ] [tx_split_coins] = await wallet.generate_signed_transaction( uint64(1), ph, DEFAULT_TX_CONFIG, uint64(0), primaries=primaries ) @@ -1316,9 +1321,10 @@ async def test_wallet_create_hit_max_send_amount( await wallet.push_transaction(tx_split_coins) await full_node_1.process_transaction_records(records=[tx_split_coins]) - await wait_for_coins_in_wallet(coins=set(tx_split_coins.additions), wallet=wallet) + await wait_for_coins_in_wallet(coins=set(tx_split_coins.additions), wallet=wallet, timeout=20) max_sent_amount = await wallet.get_max_send_amount() + assert max_sent_amount < (await wallet.get_spendable_balance()) # 1) Generate transaction that is under the limit [transaction_record] = await wallet.generate_signed_transaction( @@ -1341,7 +1347,10 @@ async def test_wallet_create_hit_max_send_amount( assert transaction_record.amount == uint64(max_sent_amount) # 3) Generate transaction that is greater than limit - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match=f"Can't send more than {max_sent_amount} mojos in a single transaction, got {max_sent_amount + 1}", + ): await wallet.generate_signed_transaction( uint64(max_sent_amount + 1), ph, From 46e816ab7b35319892de59d7aad0f6a7c8345b3e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 1 Nov 2023 07:52:50 -0700 Subject: [PATCH 006/274] Rework max send amount to be based on coin quantity rather than amount --- chia/wallet/cat_wallet/cat_wallet.py | 34 +++++++++++----------------- chia/wallet/coin_selection.py | 2 +- chia/wallet/wallet.py | 32 ++++++++------------------ tests/wallet/test_wallet.py | 10 +++----- 4 files changed, 27 insertions(+), 51 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 072f678dd8d1..5fabffd27c33 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -316,25 +316,20 @@ async def get_unconfirmed_balance(self, unspent_records: Optional[Set[WalletCoin def cost_of_single_tx(self) -> int: return 30000000 # Estimate - async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128: - spendable: List[WalletCoinRecord] = list(await self.get_cat_spendable_coins()) - 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 / 2 # avoid full block TXs - current_cost = 0 - total_amount = 0 - total_coin_count = 0 + @property + def max_send_quantity(self) -> int: + # avoid full block TXs + return int(self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 2 / self.cost_of_single_tx) - 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 + async def get_max_spendable_coins(self, records: Optional[Set[WalletCoinRecord]] = None) -> Set[WalletCoinRecord]: + spendable: List[WalletCoinRecord] = list( + await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records) + ) + spendable.sort(reverse=True, key=lambda record: record.coin.amount) + return set(spendable[0 : min(len(spendable), self.max_send_quantity)]) - return uint128(total_amount) + async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128: + return uint128(sum(cr.coin.amount for cr in await self.get_max_spendable_coins())) def get_name(self) -> str: return self.wallet_info.name @@ -491,7 +486,7 @@ async def get_cat_spendable_coins(self, records: Optional[Set[WalletCoinRecord]] if lineage is not None and not lineage.is_none(): result.append(record) - return result + return list(await self.get_max_spendable_coins(set(result))) async def select_coins( self, @@ -793,9 +788,6 @@ async def generate_signed_transaction( payments.append(Payment(puzhash, amount, memos_with_hint)) payment_sum = sum([p.amount for p in payments]) - max_send = await self.get_max_send_amount() - if payment_sum > max_send: - raise ValueError(f" Insufficient funds. Your max amount is {max_send} mojos in a single transaction.") unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( payments, tx_config, diff --git a/chia/wallet/coin_selection.py b/chia/wallet/coin_selection.py index fbe604e8f130..394be49495a0 100644 --- a/chia/wallet/coin_selection.py +++ b/chia/wallet/coin_selection.py @@ -55,7 +55,7 @@ async def select_coins( # but unconfirmed, and we are waiting for the change. (unconfirmed_additions) if sum_spendable_coins < amount: raise ValueError( - f"Transaction for {amount} is greater than spendable balance of {sum_spendable_coins}. " + f"Transaction for {amount} is greater than max spendable balance in a block of {sum_spendable_coins}. " "There may be other transactions pending or our minimum coin amount is too high." ) if amount == 0 and sum_spendable_coins == 0: diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 13c123f532ce..10e0a5f5f282 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -71,26 +71,20 @@ async def create( def cost_of_single_tx(self) -> int: return 11000000 # Estimate - async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128: + @property + def max_send_quantity(self) -> int: + # avoid full block TXs + return int(self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 5 / self.cost_of_single_tx) + + async def get_max_spendable_coins(self, records: Optional[Set[WalletCoinRecord]] = None) -> Set[WalletCoinRecord]: 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) + return set(spendable[0 : min(len(spendable), self.max_send_quantity)]) - 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) + async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128: + return uint128(sum(cr.coin.amount for cr in await self.get_max_spendable_coins())) @classmethod def type(cls) -> WalletType: @@ -219,9 +213,7 @@ async def select_coins( 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()) - ) + spendable_coins: List[WalletCoinRecord] = list(await self.get_max_spendable_coins()) # Try to use coins from the store, if there isn't enough of "unused" # coins use change coins that are not confirmed yet @@ -267,10 +259,6 @@ async def _generate_unsigned_transaction( total_amount = amount + sum(primary.amount for primary in primaries) + fee total_balance = await self.get_spendable_balance() - 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( diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index d1ff6522627c..d8add8766b8e 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -1308,12 +1308,7 @@ async def test_wallet_create_hit_max_send_amount( await time_out_assert(20, wallet.get_confirmed_balance, expected_confirmed_balance) - primaries = [ - Payment(ph, uint64(1000000000 + i)) - for i in range( - int(wallet.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM / 5 / wallet.cost_of_single_tx) + 1 - ) - ] + primaries = [Payment(ph, uint64(1000000000 + i)) for i in range(int(wallet.max_send_quantity) + 1)] [tx_split_coins] = await wallet.generate_signed_transaction( uint64(1), ph, DEFAULT_TX_CONFIG, uint64(0), primaries=primaries ) @@ -1349,7 +1344,8 @@ async def test_wallet_create_hit_max_send_amount( # 3) Generate transaction that is greater than limit with pytest.raises( ValueError, - match=f"Can't send more than {max_sent_amount} mojos in a single transaction, got {max_sent_amount + 1}", + match=f"Transaction for {max_sent_amount + 1} is greater than max spendable balance in a block of " + f"{max_sent_amount}. There may be other transactions pending or our minimum coin amount is too high.", ): await wallet.generate_signed_transaction( uint64(max_sent_amount + 1), From 49f5b96925ecc5df98a0d6aa4f36cc799ff6f098 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 17 Nov 2023 10:46:07 +1300 Subject: [PATCH 007/274] vault puzzles and tests --- .../puzzles/deployed_puzzle_hashes.json | 4 + chia/wallet/puzzles/p2_delegated_secp.clsp | 14 ++ .../wallet/puzzles/p2_delegated_secp.clsp.hex | 1 + chia/wallet/puzzles/vault_recovery.clsp | 76 +++++++++ chia/wallet/puzzles/vault_recovery.clsp.hex | 1 + .../wallet/puzzles/vault_recovery_escape.clsp | 18 ++ .../puzzles/vault_recovery_escape.clsp.hex | 1 + .../wallet/puzzles/vault_recovery_finish.clsp | 13 ++ .../puzzles/vault_recovery_finish.clsp.hex | 1 + tests/wallet/vault/__init__.py | 0 tests/wallet/vault/test_vault_clsp.py | 157 ++++++++++++++++++ 11 files changed, 286 insertions(+) create mode 100644 chia/wallet/puzzles/p2_delegated_secp.clsp create mode 100644 chia/wallet/puzzles/p2_delegated_secp.clsp.hex create mode 100644 chia/wallet/puzzles/vault_recovery.clsp create mode 100644 chia/wallet/puzzles/vault_recovery.clsp.hex create mode 100644 chia/wallet/puzzles/vault_recovery_escape.clsp create mode 100644 chia/wallet/puzzles/vault_recovery_escape.clsp.hex create mode 100644 chia/wallet/puzzles/vault_recovery_finish.clsp create mode 100644 chia/wallet/puzzles/vault_recovery_finish.clsp.hex create mode 100644 tests/wallet/vault/__init__.py create mode 100644 tests/wallet/vault/test_vault_clsp.py diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 080c059108b1..1c7428e923c5 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -45,6 +45,7 @@ "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", + "p2_delegated_secp": "f02d79588cbfea39018c8e4e9defd6edf17eca7f47f8df8a433702c406e3c47a", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", "p2_parent": "b10ce2d0b18dcf8c21ddfaf55d9b9f0adcbf1e0beb55b1a8b9cad9bbff4e5f22", "p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363", @@ -61,5 +62,8 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", + "vault_recovery": "dba16216348c39fed35fde431c9ad5b0f81e8f7cf9bd6d470ccabc235fa04b14", + "vault_recovery_escape": "0b2d3925e9a5097d95734f80f9205b4f6e37d69c317907cbc01a208f37fa9b40", + "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp b/chia/wallet/puzzles/p2_delegated_secp.clsp new file mode 100644 index 000000000000..3480e01ee650 --- /dev/null +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp @@ -0,0 +1,14 @@ +; p2_delegated with SECP256-R1 signature +; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) + +(mod (SECP_PK delegated_puzzle delegated_solution signature) + (include *standard-cl-21*) + (include condition_codes.clib) + (include sha256tree.clib) + + (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle)) signature) + ; secp256r1_verify returns 0 if successful. + (x) ; this doesn't actually run because secp256_verify will raise on failure + (a delegated_puzzle delegated_solution) + ) +) diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex new file mode 100644 index 000000000000..0f9d7a333f20 --- /dev/null +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ffff841c3a8f00ff05ffff0bffff02ff02ffff04ff02ffff04ff0bff8080808080ff2f80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff02ff0bff1780ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/puzzles/vault_recovery.clsp b/chia/wallet/puzzles/vault_recovery.clsp new file mode 100644 index 000000000000..66b2437175ee --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery.clsp @@ -0,0 +1,76 @@ +; Recovery spend path for vault +; This puzzle is included in the vault puzzle (p2_1_of_n) merkle root +; when executed it creates a new p2_1_of_n of the timelocked p2_conditions and the escape puzzle +; the escape puzzle sends funds back to the vault (my_puzzlehash) + +(mod + ( + P2_1_OF_N_MOD_HASH + ESCAPE_RECOVERY_MOD_HASH + FINISH_RECOVERY_MOD_HASH + BLS_PK + TIMELOCK + my_puzzlehash + my_amount + recovery_conditions + ) + + (include *standard-cl-21*) + (include condition_codes.clib) + (include sha256tree.clib) + (include curry.clib) + + (defconstant ONE 1) + + (defun create_recovery_puzzlehash + ( + P2_1_OF_N_MOD_HASH + ESCAPE_RECOVERY_MOD_HASH + FINISH_RECOVERY_MOD_HASH + BLS_PK + TIMELOCK + my_puzzlehash + my_amount + recovery_conditions + ) + (curry_hashes P2_1_OF_N_MOD_HASH + (sha256 ONE + ; calculate the merkle root of the two puzzles + (sha256 + TWO + (sha256 ONE + (curry_hashes ESCAPE_RECOVERY_MOD_HASH + (sha256 ONE BLS_PK) (sha256 ONE my_puzzlehash) (sha256 ONE my_amount) + ) + ) + (sha256 ONE + (curry_hashes FINISH_RECOVERY_MOD_HASH + (sha256 ONE TIMELOCK) (sha256tree recovery_conditions) + ) + ) + ) + ) + ) + ) + + (list + (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) + (list CREATE_COIN + (create_recovery_puzzlehash + P2_1_OF_N_MOD_HASH + ESCAPE_RECOVERY_MOD_HASH + FINISH_RECOVERY_MOD_HASH + BLS_PK + TIMELOCK + my_puzzlehash + my_amount + recovery_conditions + ) + my_amount + ) + (list ASSERT_MY_AMOUNT my_amount) + (list ASSERT_MY_PUZZLEHASH my_puzzlehash) + ) + + +) diff --git a/chia/wallet/puzzles/vault_recovery.clsp.hex b/chia/wallet/puzzles/vault_recovery.clsp.hex new file mode 100644 index 000000000000..34ccbd7dab3e --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff8202ffff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff8200bfffff04ff82017fffff04ff8202ffff8080808080808080808080ffff04ff82017fffff0180808080ffff04ffff04ffff0149ffff04ff82017fffff01808080ffff04ffff04ffff0148ffff04ff8200bfffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff82017f80ff8080808080808080ffff0bffff0101ffff02ff16ffff04ff02ffff04ff17ffff04ffff0bffff0101ff5f80ffff04ffff02ff08ffff04ff02ffff04ff8202ffff80808080ff808080808080808080ff8080808080ff018080 diff --git a/chia/wallet/puzzles/vault_recovery_escape.clsp b/chia/wallet/puzzles/vault_recovery_escape.clsp new file mode 100644 index 000000000000..4ea7d4b6d32e --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery_escape.clsp @@ -0,0 +1,18 @@ +; Escape spend for a vault recovery +; This is to cancel a recovery spend (before the timelocked p2_conditions path is spent) +; BLS_PK is curried into vault_recovery +; P2_PUZZLEHASH is the puzzlehash for the vault root +; AMOUNT is the full amount of the coin + +(mod + (BLS_PK P2_PUZZLEHASH AMOUNT) + + (include *standard-cl-21*) + (include condition_codes.clib) + + (list + (list AGG_SIG_ME BLS_PK (sha256 P2_PUZZLEHASH AMOUNT)) + (list CREATE_COIN P2_PUZZLEHASH AMOUNT) + (list ASSERT_MY_AMOUNT AMOUNT) + ) +) diff --git a/chia/wallet/puzzles/vault_recovery_escape.clsp.hex b/chia/wallet/puzzles/vault_recovery_escape.clsp.hex new file mode 100644 index 000000000000..57ea9a84be15 --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery_escape.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ffff0132ffff04ff05ffff04ffff0bff0bff1780ffff0180808080ffff04ffff04ffff0133ffff04ff0bffff04ff17ffff0180808080ffff04ffff04ffff0149ffff04ff17ffff01808080ffff0180808080ffff04ff80ff018080 diff --git a/chia/wallet/puzzles/vault_recovery_finish.clsp b/chia/wallet/puzzles/vault_recovery_finish.clsp new file mode 100644 index 000000000000..05bb5eb4338c --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery_finish.clsp @@ -0,0 +1,13 @@ +; p2_conditions for vault +; TIMELOCK is curried into the vault_recovery puzzle +; CONDITIONS are curried in when a recovery is initiated + +(mod (TIMELOCK CONDITIONS) + (include *standard-cl-21*) + (include condition_codes.clib) + + (c + (list ASSERT_SECONDS_RELATIVE TIMELOCK) + CONDITIONS + ) +) diff --git a/chia/wallet/puzzles/vault_recovery_finish.clsp.hex b/chia/wallet/puzzles/vault_recovery_finish.clsp.hex new file mode 100644 index 000000000000..0f3a14d66f08 --- /dev/null +++ b/chia/wallet/puzzles/vault_recovery_finish.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ffff0150ffff04ff05ffff01808080ff0b80ffff04ff80ff018080 diff --git a/tests/wallet/vault/__init__.py b/tests/wallet/vault/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py new file mode 100644 index 000000000000..961022b24bbb --- /dev/null +++ b/tests/wallet/vault/test_vault_clsp.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from hashlib import sha256 +from typing import Tuple + +import pytest +from blspy import PrivateKey +from chia_rs import ENABLE_SECP_OPS +from ecdsa import NIST256p, SigningKey + +from chia.types.blockchain_format.program import INFINITE_COST, Program +from chia.types.condition_opcodes import ConditionOpcode +from chia.util.condition_tools import conditions_dict_for_solution +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.util.merkle_tree import MerkleTree +from tests.clvm.test_puzzles import secret_exponent_for_index + +P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_secp.clsp") +P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") +P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() +RECOVERY_MOD: Program = load_clvm("vault_recovery.clsp") +RECOVERY_MOD_HASH = RECOVERY_MOD.get_tree_hash() +RECOVERY_ESCAPE_MOD: Program = load_clvm("vault_recovery_escape.clsp") +RECOVERY_ESCAPE_MOD_HASH = RECOVERY_ESCAPE_MOD.get_tree_hash() +RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") +RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() +ACS = Program.to(1) +ACS_PH = ACS.get_tree_hash() + + +def run_with_secp(puzzle: Program, solution: Program) -> Tuple[int, Program]: + return puzzle._run(INFINITE_COST, ENABLE_SECP_OPS, solution) + + +def test_recovery_puzzles() -> None: + bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + bls_pk = bls_sk.get_g1() + + p2_puzzlehash = ACS_PH + vault_puzzlehash = Program.to("vault_puzzlehash") + amount = 10000 + timelock = 5000 + recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) + + curried_escape_puzzle = RECOVERY_ESCAPE_MOD.curry(bls_pk, vault_puzzlehash, amount) + curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + + curried_recovery_puzzle = RECOVERY_MOD.curry( + P2_1_OF_N_MOD_HASH, RECOVERY_ESCAPE_MOD_HASH, RECOVERY_FINISH_MOD_HASH, bls_pk, timelock + ) + + recovery_solution = Program.to([vault_puzzlehash, amount, recovery_conditions]) + + conds = conditions_dict_for_solution(curried_recovery_puzzle, recovery_solution, INFINITE_COST) + + # Calculate the merkle root and expected recovery puzzle + merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) + merkle_root = merkle_tree.calculate_root() + recovery_puzzle = P2_1_OF_N_MOD.curry(merkle_root) + recovery_puzzlehash = recovery_puzzle.get_tree_hash() + + # check for correct puzhash in conditions + assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == recovery_puzzlehash + + # Spend the recovery puzzle + # 1. Finish Recovery (after timelock) + proof = merkle_tree.generate_proof(curried_finish_puzzle.get_tree_hash()) + finish_proof = Program.to((proof[0], proof[1][0])) + inner_solution = Program.to([]) + finish_solution = Program.to([finish_proof, curried_finish_puzzle, inner_solution]) + finish_conds = conditions_dict_for_solution(recovery_puzzle, finish_solution, INFINITE_COST) + assert finish_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == p2_puzzlehash + + # 2. Escape Recovery + proof = merkle_tree.generate_proof(curried_escape_puzzle.get_tree_hash()) + escape_proof = Program.to((proof[0], proof[1][0])) + inner_solution = Program.to([]) + escape_solution = Program.to([escape_proof, curried_escape_puzzle, inner_solution]) + escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) + assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == vault_puzzlehash + + +def test_p2_delegated_secp() -> None: + secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) + secp_pk = secp_sk.verifying_key.to_string("compressed") + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + + delegated_puzzle = ACS + delegated_solution = Program.to([[51, ACS_PH, 1000]]) + signed_delegated_puzzle = secp_sk.sign_deterministic(delegated_puzzle.get_tree_hash()) + + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle]) + _, conds = run_with_secp(secp_puzzle, secp_solution) + + assert conds.at("frf").as_atom() == ACS_PH + + # test that a bad secp sig fails + sig_bytes = bytearray(signed_delegated_puzzle) + sig_bytes[0] ^= (sig_bytes[0] + 1) % 256 + bad_signature = bytes(sig_bytes) + + bad_solution = Program.to([delegated_puzzle, delegated_solution, bad_signature]) + with pytest.raises(ValueError, match="secp256r1_verify failed"): + run_with_secp(secp_puzzle, bad_solution) + + +def test_vault_root_puzzle() -> None: + # create the secp and recovery puzzles + # secp puzzle + secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) + secp_pk = secp_sk.verifying_key.to_string("compressed") + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + secp_puzzlehash = secp_puzzle.get_tree_hash() + + # recovery puzzle + bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + bls_pk = bls_sk.get_g1() + timelock = 5000 + amount = 10000 + + recovery_puzzle = RECOVERY_MOD.curry( + P2_1_OF_N_MOD_HASH, RECOVERY_ESCAPE_MOD_HASH, RECOVERY_FINISH_MOD_HASH, bls_pk, timelock + ) + recovery_puzzlehash = recovery_puzzle.get_tree_hash() + + # create the vault root puzzle + vault_merkle_tree = MerkleTree([secp_puzzlehash, recovery_puzzlehash]) + vault_merkle_root = vault_merkle_tree.calculate_root() + vault_puzzle = P2_1_OF_N_MOD.curry(vault_merkle_root) + vault_puzzlehash = vault_puzzle.get_tree_hash() + + # secp spend path + delegated_puzzle = ACS + delegated_solution = Program.to([[51, ACS_PH, amount]]) + signed_delegated_puzzle = secp_sk.sign_deterministic(delegated_puzzle.get_tree_hash()) + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle]) + proof = vault_merkle_tree.generate_proof(secp_puzzlehash) + secp_proof = Program.to((proof[0], proof[1][0])) + vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) + secp_conds = conditions_dict_for_solution(vault_puzzle, vault_solution, INFINITE_COST) + assert secp_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH + + # recovery spend path + recovery_conditions = Program.to([[51, ACS_PH, amount]]) + curried_escape_puzzle = RECOVERY_ESCAPE_MOD.curry(bls_pk, vault_puzzlehash, amount) + curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + recovery_merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) + recovery_merkle_root = recovery_merkle_tree.calculate_root() + recovery_merkle_puzzle = P2_1_OF_N_MOD.curry(recovery_merkle_root) + recovery_merkle_puzzlehash = recovery_merkle_puzzle.get_tree_hash() + recovery_solution = Program.to([vault_puzzlehash, amount, recovery_conditions]) + + proof = vault_merkle_tree.generate_proof(recovery_puzzlehash) + recovery_proof = Program.to((proof[0], proof[1][0])) + vault_solution = Program.to([recovery_proof, recovery_puzzle, recovery_solution]) + recovery_conds = conditions_dict_for_solution(vault_puzzle, vault_solution, INFINITE_COST) + assert recovery_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == recovery_merkle_puzzlehash From 3016ae87c5f83b431aba8fb7a83513781e84e0d7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 19 Oct 2023 08:14:59 -0700 Subject: [PATCH 008/274] Return offer transactions from create_offer_for_ids --- chia/data_layer/data_layer_wallet.py | 16 ++++++++--- chia/rpc/wallet_rpc_api.py | 3 ++- chia/wallet/nft_wallet/nft_wallet.py | 4 +-- chia/wallet/trade_manager.py | 28 ++++++++++++-------- tests/wallet/cat_wallet/test_trades.py | 24 ++++++++--------- tests/wallet/db_wallet/test_dl_offers.py | 6 ++--- tests/wallet/nft_wallet/test_nft_1_offers.py | 18 ++++++------- tests/wallet/nft_wallet/test_nft_offers.py | 14 +++++----- 8 files changed, 65 insertions(+), 48 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index b9c23b24b841..71d953f880cd 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -1155,7 +1155,7 @@ async def make_update_offer( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Offer: + ) -> Tuple[Offer, List[TransactionRecord]]: dl_wallet = None for wallet in wallet_state_manager.wallets.values(): if wallet.type() == WalletType.DATA_LAYER.value: @@ -1167,6 +1167,7 @@ async def make_update_offer( offered_launchers: List[bytes32] = [k for k, v in offer_dict.items() if v < 0 and k is not None] fee_left_to_pay: uint64 = fee all_bundles: List[SpendBundle] = [] + all_transactions: List[TransactionRecord] = [] for launcher in offered_launchers: try: this_solver: Solver = solver[launcher.hex()] @@ -1212,7 +1213,16 @@ async def make_update_offer( txs[0].spend_bundle, coin_spends=all_other_spends, ) - all_bundles.append(SpendBundle.aggregate([signed_bundle, new_bundle])) + agg_bundle: SpendBundle = SpendBundle.aggregate([signed_bundle, new_bundle]) + all_bundles.append(agg_bundle) + all_transactions.append( + dataclasses.replace( + txs[0], + spend_bundle=agg_bundle, + name=agg_bundle.name(), + ) + ) + all_transactions.extend(txs[1:]) # create some dummy requested payments requested_payments = { @@ -1220,7 +1230,7 @@ async def make_update_offer( for k, v in offer_dict.items() if v > 0 } - return Offer(requested_payments, SpendBundle.aggregate(all_bundles), driver_dict) + return Offer(requested_payments, SpendBundle.aggregate(all_bundles), driver_dict), all_transactions @staticmethod async def finish_graftroot_solutions(offer: Offer, solver: Solver) -> Offer: diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index cc8d570229aa..0eae0f37c696 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1843,10 +1843,11 @@ async def create_offer_for_ids( extra_conditions=extra_conditions, ) if result[0]: - success, trade_record, error = result + success, trade_record, tx_records, error = result return { "offer": Offer.from_bytes(trade_record.offer).to_bech32(), "trade_record": trade_record.to_json_dict_convenience(), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in tx_records], } raise ValueError(result[2]) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 675d904c4998..d5fa9afa2e70 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -796,7 +796,7 @@ async def make_nft1_offer( tx_config: TXConfig, fee: uint64, extra_conditions: Tuple[Condition, ...], - ) -> Offer: + ) -> Tuple[Offer, List[TransactionRecord]]: # First, let's take note of all the royalty enabled NFTs royalty_nft_asset_dict: Dict[bytes32, int] = {} for asset, amount in offer_dict.items(): @@ -1089,7 +1089,7 @@ async def make_nft1_offer( txs_bundle = SpendBundle.aggregate([tx.spend_bundle for tx in all_transactions if tx.spend_bundle is not None]) aggregate_bundle = SpendBundle.aggregate([txs_bundle, *additional_bundles]) offer = Offer(notarized_payments, aggregate_bundle, driver_dict) - return offer + return offer, all_transactions async def set_bulk_nft_did( self, diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 9c72d958aa12..08fb9928b195 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -5,7 +5,7 @@ import time from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast -from typing_extensions import Literal +from typing_extensions import Literal, Never from chia.data_layer.data_layer_wallet import DataLayerWallet from chia.protocols.wallet_protocol import CoinState @@ -374,7 +374,9 @@ async def create_offer_for_ids( validate_only: bool = False, extra_conditions: Tuple[Condition, ...] = tuple(), taking: bool = False, - ) -> Union[Tuple[Literal[True], TradeRecord, None], Tuple[Literal[False], None, str]]: + ) -> Union[ + Tuple[Literal[True], TradeRecord, List[TransactionRecord], None], Tuple[Literal[False], None, List[Never], str] + ]: if driver_dict is None: driver_dict = {} if solver is None: @@ -391,7 +393,7 @@ async def create_offer_for_ids( if not result[0] or result[1] is None: raise Exception(f"Error creating offer: {result[2]}") - success, created_offer, error = result + success, created_offer, tx_records, error = result now = uint64(int(time.time())) trade_offer: TradeRecord = TradeRecord( @@ -412,7 +414,7 @@ async def create_offer_for_ids( if success is True and trade_offer is not None and not validate_only: await self.save_trade(trade_offer, created_offer) - return success, trade_offer, error + return success, trade_offer, tx_records, error async def _create_offer_for_ids( self, @@ -423,7 +425,9 @@ async def _create_offer_for_ids( fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), taking: bool = False, - ) -> Union[Tuple[Literal[True], Offer, None], Tuple[Literal[False], None, str]]: + ) -> Union[ + Tuple[Literal[True], Offer, List[TransactionRecord], None], Tuple[Literal[False], None, List[Never], str] + ]: """ Offer is dictionary of wallet ids and amount """ @@ -512,7 +516,9 @@ async def _create_offer_for_ids( requested_payments, driver_dict, taking ) - potential_special_offer: Optional[Offer] = await self.check_for_special_offer_making( + potential_special_offer: Optional[ + Tuple[Offer, List[TransactionRecord]] + ] = await self.check_for_special_offer_making( offer_dict_no_ints, driver_dict, tx_config, @@ -522,7 +528,7 @@ async def _create_offer_for_ids( ) if potential_special_offer is not None: - return True, potential_special_offer, None + return True, potential_special_offer[0], potential_special_offer[1], None all_coins: List[Coin] = [c for coins in coins_to_offer.values() for c in coins] notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments( @@ -586,11 +592,11 @@ async def _create_offer_for_ids( ) offer = Offer(notarized_payments, total_spend_bundle, driver_dict) - return True, offer, None + return True, offer, all_transactions, None except Exception as e: self.log.exception("Error creating trade offer") - return False, None, str(e) + return False, None, [], str(e) async def maybe_create_wallets_for_offer(self, offer: Offer) -> None: for key in offer.arbitrage(): @@ -773,7 +779,7 @@ async def respond_to_offer( if not result[0] or result[1] is None: raise ValueError(result[2]) - success, take_offer, error = result + success, take_offer, _, error = result complete_offer, valid_spend_solver = await self.check_for_final_modifications( Offer.aggregate([offer, take_offer]), solver, tx_config @@ -835,7 +841,7 @@ async def check_for_special_offer_making( solver: Solver, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Optional[Offer]: + ) -> Optional[Tuple[Offer, List[TransactionRecord]]]: for puzzle_info in driver_dict.values(): if ( puzzle_info.check_type([AssetType.SINGLETON.value, AssetType.METADATA.value, AssetType.OWNERSHIP.value]) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 1c88f681680c..4b627c848c83 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -442,7 +442,7 @@ async def test_cat_trades( taker_unused_index = taker_unused_dr.index # Execute all of the trades # chia_for_cat - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( chia_for_cat, wallet_environments.tx_config, fee=uint64(1) ) assert error is None @@ -650,7 +650,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): ) # cat_for_chia - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( cat_for_chia, wallet_environments.tx_config ) assert error is None @@ -768,7 +768,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): ) assert taker_unused_dr is not None taker_unused_index = taker_unused_dr.index - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( cat_for_cat, wallet_environments.tx_config ) assert error is None @@ -954,7 +954,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): assert taker_unused_index < taker_unused_dr.index # chia_for_multiple_cat - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( chia_for_multiple_cat, wallet_environments.tx_config, driver_dict=driver_dict, @@ -1196,7 +1196,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): ) # multiple_cat_for_chia - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( multiple_cat_for_chia, wallet_environments.tx_config, ) @@ -1321,7 +1321,7 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) # chia_and_cat_for_cat - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( chia_and_cat_for_cat, wallet_environments.tx_config, ) @@ -1563,7 +1563,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) return TradeStatus(trade_rec.status) - success, trade_make, error = await trade_manager_maker.create_offer_for_ids(cat_for_chia, DEFAULT_TX_CONFIG) + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids(cat_for_chia, DEFAULT_TX_CONFIG) assert error is None assert success is True assert trade_make is not None @@ -1623,7 +1623,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: await trade_manager_taker.respond_to_offer(Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG) # Now we're going to create the other way around for test coverage sake - success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) assert error is None assert success is True assert trade_make is not None @@ -1684,7 +1684,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) return TradeStatus(trade_rec.status) - success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) await time_out_assert(10, get_trade_and_status, TradeStatus.PENDING_ACCEPT, trade_manager_maker, trade_make) assert error is None assert success is True @@ -1743,7 +1743,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: return TradeStatus(trade_rec.status) raise ValueError("Couldn't find the trade record") - success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) await time_out_assert(10, get_trade_and_status, TradeStatus.PENDING_ACCEPT, trade_manager_maker, trade_make) assert error is None assert success is True @@ -1806,7 +1806,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: return TradeStatus(trade_rec.status) raise ValueError("Couldn't find the trade record") - success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) await time_out_assert(30, get_trade_and_status, TradeStatus.PENDING_ACCEPT, trade_manager_maker, trade_make) assert error is None assert success is True @@ -1871,7 +1871,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: return TradeStatus(trade_rec.status) raise ValueError("Couldn't find the trade record") - success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) await time_out_assert(10, get_trade_and_status, TradeStatus.PENDING_ACCEPT, trade_manager_maker, trade_make) assert error is None assert success is True diff --git a/tests/wallet/db_wallet/test_dl_offers.py b/tests/wallet/db_wallet/test_dl_offers.py index 18788f1ec9ff..819e3c8d6d19 100644 --- a/tests/wallet/db_wallet/test_dl_offers.py +++ b/tests/wallet/db_wallet/test_dl_offers.py @@ -104,7 +104,7 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: fee = uint64(2_000_000_000_000) - success, offer_maker, error = await trade_manager_maker.create_offer_for_ids( + success, offer_maker, _, error = await trade_manager_maker.create_offer_for_ids( {launcher_id_maker: -1, launcher_id_taker: 1}, DEFAULT_TX_CONFIG, solver=Solver( @@ -266,7 +266,7 @@ async def test_dl_offer_cancellation(wallets_prefarm: Any, trusted: bool) -> Non ROWS.append(addition) root, proofs = build_merkle_tree(ROWS) - success, offer, error = await trade_manager.create_offer_for_ids( + success, offer, _, error = await trade_manager.create_offer_for_ids( {launcher_id: -1, launcher_id_2: 1}, DEFAULT_TX_CONFIG, solver=Solver( @@ -386,7 +386,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: fee = uint64(2_000_000_000_000) - success, offer_maker, error = await trade_manager_maker.create_offer_for_ids( + success, offer_maker, _, error = await trade_manager_maker.create_offer_for_ids( {launcher_id_maker_1: -1, launcher_id_taker_1: 1, launcher_id_maker_2: -1, launcher_id_taker_2: 1}, DEFAULT_TX_CONFIG, solver=Solver( diff --git a/tests/wallet/nft_wallet/test_nft_1_offers.py b/tests/wallet/nft_wallet/test_nft_1_offers.py index e6714d46be43..cfb5c3407698 100644 --- a/tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/tests/wallet/nft_wallet/test_nft_1_offers.py @@ -169,7 +169,7 @@ async def test_nft_offer_sell_nft( offer_did_nft_for_xch = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_did_nft_for_xch, DEFAULT_TX_CONFIG, {}, fee=maker_fee ) assert success is True @@ -312,7 +312,7 @@ async def test_nft_offer_request_nft( offer_dict = {nft_to_request_asset_id: 1, wallet_maker.id(): -xch_offered} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_dict, DEFAULT_TX_CONFIG, driver_dict, fee=maker_fee ) assert success is True @@ -466,7 +466,7 @@ async def test_nft_offer_sell_did_to_did( offer_did_nft_for_xch = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_did_nft_for_xch, DEFAULT_TX_CONFIG, {}, fee=maker_fee ) assert success is True @@ -650,7 +650,7 @@ async def test_nft_offer_sell_nft_for_cat( offer_did_nft_for_xch = {nft_to_offer_asset_id: -1, cat_wallet_maker.id(): cats_requested} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_did_nft_for_xch, DEFAULT_TX_CONFIG, {}, fee=maker_fee ) @@ -842,7 +842,7 @@ async def test_nft_offer_request_nft_for_cat( offer_dict = {nft_to_request_asset_id: 1, cat_wallet_maker.id(): -cats_requested} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_dict, DEFAULT_TX_CONFIG, driver_dict, fee=maker_fee ) assert success is True @@ -976,7 +976,7 @@ async def test_nft_offer_sell_cancel( offer_did_nft_for_xch = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_did_nft_for_xch, DEFAULT_TX_CONFIG, {}, fee=maker_fee ) @@ -1097,7 +1097,7 @@ async def test_nft_offer_sell_cancel_in_batch( offer_did_nft_for_xch = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_did_nft_for_xch, DEFAULT_TX_CONFIG, {}, fee=maker_fee ) @@ -1362,7 +1362,7 @@ async def test_complex_nft_offer( ), } - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( complex_nft_offer, DEFAULT_TX_CONFIG, driver_dict=driver_dict, fee=FEE ) assert error is None @@ -1475,7 +1475,7 @@ async def get_cat_wallet_and_check_balance(asset_id: str, wsm: Any) -> uint128: ), } - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( complex_nft_offer, DEFAULT_TX_CONFIG, driver_dict=driver_dict, fee=uint64(0) ) assert error is None diff --git a/tests/wallet/nft_wallet/test_nft_offers.py b/tests/wallet/nft_wallet/test_nft_offers.py index d802ad27b14f..0bbdb13e52ac 100644 --- a/tests/wallet/nft_wallet/test_nft_offers.py +++ b/tests/wallet/nft_wallet/test_nft_offers.py @@ -135,7 +135,7 @@ async def test_nft_offer_with_fee( await wallet_taker.wallet_state_manager.puzzle_store.get_current_derivation_record_for_wallet(uint32(1)) ).index - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_nft_for_xch, tx_config, driver_dict, fee=maker_fee ) assert success is True @@ -207,7 +207,7 @@ async def test_nft_offer_with_fee( maker_fee = uint64(10) offer_xch_for_nft = {wallet_maker.id(): -xch_offered, nft_to_buy_asset_id: 1} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_xch_for_nft, tx_config, driver_dict_to_buy, fee=maker_fee ) assert success is True @@ -325,7 +325,7 @@ async def test_nft_offer_cancellations( maker_fee = uint64(10) offer_nft_for_xch = {wallet_maker.id(): xch_request, nft_asset_id: -1} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_nft_for_xch, DEFAULT_TX_CONFIG, driver_dict, fee=maker_fee ) assert success is True @@ -469,7 +469,7 @@ async def test_nft_offer_with_metadata_update( maker_fee = uint64(10) offer_nft_for_xch = {wallet_maker.id(): xch_request, nft_asset_id: -1} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_nft_for_xch, DEFAULT_TX_CONFIG, driver_dict, fee=maker_fee ) assert success is True @@ -644,7 +644,7 @@ async def test_nft_offer_nft_for_cat( await wallet_taker.wallet_state_manager.puzzle_store.get_current_derivation_record_for_wallet(uint32(1)) ).index - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_nft_for_cat, tx_config, driver_dict, fee=maker_fee ) assert success is True @@ -728,7 +728,7 @@ async def test_nft_offer_nft_for_cat( cat_wallet_maker.id(): -maker_cat_amount, } - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_multi_cats_for_nft, tx_config, driver_dict_to_buy, fee=maker_fee ) assert success is True @@ -878,7 +878,7 @@ async def test_nft_offer_nft_for_nft( maker_fee = uint64(10) offer_nft_for_nft = {nft_to_take_asset_id: 1, nft_to_offer_asset_id: -1} - success, trade_make, error = await trade_manager_maker.create_offer_for_ids( + success, trade_make, _, error = await trade_manager_maker.create_offer_for_ids( offer_nft_for_nft, DEFAULT_TX_CONFIG, driver_dict, fee=maker_fee ) assert success is True From 14086bc7e98095273cb614109398c4a900d4defd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 25 Oct 2023 06:49:24 -0700 Subject: [PATCH 009/274] Return transaction list from CATWallet.create_new_cat_wallet --- chia/rpc/wallet_rpc_api.py | 9 +++++++-- chia/wallet/cat_wallet/cat_wallet.py | 4 ++-- chia/wallet/dao_wallet/dao_wallet.py | 2 +- chia/wallet/vc_wallet/cr_cat_wallet.py | 2 +- tests/wallet/cat_wallet/test_cat_wallet.py | 20 ++++++++++---------- tests/wallet/cat_wallet/test_trades.py | 14 +++++++------- tests/wallet/nft_wallet/test_nft_1_offers.py | 8 ++++---- tests/wallet/nft_wallet/test_nft_offers.py | 4 ++-- tests/wallet/rpc/test_wallet_rpc.py | 2 +- 9 files changed, 35 insertions(+), 30 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index cc8d570229aa..0f3da332be80 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -691,7 +691,7 @@ async def create_new_wallet( if not push: raise ValueError("Test CAT minting must be pushed automatically") # pragma: no cover async with self.service.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, txs = await CATWallet.create_new_cat_wallet( wallet_state_manager, main_wallet, {"identifier": "genesis_by_id"}, @@ -702,7 +702,12 @@ async def create_new_wallet( ) 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()} + return { + "type": cat_wallet.type(), + "asset_id": asset_id, + "wallet_id": cat_wallet.id(), + "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], + } else: raise ValueError( "Support for this RPC mode has been dropped." diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 6e4cc7f9667f..ff04f356522d 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -95,7 +95,7 @@ async def create_new_cat_wallet( tx_config: TXConfig, fee: uint64 = uint64(0), name: Optional[str] = None, - ) -> CATWallet: + ) -> Tuple[CATWallet, List[TransactionRecord]]: self = CATWallet() self.standard_wallet = wallet self.log = logging.getLogger(__name__) @@ -180,7 +180,7 @@ async def create_new_cat_wallet( ) chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle, name=spend_bundle.name()) await self.wallet_state_manager.add_pending_transactions([chia_tx, cat_record]) - return self + return self, [chia_tx, cat_record] @staticmethod async def get_or_create_wallet_for_cat( diff --git a/chia/wallet/dao_wallet/dao_wallet.py b/chia/wallet/dao_wallet/dao_wallet.py index 6fad5da3ac3e..bb1aeb15f4f4 100644 --- a/chia/wallet/dao_wallet/dao_wallet.py +++ b/chia/wallet/dao_wallet/dao_wallet.py @@ -687,7 +687,7 @@ async def generate_new_dao( "treasury_id": launcher_coin.name(), "coins": different_coins, } - new_cat_wallet = await CATWallet.create_new_cat_wallet( + new_cat_wallet, _ = await CATWallet.create_new_cat_wallet( self.wallet_state_manager, self.standard_wallet, cat_tail_info, diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index 89683adbfc0b..00b7779d741a 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -88,7 +88,7 @@ async def create_new_cat_wallet( tx_config: TXConfig, fee: uint64 = uint64(0), name: Optional[str] = None, - ) -> CATWallet: # pragma: no cover + ) -> Tuple[CATWallet, List[TransactionRecord]]: # pragma: no cover raise NotImplementedError("create_new_cat_wallet is a legacy method and is not available on CR-CAT wallets") @staticmethod diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index b8227301fc39..64732c525e65 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -75,7 +75,7 @@ async def test_cat_creation(self, self_hostname, two_wallet_nodes, trusted): await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -145,14 +145,14 @@ async def test_cat_creation_unique_lineage_store(self, self_hostname, two_wallet await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) async with wallet_node.wallet_state_manager.lock: - cat_wallet_1: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_1, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100), DEFAULT_TX_CONFIG, ) - cat_wallet_2: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_2, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -206,7 +206,7 @@ async def test_cat_spend(self, self_hostname, two_wallet_nodes, trusted): await time_out_assert(20, wallet.get_confirmed_balance, funds) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -318,7 +318,7 @@ async def test_cat_reuse_address(self, self_hostname, two_wallet_nodes, trusted) await time_out_assert(20, wallet.get_confirmed_balance, funds) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -420,7 +420,7 @@ async def test_get_wallet_for_asset_id(self, self_hostname, two_wallet_nodes, tr await time_out_assert(20, wallet.get_confirmed_balance, funds) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -482,7 +482,7 @@ async def test_cat_doesnt_see_eve(self, self_hostname, two_wallet_nodes, trusted await time_out_assert(20, wallet.get_confirmed_balance, funds) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -579,7 +579,7 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust await time_out_assert(20, wallet_0.get_confirmed_balance, funds) async with wallet_node_0.wallet_state_manager.lock: - cat_wallet_0: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_0, _ = await CATWallet.create_new_cat_wallet( wallet_node_0.wallet_state_manager, wallet_0, {"identifier": "genesis_by_id"}, @@ -700,7 +700,7 @@ async def test_cat_max_amount_send(self, self_hostname, two_wallet_nodes, truste await time_out_assert(20, wallet.get_confirmed_balance, funds) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, @@ -815,7 +815,7 @@ async def test_cat_hint(self, self_hostname, two_wallet_nodes, trusted, autodisc await time_out_assert(20, wallet.get_confirmed_balance, funds) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 1c88f681680c..078c569e7874 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -295,7 +295,7 @@ async def test_cat_trades( # Mint some standard CATs async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -304,7 +304,7 @@ async def test_cat_trades( ) async with wallet_node_taker.wallet_state_manager.lock: - new_cat_wallet_taker = await CATWallet.create_new_cat_wallet( + new_cat_wallet_taker, _ = await CATWallet.create_new_cat_wallet( wallet_node_taker.wallet_state_manager, wallet_taker, {"identifier": "genesis_by_id"}, @@ -1529,7 +1529,7 @@ async def test_trade_cancellation(self, wallets_prefarm): xch_to_cat_amount = uint64(100) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -1656,7 +1656,7 @@ async def test_trade_cancellation_balance_check(self, wallets_prefarm): xch_to_cat_amount = uint64(100) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -1711,7 +1711,7 @@ async def test_trade_conflict(self, three_wallets_prefarm): xch_to_cat_amount = uint64(100) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -1775,7 +1775,7 @@ async def test_trade_bad_spend(self, wallets_prefarm): xch_to_cat_amount = uint64(100) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -1840,7 +1840,7 @@ async def test_trade_high_fee(self, wallets_prefarm): xch_to_cat_amount = uint64(100) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, diff --git a/tests/wallet/nft_wallet/test_nft_1_offers.py b/tests/wallet/nft_wallet/test_nft_1_offers.py index e6714d46be43..4d70c776b667 100644 --- a/tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/tests/wallet/nft_wallet/test_nft_1_offers.py @@ -607,7 +607,7 @@ async def test_nft_offer_sell_nft_for_cat( cats_to_trade = uint64(10000) async with wallet_node_maker.wallet_state_manager.lock: full_node_api.full_node.log.warning(f"Mempool size: {full_node_api.full_node.mempool_manager.mempool.size()}") - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -788,7 +788,7 @@ async def test_nft_offer_request_nft_for_cat( cats_to_mint = 100000 cats_to_trade = uint64(20000) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -1173,11 +1173,11 @@ async def test_complex_nft_offer( CAT_AMOUNT = uint64(100000000) async with wsm_maker.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wsm_maker, wallet_maker, {"identifier": "genesis_by_id"}, CAT_AMOUNT, DEFAULT_TX_CONFIG ) async with wsm_maker.lock: - cat_wallet_taker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_taker, _ = await CATWallet.create_new_cat_wallet( wsm_taker, wallet_taker, {"identifier": "genesis_by_id"}, CAT_AMOUNT, DEFAULT_TX_CONFIG ) cat_spend_bundle_maker = ( diff --git a/tests/wallet/nft_wallet/test_nft_offers.py b/tests/wallet/nft_wallet/test_nft_offers.py index d802ad27b14f..ca7c685d18af 100644 --- a/tests/wallet/nft_wallet/test_nft_offers.py +++ b/tests/wallet/nft_wallet/test_nft_offers.py @@ -585,7 +585,7 @@ async def test_nft_offer_nft_for_cat( # Create two new CATs and wallets for maker and taker cats_to_mint = 10000 async with wallet_node_0.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_0.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, @@ -597,7 +597,7 @@ async def test_nft_offer_nft_for_cat( await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=20) async with wallet_node_1.wallet_state_manager.lock: - cat_wallet_taker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_taker, _ = await CATWallet.create_new_cat_wallet( wallet_node_1.wallet_state_manager, wallet_taker, {"identifier": "genesis_by_id"}, diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index cb87f126450f..4b98aef17746 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -376,7 +376,7 @@ async def test_get_balance(wallet_rpc_environment: WalletRpcTestEnvironment): wallet_rpc_client = env.wallet_1.rpc_client await full_node_api.farm_blocks_to_wallet(2, wallet) async with wallet_node.wallet_state_manager.lock: - cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet, _ = await CATWallet.create_new_cat_wallet( wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100), DEFAULT_TX_CONFIG ) await assert_get_balance(wallet_rpc_client, wallet_node, wallet) From 4ec3d09147f5354c6c502b8af7aa0e037d62d1b3 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 09:11:41 -0800 Subject: [PATCH 010/274] Add a marshaller for RPC APIs --- chia/rpc/util.py | 44 ++++++++++++++++++++++++++++++---- tests/core/test_rpc_util.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 tests/core/test_rpc_util.py diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 0b71ca25c5fe..303f214c7041 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -2,17 +2,55 @@ import logging import traceback -from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple +from dataclasses import dataclass +from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, TypeVar, get_type_hints import aiohttp +from typing_extensions import TypedDict, dataclass_transform from chia.types.blockchain_format.coin import Coin from chia.util.json_util import obj_to_response +from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_from_json_dicts, parse_timelock_info from chia.wallet.util.tx_config import TXConfig, TXConfigLoader log = logging.getLogger(__name__) +RpcEndpoint = Callable[..., Coroutine[Any, Any, Dict[str, Any]]] +MarshallableRpcEndpoint = Callable[..., Coroutine[Any, Any, Streamable]] + + +class RequestType(TypedDict): + pass + + +_T_Streamable = TypeVar("_T_Streamable", bound="Streamable") + + +@dataclass_transform() +def get_streamable_from_request_type(cls: Type[RequestType]) -> Type[_T_Streamable]: + return streamable( + dataclass(frozen=True)(type("_" + cls.__name__, (Streamable,), {"__annotations__": cls.__annotations__})) + ) + + +def marshall(func: MarshallableRpcEndpoint) -> RpcEndpoint: + hints = get_type_hints(func) + request_class: Type[RequestType] = hints["request"] + + async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]: + response_obj: Streamable = await func( + self, + request_class( + get_streamable_from_request_type(request_class).from_json_dict(request).__dict__ # type: ignore + ), + *args, + **kwargs, + ) + return response_obj.to_json_dict() + + return rpc_endpoint + def wrap_http_handler(f) -> Callable: async def inner(request) -> aiohttp.web.Response: @@ -36,9 +74,7 @@ async def inner(request) -> aiohttp.web.Response: return inner -def tx_endpoint( - func: Callable[..., Coroutine[Any, Any, Dict[str, Any]]] -) -> Callable[..., Coroutine[Any, Any, Dict[str, Any]]]: +def tx_endpoint(func: RpcEndpoint) -> RpcEndpoint: async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]: assert self.service.logged_in_fingerprint is not None tx_config_loader: TXConfigLoader = TXConfigLoader.from_json_dict(request) diff --git a/tests/core/test_rpc_util.py b/tests/core/test_rpc_util.py new file mode 100644 index 000000000000..fbfa2bdcd2fc --- /dev/null +++ b/tests/core/test_rpc_util.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List + +import pytest + +from chia.rpc.util import RequestType, marshall +from chia.util.ints import uint32 +from chia.util.streamable import Streamable, streamable + + +@streamable +@dataclass(frozen=True) +class SubObject(Streamable): + qux: str + + +class TestRequestType(RequestType): + foo: str + bar: uint32 + bat: bytes + bam: SubObject + + +@streamable +@dataclass(frozen=True) +class TestResponseObject(Streamable): + qat: List[str] + + +@pytest.mark.anyio +async def test_rpc_marshalling() -> None: + @marshall + async def test_rpc_endpoint(self: None, request: TestRequestType) -> TestResponseObject: + return TestResponseObject([request["foo"], str(request["bar"]), request["bat"].hex(), request["bam"].qux]) + + assert await test_rpc_endpoint( + None, + { + "foo": "foo", + "bar": 1, + "bat": b"\xff", + "bam": { + "qux": "qux", + }, + }, + ) == {"qat": ["foo", "1", "ff", "qux"]} From b13506122ba0fb39d5171c98402c9550f2df6ae6 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 12:42:12 -0800 Subject: [PATCH 011/274] Missed an instance in a newer test --- tests/wallet/cat_wallet/test_trades.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 4b627c848c83..533b63fd81a0 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -1931,12 +1931,16 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: return TradeStatus(trade_rec.status) raise ValueError("Couldn't find the trade record") # pragma: no cover - success, trade_make_1, error = await trade_manager_maker.create_offer_for_ids(chia_for_cat, DEFAULT_TX_CONFIG) + success, trade_make_1, _, error = await trade_manager_maker.create_offer_for_ids( + chia_for_cat, DEFAULT_TX_CONFIG + ) await time_out_assert(10, get_trade_and_status, TradeStatus.PENDING_ACCEPT, trade_manager_maker, trade_make_1) assert error is None assert success is True assert trade_make_1 is not None - success, trade_make_2, error = await trade_manager_maker.create_offer_for_ids(cat_for_chia, DEFAULT_TX_CONFIG) + success, trade_make_2, _, error = await trade_manager_maker.create_offer_for_ids( + cat_for_chia, DEFAULT_TX_CONFIG + ) await time_out_assert(10, get_trade_and_status, TradeStatus.PENDING_ACCEPT, trade_manager_maker, trade_make_2) assert error is None assert success is True From 19651bb1c8e96d02c0c7cf7079cbab9faa1c6f62 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 12:51:25 -0800 Subject: [PATCH 012/274] Missed an instance in a newer test --- tests/wallet/cat_wallet/test_trades.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 078c569e7874..eb7255e977b8 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -1896,7 +1896,7 @@ async def test_aggregated_trade_state(self, wallets_prefarm): xch_to_cat_amount = uint64(100) async with wallet_node_maker.wallet_state_manager.lock: - cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( + cat_wallet_maker, _ = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, From efe19f44f971be54933a46b1fd52bbe874345ded Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Nov 2023 13:54:09 -0800 Subject: [PATCH 013/274] Rename bad variable names I guess???? --- tests/core/test_rpc_util.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/core/test_rpc_util.py b/tests/core/test_rpc_util.py index fbfa2bdcd2fc..4ac09fbac7a2 100644 --- a/tests/core/test_rpc_util.py +++ b/tests/core/test_rpc_util.py @@ -17,8 +17,8 @@ class SubObject(Streamable): class TestRequestType(RequestType): - foo: str - bar: uint32 + foofoo: str + barbar: uint32 bat: bytes bam: SubObject @@ -33,16 +33,16 @@ class TestResponseObject(Streamable): async def test_rpc_marshalling() -> None: @marshall async def test_rpc_endpoint(self: None, request: TestRequestType) -> TestResponseObject: - return TestResponseObject([request["foo"], str(request["bar"]), request["bat"].hex(), request["bam"].qux]) + return TestResponseObject([request["foofoo"], str(request["barbar"]), request["bat"].hex(), request["bam"].qux]) assert await test_rpc_endpoint( None, { - "foo": "foo", - "bar": 1, + "foofoo": "foofoo", + "barbar": 1, "bat": b"\xff", "bam": { "qux": "qux", }, }, - ) == {"qat": ["foo", "1", "ff", "qux"]} + ) == {"qat": ["foofoo", "1", "ff", "qux"]} From f88c34a4b6be845877a8d75a3b762c9f9028935f Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sun, 19 Nov 2023 14:46:18 +1300 Subject: [PATCH 014/274] use p2_secp for recovery escape --- .../puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/vault_recovery.clsp | 16 +++------ chia/wallet/puzzles/vault_recovery.clsp.hex | 2 +- tests/wallet/vault/test_vault_clsp.py | 36 +++++++++++-------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 1c7428e923c5..c682064d4621 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -62,7 +62,7 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", - "vault_recovery": "dba16216348c39fed35fde431c9ad5b0f81e8f7cf9bd6d470ccabc235fa04b14", + "vault_recovery": "8f09bc0c13b0ee0f23f169ee4de7167dbec145b24aa4944746186bc8914ef40f", "vault_recovery_escape": "0b2d3925e9a5097d95734f80f9205b4f6e37d69c317907cbc01a208f37fa9b40", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" diff --git a/chia/wallet/puzzles/vault_recovery.clsp b/chia/wallet/puzzles/vault_recovery.clsp index 66b2437175ee..eb92f5ceb8c8 100644 --- a/chia/wallet/puzzles/vault_recovery.clsp +++ b/chia/wallet/puzzles/vault_recovery.clsp @@ -6,8 +6,8 @@ (mod ( P2_1_OF_N_MOD_HASH - ESCAPE_RECOVERY_MOD_HASH FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH BLS_PK TIMELOCK my_puzzlehash @@ -25,12 +25,9 @@ (defun create_recovery_puzzlehash ( P2_1_OF_N_MOD_HASH - ESCAPE_RECOVERY_MOD_HASH FINISH_RECOVERY_MOD_HASH - BLS_PK + P2_SECP_PUZZLEHASH TIMELOCK - my_puzzlehash - my_amount recovery_conditions ) (curry_hashes P2_1_OF_N_MOD_HASH @@ -39,9 +36,7 @@ (sha256 TWO (sha256 ONE - (curry_hashes ESCAPE_RECOVERY_MOD_HASH - (sha256 ONE BLS_PK) (sha256 ONE my_puzzlehash) (sha256 ONE my_amount) - ) + P2_SECP_PUZZLEHASH ) (sha256 ONE (curry_hashes FINISH_RECOVERY_MOD_HASH @@ -58,12 +53,9 @@ (list CREATE_COIN (create_recovery_puzzlehash P2_1_OF_N_MOD_HASH - ESCAPE_RECOVERY_MOD_HASH FINISH_RECOVERY_MOD_HASH - BLS_PK + P2_SECP_PUZZLEHASH TIMELOCK - my_puzzlehash - my_amount recovery_conditions ) my_amount diff --git a/chia/wallet/puzzles/vault_recovery.clsp.hex b/chia/wallet/puzzles/vault_recovery.clsp.hex index 34ccbd7dab3e..697c8adf571f 100644 --- a/chia/wallet/puzzles/vault_recovery.clsp.hex +++ b/chia/wallet/puzzles/vault_recovery.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff8202ffff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff8200bfffff04ff82017fffff04ff8202ffff8080808080808080808080ffff04ff82017fffff0180808080ffff04ffff04ffff0149ffff04ff82017fffff01808080ffff04ffff04ffff0148ffff04ff8200bfffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff82017f80ff8080808080808080ffff0bffff0101ffff02ff16ffff04ff02ffff04ff17ffff04ffff0bffff0101ff5f80ffff04ffff02ff08ffff04ff02ffff04ff8202ffff80808080ff808080808080808080ff8080808080ff018080 +ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff8202ffff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff8202ffff8080808080808080ffff04ff82017fffff0180808080ffff04ffff04ffff0149ffff04ff82017fffff01808080ffff04ffff04ffff0148ffff04ff8200bfffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 961022b24bbb..d098f70f97f3 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -20,8 +20,6 @@ P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() RECOVERY_MOD: Program = load_clvm("vault_recovery.clsp") RECOVERY_MOD_HASH = RECOVERY_MOD.get_tree_hash() -RECOVERY_ESCAPE_MOD: Program = load_clvm("vault_recovery_escape.clsp") -RECOVERY_ESCAPE_MOD_HASH = RECOVERY_ESCAPE_MOD.get_tree_hash() RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() ACS = Program.to(1) @@ -35,6 +33,8 @@ def run_with_secp(puzzle: Program, solution: Program) -> Tuple[int, Program]: def test_recovery_puzzles() -> None: bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) bls_pk = bls_sk.get_g1() + secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) + secp_pk = secp_sk.verifying_key.to_string("compressed") p2_puzzlehash = ACS_PH vault_puzzlehash = Program.to("vault_puzzlehash") @@ -42,11 +42,13 @@ def test_recovery_puzzles() -> None: timelock = 5000 recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) - curried_escape_puzzle = RECOVERY_ESCAPE_MOD.curry(bls_pk, vault_puzzlehash, amount) - curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + escape_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + escape_puzzlehash = escape_puzzle.get_tree_hash() + finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + finish_puzzlehash = finish_puzzle.get_tree_hash() curried_recovery_puzzle = RECOVERY_MOD.curry( - P2_1_OF_N_MOD_HASH, RECOVERY_ESCAPE_MOD_HASH, RECOVERY_FINISH_MOD_HASH, bls_pk, timelock + P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, escape_puzzlehash, bls_pk, timelock ) recovery_solution = Program.to([vault_puzzlehash, amount, recovery_conditions]) @@ -54,7 +56,7 @@ def test_recovery_puzzles() -> None: conds = conditions_dict_for_solution(curried_recovery_puzzle, recovery_solution, INFINITE_COST) # Calculate the merkle root and expected recovery puzzle - merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) + merkle_tree = MerkleTree([escape_puzzlehash, finish_puzzlehash]) merkle_root = merkle_tree.calculate_root() recovery_puzzle = P2_1_OF_N_MOD.curry(merkle_root) recovery_puzzlehash = recovery_puzzle.get_tree_hash() @@ -64,20 +66,23 @@ def test_recovery_puzzles() -> None: # Spend the recovery puzzle # 1. Finish Recovery (after timelock) - proof = merkle_tree.generate_proof(curried_finish_puzzle.get_tree_hash()) + proof = merkle_tree.generate_proof(finish_puzzlehash) finish_proof = Program.to((proof[0], proof[1][0])) inner_solution = Program.to([]) - finish_solution = Program.to([finish_proof, curried_finish_puzzle, inner_solution]) + finish_solution = Program.to([finish_proof, finish_puzzle, inner_solution]) finish_conds = conditions_dict_for_solution(recovery_puzzle, finish_solution, INFINITE_COST) assert finish_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == p2_puzzlehash # 2. Escape Recovery - proof = merkle_tree.generate_proof(curried_escape_puzzle.get_tree_hash()) + proof = merkle_tree.generate_proof(escape_puzzlehash) escape_proof = Program.to((proof[0], proof[1][0])) - inner_solution = Program.to([]) - escape_solution = Program.to([escape_proof, curried_escape_puzzle, inner_solution]) + delegated_puzzle = ACS + delegated_solution = Program.to([[51, ACS_PH, amount]]) + signed_delegated_puzzle = secp_sk.sign_deterministic(delegated_puzzle.get_tree_hash()) + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle]) + escape_solution = Program.to([escape_proof, escape_puzzle, secp_solution]) escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) - assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == vault_puzzlehash + assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH def test_p2_delegated_secp() -> None: @@ -112,14 +117,15 @@ def test_vault_root_puzzle() -> None: secp_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) secp_puzzlehash = secp_puzzle.get_tree_hash() - # recovery puzzle + # recovery keys bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) bls_pk = bls_sk.get_g1() + timelock = 5000 amount = 10000 recovery_puzzle = RECOVERY_MOD.curry( - P2_1_OF_N_MOD_HASH, RECOVERY_ESCAPE_MOD_HASH, RECOVERY_FINISH_MOD_HASH, bls_pk, timelock + P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock ) recovery_puzzlehash = recovery_puzzle.get_tree_hash() @@ -142,7 +148,7 @@ def test_vault_root_puzzle() -> None: # recovery spend path recovery_conditions = Program.to([[51, ACS_PH, amount]]) - curried_escape_puzzle = RECOVERY_ESCAPE_MOD.curry(bls_pk, vault_puzzlehash, amount) + curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) recovery_merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) recovery_merkle_root = recovery_merkle_tree.calculate_root() From 3914a94215b1f472182830b0a1256811ce7a4090 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 20 Nov 2023 20:34:28 +1300 Subject: [PATCH 015/274] rename puzzles for clarity --- chia/wallet/puzzles/deployed_puzzle_hashes.json | 2 +- .../{vault_recovery.clsp => vault_p2_recovery.clsp} | 0 ...vault_recovery.clsp.hex => vault_p2_recovery.clsp.hex} | 0 tests/wallet/vault/test_vault_clsp.py | 8 ++++---- 4 files changed, 5 insertions(+), 5 deletions(-) rename chia/wallet/puzzles/{vault_recovery.clsp => vault_p2_recovery.clsp} (100%) rename chia/wallet/puzzles/{vault_recovery.clsp.hex => vault_p2_recovery.clsp.hex} (100%) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index c682064d4621..65322dd70940 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -62,7 +62,7 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", - "vault_recovery": "8f09bc0c13b0ee0f23f169ee4de7167dbec145b24aa4944746186bc8914ef40f", + "vault_p2_recovery": "8f09bc0c13b0ee0f23f169ee4de7167dbec145b24aa4944746186bc8914ef40f", "vault_recovery_escape": "0b2d3925e9a5097d95734f80f9205b4f6e37d69c317907cbc01a208f37fa9b40", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" diff --git a/chia/wallet/puzzles/vault_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp similarity index 100% rename from chia/wallet/puzzles/vault_recovery.clsp rename to chia/wallet/puzzles/vault_p2_recovery.clsp diff --git a/chia/wallet/puzzles/vault_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex similarity index 100% rename from chia/wallet/puzzles/vault_recovery.clsp.hex rename to chia/wallet/puzzles/vault_p2_recovery.clsp.hex diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index d098f70f97f3..833713cab2e6 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -18,8 +18,8 @@ P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_secp.clsp") P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() -RECOVERY_MOD: Program = load_clvm("vault_recovery.clsp") -RECOVERY_MOD_HASH = RECOVERY_MOD.get_tree_hash() +P2_RECOVERY_MOD: Program = load_clvm("vault_p2_recovery.clsp") +P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() ACS = Program.to(1) @@ -47,7 +47,7 @@ def test_recovery_puzzles() -> None: finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) finish_puzzlehash = finish_puzzle.get_tree_hash() - curried_recovery_puzzle = RECOVERY_MOD.curry( + curried_recovery_puzzle = P2_RECOVERY_MOD.curry( P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, escape_puzzlehash, bls_pk, timelock ) @@ -124,7 +124,7 @@ def test_vault_root_puzzle() -> None: timelock = 5000 amount = 10000 - recovery_puzzle = RECOVERY_MOD.curry( + recovery_puzzle = P2_RECOVERY_MOD.curry( P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock ) recovery_puzzlehash = recovery_puzzle.get_tree_hash() From 09cdfce07d523e885eb3ec31636a5055beb7ae74 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 20 Nov 2023 20:40:39 +1300 Subject: [PATCH 016/274] first lifecycle test --- chia/wallet/vault/__init__.py | 0 chia/wallet/vault/vault_drivers.py | 47 +++++++ tests/wallet/vault/test_vault_lifecycle.py | 145 +++++++++++++++++++++ 3 files changed, 192 insertions(+) create mode 100644 chia/wallet/vault/__init__.py create mode 100644 chia/wallet/vault/vault_drivers.py create mode 100644 tests/wallet/vault/test_vault_lifecycle.py diff --git a/chia/wallet/vault/__init__.py b/chia/wallet/vault/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py new file mode 100644 index 000000000000..9a0cca823a09 --- /dev/null +++ b/chia/wallet/vault/vault_drivers.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from chia_rs import G1Element + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.util.merkle_tree import MerkleTree + +# MODS +P2_CONDITIONS_MOD: Program = load_clvm("p2_conditions.clsp") +P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_secp.clsp") +P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") +P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() +P2_RECOVERY_MOD: Program = load_clvm("vault_p2_recovery.clsp") +P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() +RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") +RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() + + +# PUZZLES +def construct_p2_delegated_secp(secp_pk: bytes) -> Program: + return P2_DELEGATED_SECP_MOD.curry(secp_pk) + + +def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> Program: + return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) + + +def construct_p2_recovery_puzzle(secp_puzzlehash: bytes32, bls_pk: G1Element, timelock: uint64) -> Program: + return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock) + + +def construct_vault_puzzle(secp_puzzlehash: bytes32, recovery_puzzlehash: bytes32) -> Program: + return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzlehash, recovery_puzzlehash]).calculate_root()) + + +# MERKLE +def construct_vault_merkle_tree(secp_puzzlehash: bytes32, recovery_puzzlehash: bytes32) -> MerkleTree: + return MerkleTree([secp_puzzlehash, recovery_puzzlehash]) + + +def get_vault_proof(merkle_tree: MerkleTree, puzzlehash: bytes32) -> Program: + proof = merkle_tree.generate_proof(puzzlehash) + vault_proof: Program = Program.to((proof[0], proof[1][0])) + return vault_proof diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py new file mode 100644 index 000000000000..63bfce8c8e02 --- /dev/null +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from hashlib import sha256 +from typing import Optional, Tuple + +import pytest +from chia_rs import AugSchemeMPL, G2Element, PrivateKey +from ecdsa import NIST256p, SigningKey + +from chia.clvm.spend_sim import CostLogger, sim_and_client +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.coin_spend import CoinSpend +from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.spend_bundle import SpendBundle +from chia.util.errors import Err +from chia.util.ints import uint64 +from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions +from chia.wallet.vault.vault_drivers import ( + construct_p2_delegated_secp, + construct_p2_recovery_puzzle, + construct_recovery_finish, + construct_vault_merkle_tree, + construct_vault_puzzle, + get_vault_proof, +) +from tests.clvm.test_puzzles import secret_exponent_for_index + +SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) +SECP_PK = SECP_SK.verifying_key.to_string("compressed") + +BLS_SK = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) +BLS_PK = BLS_SK.get_g1() + +TIMELOCK = uint64(1000) +ACS = Program.to(0) +ACS_PH = ACS.get_tree_hash() + + +@pytest.mark.anyio +async def test_vault_inner(cost_logger: CostLogger) -> None: + async with sim_and_client() as (sim, client): + sim.pass_blocks(DEFAULT_CONSTANTS.SOFT_FORK3_HEIGHT) # Make sure secp_verify is available + + # Setup puzzles + secp_puzzle = construct_p2_delegated_secp(SECP_PK) + secp_puzzlehash = secp_puzzle.get_tree_hash() + p2_recovery_puzzle = construct_p2_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) + p2_recovery_puzzlehash = p2_recovery_puzzle.get_tree_hash() + vault_puzzle = construct_vault_puzzle(secp_puzzlehash, p2_recovery_puzzlehash) + vault_puzzlehash = vault_puzzle.get_tree_hash() + vault_merkle_tree = construct_vault_merkle_tree(secp_puzzlehash, p2_recovery_puzzlehash) + + await sim.farm_block(vault_puzzlehash) + + vault_coin: Coin = ( + await client.get_coin_records_by_puzzle_hashes([vault_puzzlehash], include_spent_coins=False) + )[0].coin + + # SECP SPEND + amount = 10000 + secp_conditions = Program.to([[51, ACS_PH, amount], [51, vault_puzzlehash, vault_coin.amount - amount]]) + secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) + secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) + secp_signature = SECP_SK.sign_deterministic(secp_delegated_puzzle.get_tree_hash()) + + secp_solution = Program.to([secp_delegated_puzzle, secp_delegated_solution, secp_signature]) + proof = get_vault_proof(vault_merkle_tree, secp_puzzlehash) + vault_solution_secp = Program.to([proof, secp_puzzle, secp_solution]) + vault_spendbundle = SpendBundle([CoinSpend(vault_coin, vault_puzzle, vault_solution_secp)], G2Element()) + + result: Tuple[MempoolInclusionStatus, Optional[Err]] = await client.push_tx(vault_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS + await sim.farm_block() + + # RECOVERY SPEND + vault_coin = (await client.get_coin_records_by_puzzle_hashes([vault_puzzlehash], include_spent_coins=False))[ + 0 + ].coin + + recovery_conditions = Program.to([[51, ACS_PH, vault_coin.amount]]) + recovery_solution = Program.to([vault_puzzlehash, vault_coin.amount, recovery_conditions]) + recovery_proof = get_vault_proof(vault_merkle_tree, p2_recovery_puzzlehash) + vault_solution_recovery = Program.to([recovery_proof, p2_recovery_puzzle, recovery_solution]) + vault_spendbundle = SpendBundle( + [CoinSpend(vault_coin, vault_puzzle, vault_solution_recovery)], + AugSchemeMPL.sign( + BLS_SK, + ( + recovery_conditions.get_tree_hash() + + vault_coin.name() + + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA + ), + ), + ) + + result = await client.push_tx(vault_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS + await sim.farm_block() + + recovery_finish_puzzle = construct_recovery_finish(TIMELOCK, recovery_conditions) + recovery_finish_puzzlehash = recovery_finish_puzzle.get_tree_hash() + recovery_puzzle = construct_vault_puzzle(secp_puzzlehash, recovery_finish_puzzlehash) + recovery_puzzlehash = recovery_puzzle.get_tree_hash() + recovery_merkle_tree = construct_vault_merkle_tree(secp_puzzlehash, recovery_finish_puzzlehash) + + recovery_coin: Coin = ( + await client.get_coin_records_by_puzzle_hashes([recovery_puzzlehash], include_spent_coins=False) + )[0].coin + + # Finish recovery + proof = get_vault_proof(recovery_merkle_tree, recovery_finish_puzzlehash) + recovery_finish_solution = Program.to([]) + recovery_solution = Program.to([proof, recovery_finish_puzzle, recovery_finish_solution]) + finish_spendbundle = SpendBundle([CoinSpend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + + result = await client.push_tx(finish_spendbundle) + assert result[1] == Err.ASSERT_SECONDS_RELATIVE_FAILED + + # Skip time + sim.pass_time(TIMELOCK) + await sim.farm_block() + + result = await client.push_tx(finish_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS + + # Escape recovery + # just farm a coin to the recovery puzhash + await sim.farm_block(recovery_puzzlehash) + recovery_coin = ( + await client.get_coin_records_by_puzzle_hashes([recovery_puzzlehash], include_spent_coins=False) + )[0].coin + + proof = get_vault_proof(recovery_merkle_tree, secp_puzzlehash) + secp_conditions = Program.to([[51, ACS_PH, recovery_coin.amount]]) + secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) + secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) + secp_signature = SECP_SK.sign_deterministic(secp_delegated_puzzle.get_tree_hash()) + secp_solution = Program.to([secp_delegated_puzzle, secp_delegated_solution, secp_signature]) + + recovery_solution = Program.to([proof, secp_puzzle, secp_solution]) + escape_spendbundle = SpendBundle([CoinSpend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + result = await client.push_tx(escape_spendbundle) + assert result[0] == MempoolInclusionStatus.SUCCESS From bf759afdf04b4c4d7b50edbe734e08d2e2e06eea Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 20 Nov 2023 08:00:03 -0800 Subject: [PATCH 017/274] Fix DL wallet transaction generation --- chia/data_layer/data_layer_wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index 71d953f880cd..5ac2ea0e8ea9 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -610,7 +610,7 @@ async def create_update_state_spend( ) assert chia_tx.spend_bundle is not None aggregate_bundle = SpendBundle.aggregate([dl_tx.spend_bundle, chia_tx.spend_bundle]) - dl_tx = dataclasses.replace(dl_tx, spend_bundle=aggregate_bundle) + dl_tx = dataclasses.replace(dl_tx, spend_bundle=aggregate_bundle, name=aggregate_bundle.name()) chia_tx = dataclasses.replace(chia_tx, spend_bundle=None) txs: List[TransactionRecord] = [dl_tx, chia_tx] else: From 540fccb76151f909a2fe8d4991c31e8a3e4f92c7 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 21 Nov 2023 18:10:06 +1300 Subject: [PATCH 018/274] add coin_id and genesis challenge to secp puz --- .../puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/p2_delegated_secp.clsp | 5 +-- .../wallet/puzzles/p2_delegated_secp.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 6 ++++ tests/wallet/vault/test_vault_clsp.py | 34 ++++++++++++++----- tests/wallet/vault/test_vault_lifecycle.py | 30 +++++++++++++--- 6 files changed, 63 insertions(+), 16 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 65322dd70940..147d69e28309 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -45,7 +45,7 @@ "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", - "p2_delegated_secp": "f02d79588cbfea39018c8e4e9defd6edf17eca7f47f8df8a433702c406e3c47a", + "p2_delegated_secp": "05a1a73d30933e9a3236bde0e00a04ded7f2277cb6645d539bb3c8bb2e3ce50f", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", "p2_parent": "b10ce2d0b18dcf8c21ddfaf55d9b9f0adcbf1e0beb55b1a8b9cad9bbff4e5f22", "p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363", diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp b/chia/wallet/puzzles/p2_delegated_secp.clsp index 3480e01ee650..1f4be685fffc 100644 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp @@ -1,14 +1,15 @@ ; p2_delegated with SECP256-R1 signature ; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) -(mod (SECP_PK delegated_puzzle delegated_solution signature) +(mod (SECP_PK delegated_puzzle delegated_solution signature coin_id genesis_challenge) (include *standard-cl-21*) (include condition_codes.clib) (include sha256tree.clib) - (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle)) signature) + (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id genesis_challenge) signature) ; secp256r1_verify returns 0 if successful. (x) ; this doesn't actually run because secp256_verify will raise on failure (a delegated_puzzle delegated_solution) ) + ) diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex index 0f9d7a333f20..7e8d0f2222c1 100644 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ffff841c3a8f00ff05ffff0bffff02ff02ffff04ff02ffff04ff0bff8080808080ff2f80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff02ff0bff1780ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 +ff02ffff01ff02ffff03ffff841c3a8f00ff05ffff0bffff02ff02ffff04ff02ffff04ff0bff80808080ff5fff8200bf80ff2f80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff02ff0bff1780ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 9a0cca823a09..9751fa10a6cd 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -2,6 +2,7 @@ from chia_rs import G1Element +from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint64 @@ -45,3 +46,8 @@ def get_vault_proof(merkle_tree: MerkleTree, puzzlehash: bytes32) -> Program: proof = merkle_tree.generate_proof(puzzlehash) vault_proof: Program = Program.to((proof[0], proof[1][0])) return vault_proof + + +# SECP SIGNATURE +def construct_secp_message(delegated_puzzlehash: bytes32, coin_id: bytes32) -> bytes: + return delegated_puzzlehash + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 833713cab2e6..7c16f09b5df1 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -8,6 +8,7 @@ from chia_rs import ENABLE_SECP_OPS from ecdsa import NIST256p, SigningKey +from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.program import INFINITE_COST, Program from chia.types.condition_opcodes import ConditionOpcode from chia.util.condition_tools import conditions_dict_for_solution @@ -37,9 +38,10 @@ def test_recovery_puzzles() -> None: secp_pk = secp_sk.verifying_key.to_string("compressed") p2_puzzlehash = ACS_PH - vault_puzzlehash = Program.to("vault_puzzlehash") + vault_puzzlehash = Program.to("vault_puzzlehash").get_tree_hash() amount = 10000 timelock = 5000 + coin_id = Program.to("coin_id").get_tree_hash() recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) escape_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) @@ -78,8 +80,12 @@ def test_recovery_puzzles() -> None: escape_proof = Program.to((proof[0], proof[1][0])) delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) - signed_delegated_puzzle = secp_sk.sign_deterministic(delegated_puzzle.get_tree_hash()) - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle]) + signed_delegated_puzzle = secp_sk.sign_deterministic( + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ) + secp_solution = Program.to( + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + ) escape_solution = Program.to([escape_proof, escape_puzzle, secp_solution]) escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH @@ -90,11 +96,16 @@ def test_p2_delegated_secp() -> None: secp_pk = secp_sk.verifying_key.to_string("compressed") secp_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + coin_id = Program.to("coin_id").get_tree_hash() delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, 1000]]) - signed_delegated_puzzle = secp_sk.sign_deterministic(delegated_puzzle.get_tree_hash()) + signed_delegated_puzzle = secp_sk.sign_deterministic( + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ) - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle]) + secp_solution = Program.to( + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + ) _, conds = run_with_secp(secp_puzzle, secp_solution) assert conds.at("frf").as_atom() == ACS_PH @@ -104,7 +115,9 @@ def test_p2_delegated_secp() -> None: sig_bytes[0] ^= (sig_bytes[0] + 1) % 256 bad_signature = bytes(sig_bytes) - bad_solution = Program.to([delegated_puzzle, delegated_solution, bad_signature]) + bad_solution = Program.to( + [delegated_puzzle, delegated_solution, bad_signature, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + ) with pytest.raises(ValueError, match="secp256r1_verify failed"): run_with_secp(secp_puzzle, bad_solution) @@ -123,6 +136,7 @@ def test_vault_root_puzzle() -> None: timelock = 5000 amount = 10000 + coin_id = Program.to("coin_id").get_tree_hash() recovery_puzzle = P2_RECOVERY_MOD.curry( P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock @@ -138,8 +152,12 @@ def test_vault_root_puzzle() -> None: # secp spend path delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) - signed_delegated_puzzle = secp_sk.sign_deterministic(delegated_puzzle.get_tree_hash()) - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle]) + signed_delegated_puzzle = secp_sk.sign_deterministic( + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ) + secp_solution = Program.to( + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + ) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) secp_proof = Program.to((proof[0], proof[1][0])) vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index 63bfce8c8e02..9ed48592050f 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -21,6 +21,7 @@ construct_p2_delegated_secp, construct_p2_recovery_puzzle, construct_recovery_finish, + construct_secp_message, construct_vault_merkle_tree, construct_vault_puzzle, get_vault_proof, @@ -63,9 +64,20 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_conditions = Program.to([[51, ACS_PH, amount], [51, vault_puzzlehash, vault_coin.amount - amount]]) secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) - secp_signature = SECP_SK.sign_deterministic(secp_delegated_puzzle.get_tree_hash()) + secp_signature = SECP_SK.sign_deterministic( + construct_secp_message(secp_delegated_puzzle.get_tree_hash(), vault_coin.name()) + ) + + secp_solution = Program.to( + [ + secp_delegated_puzzle, + secp_delegated_solution, + secp_signature, + vault_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + ] + ) - secp_solution = Program.to([secp_delegated_puzzle, secp_delegated_solution, secp_signature]) proof = get_vault_proof(vault_merkle_tree, secp_puzzlehash) vault_solution_secp = Program.to([proof, secp_puzzle, secp_solution]) vault_spendbundle = SpendBundle([CoinSpend(vault_coin, vault_puzzle, vault_solution_secp)], G2Element()) @@ -136,8 +148,18 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_conditions = Program.to([[51, ACS_PH, recovery_coin.amount]]) secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) - secp_signature = SECP_SK.sign_deterministic(secp_delegated_puzzle.get_tree_hash()) - secp_solution = Program.to([secp_delegated_puzzle, secp_delegated_solution, secp_signature]) + secp_signature = SECP_SK.sign_deterministic( + construct_secp_message(secp_delegated_puzzle.get_tree_hash(), recovery_coin.name()) + ) + secp_solution = Program.to( + [ + secp_delegated_puzzle, + secp_delegated_solution, + secp_signature, + recovery_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + ] + ) recovery_solution = Program.to([proof, secp_puzzle, secp_solution]) escape_spendbundle = SpendBundle([CoinSpend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) From 44ad966d7ad16dd18689066b1a0fb44214e5de38 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 21 Nov 2023 13:50:34 -0800 Subject: [PATCH 019/274] Signer Protocol --- chia/util/streamable.py | 5 +- chia/wallet/util/signer_protocol.py | 186 +++++++++++++++++++++++++++ setup.py | 1 + tests/wallet/test_signer_protocol.py | 116 +++++++++++++++++ 4 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 chia/wallet/util/signer_protocol.py create mode 100644 tests/wallet/test_signer_protocol.py diff --git a/chia/util/streamable.py b/chia/util/streamable.py index 4dab908a2556..2a8542164dd1 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -295,7 +295,10 @@ def recurse_jsonify(d: Any) -> Any: Makes bytes objects and unhashable types into strings with 0x, and makes large ints into strings. """ - if dataclasses.is_dataclass(d): + if hasattr(d, "override_json_serialization"): + overrid_ret: Union[List[Any], Dict[str, Any], str, None, int] = d.override_json_serialization(recurse_jsonify) + return overrid_ret + elif dataclasses.is_dataclass(d): new_dict = {} for field in dataclasses.fields(d): new_dict[field.name] = recurse_jsonify(getattr(d, field.name)) diff --git a/chia/wallet/util/signer_protocol.py b/chia/wallet/util/signer_protocol.py new file mode 100644 index 000000000000..9a7209ed688f --- /dev/null +++ b/chia/wallet/util/signer_protocol.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +from contextlib import contextmanager +from dataclasses import dataclass, fields +from io import BytesIO +from typing import Any, BinaryIO, Callable, Dict, Iterator, List, Type, TypeVar + +from hsms.clvm_serde import from_program_for_type, to_program_for_type +from typing_extensions import dataclass_transform + +from chia.types.blockchain_format.coin import Coin as _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.util.byte_types import hexstr_to_bytes +from chia.util.ints import uint64 +from chia.util.streamable import ConversionError, Streamable, streamable + +USE_CLVM_SERIALIZATION = False + + +@contextmanager +def clvm_serialization_mode(use: bool) -> Iterator[None]: + global USE_CLVM_SERIALIZATION + old_mode = USE_CLVM_SERIALIZATION + USE_CLVM_SERIALIZATION = use + yield + USE_CLVM_SERIALIZATION = old_mode + + +@dataclass_transform() +class ClvmStreamableMeta(type): + def __init__(cls: ClvmStreamableMeta, *args: Any) -> None: + if cls.__name__ == "ClvmStreamable": + return + # Not sure how to fix the hints here, but it works + dcls: Type[ClvmStreamable] = streamable(dataclass(frozen=True)(cls)) # type: ignore[arg-type] + # Iterate over the fields of the class + for field_obj in fields(dcls): + field_name = field_obj.name + field_metadata = {"key": field_name} + field_metadata.update(field_obj.metadata) + setattr(field_obj, "metadata", field_metadata) + setattr(dcls, "as_program", to_program_for_type(dcls)) + setattr(dcls, "from_program", lambda prog: from_program_for_type(dcls)(prog)) + super().__init__(*args) + + +_T_ClvmStreamable = TypeVar("_T_ClvmStreamable", bound="ClvmStreamable") + + +class ClvmStreamable(Streamable, metaclass=ClvmStreamableMeta): + def as_program(self) -> Program: + raise NotImplementedError() + + @classmethod + def from_program(cls: Type[_T_ClvmStreamable], prog: Program) -> _T_ClvmStreamable: + raise NotImplementedError() + + def stream(self, f: BinaryIO) -> None: + global USE_CLVM_SERIALIZATION + if USE_CLVM_SERIALIZATION: + f.write(bytes(self.as_program())) + else: + super().stream(f) + + @classmethod + def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: + assert isinstance(f, BytesIO) + try: + result = cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) + f.read() + return result + except Exception: + return super().parse(f) + + def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], Dict[str, Any]]) -> Any: + global USE_CLVM_SERIALIZATION + if USE_CLVM_SERIALIZATION: + return bytes(self).hex() + else: + new_dict = {} + for field in fields(self): + new_dict[field.name] = default_recurse_jsonify(getattr(self, field.name)) + return new_dict + + @classmethod + def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStreamable: + if isinstance(json_dict, str): + try: + byts = hexstr_to_bytes(json_dict) + except ValueError as e: + raise ConversionError(json_dict, cls, e) + + try: + return cls.from_program(Program.from_bytes(byts)) + except Exception as e: + raise ConversionError(json_dict, cls, e) + else: + return super().from_json_dict(json_dict) + + +class Coin(ClvmStreamable): + parent_coin_id: bytes32 + puzzle_hash: bytes32 + amount: uint64 + + +class Spend(ClvmStreamable): + coin: Coin + puzzle: Program + solution: Program + + @classmethod + def from_coin_spend(cls, coin_spend: CoinSpend) -> Spend: + return cls( + Coin( + coin_spend.coin.parent_coin_info, + coin_spend.coin.puzzle_hash, + uint64(coin_spend.coin.amount), + ), + coin_spend.puzzle_reveal.to_program(), + coin_spend.solution.to_program(), + ) + + def as_coin_spend(self) -> CoinSpend: + return CoinSpend( + _Coin( + self.coin.parent_coin_id, + self.coin.puzzle_hash, + self.coin.amount, + ), + SerializedProgram.from_program(self.puzzle), + SerializedProgram.from_program(self.solution), + ) + + +class TransactionInfo(ClvmStreamable): + spends: List[Spend] + + +class SigningTarget(ClvmStreamable): + pubkey: bytes + message: bytes + hook: bytes32 + + +class SumHint(ClvmStreamable): + fingerprints: List[bytes] + synthetic_offset: bytes + + +class PathHint(ClvmStreamable): + root_fingerprint: bytes + path: List[uint64] + + +class KeyHints(ClvmStreamable): + sum_hints: List[SumHint] + path_hints: List[PathHint] + + +class SigningInstructions(ClvmStreamable): + key_hints: KeyHints + targets: List[SigningTarget] + + +class UnsignedTransaction(ClvmStreamable): + transaction_info: TransactionInfo + signing_instructions: SigningInstructions + + +class SigningResponse(ClvmStreamable): + signature: bytes + hook: bytes32 + + +class Signature(ClvmStreamable): + type: str + signature: bytes + + +class SignedTransaction(ClvmStreamable): + transaction_info: TransactionInfo + signatures: List[Signature] diff --git a/setup.py b/setup.py index 9583edab2ea3..6874354a5789 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "zstd==1.5.5.1", "packaging==23.2", "psutil==5.9.4", + "quex-hsms==0.1.dev157", ] upnp_dependencies = [ diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py new file mode 100644 index 000000000000..ad7c5598c60f --- /dev/null +++ b/tests/wallet/test_signer_protocol.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import dataclasses + +from blspy import G1Element + +from chia.types.blockchain_format.coin import Coin as ConsensusCoin +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_spend import CoinSpend +from chia.util.ints import uint64 +from chia.util.streamable import Streamable, streamable +from chia.wallet.conditions import AggSigMe +from chia.wallet.util.signer_protocol import ( + KeyHints, + SigningInstructions, + SigningTarget, + Spend, + TransactionInfo, + UnsignedTransaction, + clvm_serialization_mode, +) + + +def test_signing_lifecycle() -> None: + pubkey: G1Element = G1Element() + message: bytes = b"message" + + coin: ConsensusCoin = ConsensusCoin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)) + puzzle: Program = Program.to(1) + solution: Program = Program.to([AggSigMe(pubkey, message).to_program()]) + + coin_spend: CoinSpend = CoinSpend(coin, puzzle, solution) + + tx: UnsignedTransaction = UnsignedTransaction( + TransactionInfo([Spend.from_coin_spend(coin_spend)]), + SigningInstructions( + KeyHints([], []), + [SigningTarget(bytes(pubkey), message, bytes32([1] * 32))], + ), + ) + + assert tx == UnsignedTransaction.from_program(Program.from_bytes(bytes(tx.as_program()))) + + as_json_dict = { + "coin": { + "parent_coin_id": "0x" + tx.transaction_info.spends[0].coin.parent_coin_id.hex(), + "puzzle_hash": "0x" + tx.transaction_info.spends[0].coin.puzzle_hash.hex(), + "amount": tx.transaction_info.spends[0].coin.amount, + }, + "puzzle": "0x" + bytes(tx.transaction_info.spends[0].puzzle).hex(), + "solution": "0x" + bytes(tx.transaction_info.spends[0].solution).hex(), + } + assert tx.transaction_info.spends[0].to_json_dict() == as_json_dict + + # Test from_json_dict with the special case where it encounters the as_program serialization in the middle of JSON + assert tx.transaction_info.spends[0] == Spend.from_json_dict( + { + "coin": bytes(tx.transaction_info.spends[0].coin.as_program()).hex(), + "puzzle": bytes(tx.transaction_info.spends[0].puzzle).hex(), + "solution": bytes(tx.transaction_info.spends[0].solution).hex(), + } + ) + + # Test the optional serialization as blobs + with clvm_serialization_mode(True): + assert ( + tx.transaction_info.spends[0].to_json_dict() + == bytes(tx.transaction_info.spends[0].as_program()).hex() # type: ignore[comparison-overlap] + ) + + # Make sure it's still a dict if using a Streamable object + @streamable + @dataclasses.dataclass(frozen=True) + class TempStreamable(Streamable): + streamable_key: Spend + + with clvm_serialization_mode(True): + assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == { + "streamable_key": bytes(tx.transaction_info.spends[0].as_program()).hex() + } + + with clvm_serialization_mode(False): + assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == {"streamable_key": as_json_dict} + + with clvm_serialization_mode(False): + assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == {"streamable_key": as_json_dict} + with clvm_serialization_mode(True): + assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == { + "streamable_key": bytes(tx.transaction_info.spends[0].as_program()).hex() + } + with clvm_serialization_mode(False): + assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == {"streamable_key": as_json_dict} + + streamable_blob = bytes(tx.transaction_info.spends[0]) + with clvm_serialization_mode(True): + clvm_streamable_blob = bytes(tx.transaction_info.spends[0]) + + assert streamable_blob != clvm_streamable_blob + Spend.from_bytes(streamable_blob) + Spend.from_bytes(clvm_streamable_blob) + assert Spend.from_bytes(streamable_blob) == Spend.from_bytes(clvm_streamable_blob) == tx.transaction_info.spends[0] + + with clvm_serialization_mode(False): + assert bytes(tx.transaction_info.spends[0]) == streamable_blob + + inside_streamable_blob = bytes(TempStreamable(tx.transaction_info.spends[0])) + with clvm_serialization_mode(True): + inside_clvm_streamable_blob = bytes(TempStreamable(tx.transaction_info.spends[0])) + + assert inside_streamable_blob != inside_clvm_streamable_blob + assert ( + TempStreamable.from_bytes(inside_streamable_blob) + == TempStreamable.from_bytes(inside_clvm_streamable_blob) + == TempStreamable(tx.transaction_info.spends[0]) + ) From 50c826ab3a30920003288473f4c664fa9d2b0f2c Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 16 Nov 2023 13:36:29 -0800 Subject: [PATCH 020/274] Wallet implementation of signer protocol --- chia/rpc/util.py | 115 +++++++++++++++++++- chia/rpc/wallet_request_types.py | 40 +++++++ chia/rpc/wallet_rpc_api.py | 42 +++++++- chia/rpc/wallet_rpc_client.py | 44 ++++++++ chia/util/initial-config.yaml | 2 + chia/wallet/trading/trade_store.py | 5 + chia/wallet/wallet.py | 143 ++++++++++++++++++++++++- chia/wallet/wallet_state_manager.py | 115 +++++++++++++++++++- tests/wallet/rpc/test_wallet_rpc.py | 41 +++++++- tests/wallet/test_signer_protocol.py | 150 ++++++++++++++++++++++++++- 10 files changed, 685 insertions(+), 12 deletions(-) create mode 100644 chia/rpc/wallet_request_types.py diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 867785e1dc22..b488e34f4b37 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -1,18 +1,26 @@ from __future__ import annotations +import dataclasses import logging import traceback from dataclasses import dataclass from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, TypeVar, get_type_hints import aiohttp +from chia_rs import AugSchemeMPL from typing_extensions import TypedDict, dataclass_transform from chia.types.blockchain_format.coin import Coin +from chia.types.coin_spend import CoinSpend +from chia.types.spend_bundle import SpendBundle from chia.util.json_util import obj_to_response from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_from_json_dicts, parse_timelock_info +from chia.wallet.trade_record import TradeRecord +from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.signer_protocol import clvm_serialization_mode +from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import TXConfig, TXConfigLoader log = logging.getLogger(__name__) @@ -48,7 +56,8 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s *args, **kwargs, ) - return response_obj.to_json_dict() + with clvm_serialization_mode(not request.get("full_jsonify", False)): + return response_obj.to_json_dict() return rpc_endpoint @@ -133,9 +142,111 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s tx_records: List[TransactionRecord] = [ TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"] ] + unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs(tx_records) + + if request.get("jsonify_unsigned_txs", False): + response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] + else: + response["unsigned_transactions"] = [bytes(tx.as_program()).hex() for tx in unsigned_txs] + + new_txs: List[TransactionRecord] = [] + if request.get("sign", self.service.config.get("auto_sign_txs", True)): + new_txs = await self.service.wallet_state_manager.sign_transactions( + tx_records, response.get("signing_responses", []), "signing_responses" in response + ) + response["transactions"] = [ + TransactionRecord.to_json_dict_convenience(tx, self.service.config) for tx in new_txs + ] + else: + new_txs = tx_records if request.get("push", push): - await self.service.wallet_state_manager.add_pending_transactions(tx_records, merge_spends=merge_spends) + await self.service.wallet_state_manager.add_pending_transactions(new_txs, merge_spends=merge_spends) + + # Some backwards compatibility code + if "transaction" in response: + if ( + func.__name__ == "create_new_wallet" + and request["wallet_type"] == "pool_wallet" + or func.__name__ == "pw_join_pool" + or func.__name__ == "pw_self_pool" + or func.__name__ == "pw_absorb_rewards" + ): + # Theses RPCs return not "convenience" for some reason + response["transaction"] = new_txs[0].to_json_dict() + else: + response["transaction"] = response["transactions"][0] + if "tx_record" in response: + response["tx_record"] = response["transactions"][0] + if "fee_transaction" in response and response["fee_transaction"] is not None: + # Theses RPCs return not "convenience" for some reason + response["fee_transaction"] = new_txs[1].to_json_dict() + if "transaction_id" in response: + response["transaction_id"] = new_txs[0].name + if "transaction_ids" in response: + response["transaction_ids"] = [ + tx.name.hex() for tx in new_txs if tx.type == TransactionType.OUTGOING_CLAWBACK.value + ] + if "spend_bundle" in response: + response["spend_bundle"] = SpendBundle.aggregate( + [tx.spend_bundle for tx in new_txs if tx.spend_bundle is not None] + ) + if "signed_txs" in response: + response["signed_txs"] = response["transactions"] + if "signed_tx" in response: + response["signed_tx"] = response["transactions"][0] + if "tx" in response: + if func.__name__ == "send_notification": + response["tx"] = response["transactions"][0] + else: + response["tx"] = new_txs[0].to_json_dict() + if "tx_id" in response: + response["tx_id"] = new_txs[0].name + if "trade_record" in response: + old_offer: Offer = Offer.from_bech32(response["offer"]) + signed_coin_spends: List[CoinSpend] = [ + coin_spend + for tx in new_txs + if tx.spend_bundle is not None + for coin_spend in tx.spend_bundle.coin_spends + ] + involved_coins: List[Coin] = [spend.coin for spend in signed_coin_spends] + signed_coin_spends.extend( + [spend for spend in old_offer._bundle.coin_spends if spend.coin not in involved_coins] + ) + new_offer_bundle: SpendBundle = SpendBundle( + signed_coin_spends, + AugSchemeMPL.aggregate( + [tx.spend_bundle.aggregated_signature for tx in new_txs if tx.spend_bundle is not None] + ), + ) + new_offer: Offer = Offer(old_offer.requested_payments, new_offer_bundle, old_offer.driver_dict) + response["offer"] = new_offer.to_bech32() + old_trade_record: TradeRecord = TradeRecord.from_json_dict_convenience( + response["trade_record"], bytes(old_offer).hex() + ) + new_trade: TradeRecord = dataclasses.replace( + old_trade_record, + offer=bytes(new_offer), + trade_id=new_offer.name(), + ) + response["trade_record"] = new_trade.to_json_dict_convenience() + if ( + await self.service.wallet_state_manager.trade_manager.trade_store.get_trade_record( + old_trade_record.trade_id + ) + is not None + ): + await self.service.wallet_state_manager.trade_manager.trade_store.delete_trade_record( + old_trade_record.trade_id + ) + await self.service.wallet_state_manager.trade_manager.save_trade(new_trade, new_offer) + for tx in await self.service.wallet_state_manager.tx_store.get_transactions_by_trade_id( + old_trade_record.trade_id + ): + await self.service.wallet_state_manager.tx_store.add_transaction_record( + dataclasses.replace(tx, trade_id=new_trade.trade_id) + ) return response diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py new file mode 100644 index 000000000000..d2c0b771d093 --- /dev/null +++ b/chia/rpc/wallet_request_types.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List + +from chia.rpc.util import RequestType +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.streamable import Streamable, streamable +from chia.wallet.util.signer_protocol import SignedTransaction, SigningInstructions, SigningResponse, Spend + + +class GatherSigningInfo(RequestType): + spends: List[Spend] + + +@streamable +@dataclass(frozen=True) +class GatherSigningInfoResponse(Streamable): + signing_instructions: SigningInstructions + + +class ApplySignatures(RequestType): + spends: List[Spend] + signing_responses: List[SigningResponse] + + +@streamable +@dataclass(frozen=True) +class ApplySignaturesResponse(Streamable): + signed_transactions: List[SignedTransaction] + + +class SubmitTransactions(RequestType): + signed_transactions: List[SignedTransaction] + + +@streamable +@dataclass(frozen=True) +class SubmitTransactionsResponse(Streamable): + mempool_ids: List[bytes32] diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index e4e5927db9a3..2023cfec2e6c 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -18,7 +18,15 @@ 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.rpc.util import tx_endpoint +from chia.rpc.util import marshall, tx_endpoint +from chia.rpc.wallet_request_types import ( + ApplySignatures, + ApplySignaturesResponse, + GatherSigningInfo, + GatherSigningInfoResponse, + SubmitTransactions, + SubmitTransactionsResponse, +) from chia.server.outbound_message import NodeType, make_msg from chia.server.ws_connection import WSChiaConnection from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -100,6 +108,7 @@ 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.query_filter import HashFilter, TransactionTypeFilter +from chia.wallet.util.signer_protocol import SigningResponse from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state @@ -281,6 +290,10 @@ def get_routes(self) -> Dict[str, Endpoint]: "/vc_revoke": self.vc_revoke, # CR-CATs "/crcat_approve_pending": self.crcat_approve_pending, + # Signer Protocol + "/gather_signing_info": self.gather_signing_info, + "/apply_signatures": self.apply_signatures, + "/submit_transactions": self.submit_transactions, } def get_connections(self, request_node_type: Optional[NodeType]) -> List[Dict[str, Any]]: @@ -1989,7 +2002,9 @@ async def take_offer( return { "trade_record": trade_record.to_json_dict_convenience(), + "offer": Offer.from_bytes(trade_record.offer).to_bech32(), "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in tx_records], + "signing_responses": [SigningResponse(bytes(offer._bundle.aggregated_signature), trade_record.trade_id)], } async def get_offer(self, request: Dict[str, Any]) -> EndpointResult: @@ -4496,3 +4511,28 @@ class CRCATApprovePending(Streamable): return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], } + + @marshall + async def gather_signing_info( + self, + request: GatherSigningInfo, + ) -> GatherSigningInfoResponse: + return GatherSigningInfoResponse(await self.service.wallet_state_manager.gather_signing_info(request["spends"])) + + @marshall + async def apply_signatures( + self, + request: ApplySignatures, + ) -> ApplySignaturesResponse: + return ApplySignaturesResponse( + [await self.service.wallet_state_manager.apply_signatures(request["spends"], request["signing_responses"])] + ) + + @marshall + async def submit_transactions( + self, + request: SubmitTransactions, + ) -> SubmitTransactionsResponse: + return SubmitTransactionsResponse( + await self.service.wallet_state_manager.submit_transactions(request["signed_transactions"]) + ) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 940468e5d3a0..790a8b83b3e8 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -2,9 +2,20 @@ from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Unpack + from chia.data_layer.data_layer_wallet import Mirror, SingletonRecord from chia.pools.pool_wallet_info import PoolWalletInfo from chia.rpc.rpc_client import RpcClient +from chia.rpc.util import get_streamable_from_request_type +from chia.rpc.wallet_request_types import ( + ApplySignatures, + ApplySignaturesResponse, + GatherSigningInfo, + GatherSigningInfoResponse, + SubmitTransactions, + SubmitTransactionsResponse, +) 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 @@ -1681,3 +1692,36 @@ async def crcat_approve_pending( }, ) return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + + async def gather_signing_info( + self, + **kwargs: Unpack[GatherSigningInfo], + ) -> GatherSigningInfoResponse: + return GatherSigningInfoResponse.from_json_dict( + await self.fetch( + "gather_signing_info", + get_streamable_from_request_type(GatherSigningInfo)(**kwargs).to_json_dict(), # type: ignore[misc] + ) + ) + + async def apply_signatures( + self, + **kwargs: Unpack[ApplySignatures], + ) -> ApplySignaturesResponse: + return ApplySignaturesResponse.from_json_dict( + await self.fetch( + "apply_signatures", + get_streamable_from_request_type(ApplySignatures)(**kwargs).to_json_dict(), # type: ignore[misc] + ) + ) + + async def submit_transactions( + self, + **kwargs: Unpack[SubmitTransactions], + ) -> SubmitTransactionsResponse: + return SubmitTransactionsResponse.from_json_dict( + await self.fetch( + "submit_transactions", + get_streamable_from_request_type(SubmitTransactions)(**kwargs).to_json_dict(), # type: ignore[misc] + ) + ) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index c1d89a957a94..ce828f5c6a00 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -616,6 +616,8 @@ wallet: min_amount: 0 batch_size: 50 + auto_sign_txs: True + data_layer: # TODO: consider name # TODO: organize consistently with other sections diff --git a/chia/wallet/trading/trade_store.py b/chia/wallet/trading/trade_store.py index 55ad9ba1bc61..7e2e20d79487 100644 --- a/chia/wallet/trading/trade_store.py +++ b/chia/wallet/trading/trade_store.py @@ -470,6 +470,11 @@ async def rollback_to_block(self, block_index: int) -> None: cursor = await conn.execute("DELETE FROM trade_records WHERE confirmed_at_index>?", (block_index,)) await cursor.close() + async def delete_trade_record(self, trade_id: bytes32) -> None: + async with self.db_wrapper.writer_maybe_transaction() as conn: + await (await conn.execute("DELETE FROM trade_records WHERE trade_id=?", (trade_id.hex(),))).close() + await (await conn.execute("DELETE FROM trade_record_times WHERE trade_id=?", (trade_id,))).close() + async def _get_new_trade_records_from_old(self, old_records: List[TradeRecordOld]) -> List[TradeRecord]: async with self.db_wrapper.reader_no_transaction() as conn: valid_times: Dict[bytes32, ConditionValidTimes] = {} diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index b50f44678aee..d9b47144dbf4 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -4,7 +4,7 @@ import time from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Set, Tuple, cast -from chia_rs import AugSchemeMPL, G1Element, G2Element +from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey from typing_extensions import Unpack from chia.types.blockchain_format.coin import Coin @@ -20,19 +20,38 @@ from chia.wallet.coin_selection import select_coins from chia.wallet.conditions import AssertCoinAnnouncement, Condition, CreateCoinAnnouncement, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.derive_keys import ( + MAX_POOL_WALLETS, + _derive_path, + _derive_path_unhardened, + master_sk_to_singleton_owner_sk, +) 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, + GROUP_ORDER, + calculate_synthetic_offset, calculate_synthetic_secret_key, puzzle_for_pk, puzzle_hash_for_pk, + puzzle_hash_for_synthetic_public_key, solution_for_conditions, ) from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition, 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.signer_protocol import ( + PathHint, + Signature, + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + SumHint, + TransactionInfo, +) 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 @@ -491,3 +510,125 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: if wallet_identifier is not None and wallet_identifier.id == self.id(): return True return False + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + pk_parsed: G1Element = G1Element.from_bytes(pk) + dr: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( + puzzle_hash_for_synthetic_public_key(pk_parsed) + ) + if dr is None: + return None + return SumHint( + [dr.pubkey.get_fingerprint().to_bytes(4, "big")], + calculate_synthetic_offset(dr.pubkey, DEFAULT_HIDDEN_PUZZLE_HASH).to_bytes(32, "big"), + ) + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + pk_parsed: G1Element = G1Element.from_bytes(pk) + index: Optional[uint32] = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pk_parsed) + if index is None: + index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash( + puzzle_hash_for_synthetic_public_key(pk_parsed) + ) + root_pubkey: bytes = self.wallet_state_manager.private_key.get_g1().get_fingerprint().to_bytes(4, "big") + if index is None: + # Pool wallet may have a secret key here + for pool_wallet_index in range(MAX_POOL_WALLETS): + try_owner_sk = master_sk_to_singleton_owner_sk( + self.wallet_state_manager.private_key, uint32(pool_wallet_index) + ) + if try_owner_sk.get_g1() == pk_parsed: + return PathHint( + root_pubkey, + [uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)], + ) + return None + return PathHint( + root_pubkey, + [uint64(12381), uint64(8444), uint64(2), uint64(index)], + ) + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> List[SigningResponse]: + root_pubkey: G1Element = self.wallet_state_manager.private_key.get_g1() + pk_lookup: Dict[int, G1Element] = {root_pubkey.get_fingerprint(): root_pubkey} + sk_lookup: Dict[int, PrivateKey] = {root_pubkey.get_fingerprint(): self.wallet_state_manager.private_key} + + for path_hint in signing_instructions.key_hints.path_hints: + if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint(): + if not partial_allowed: + raise ValueError(f"No root pubkey for fingerprint {root_pubkey.get_fingerprint()}") + else: + continue + else: + path = [int(step) for step in path_hint.path] + derive_child_sk = _derive_path(self.wallet_state_manager.private_key, path) + derive_child_sk_unhardened = _derive_path_unhardened(self.wallet_state_manager.private_key, path) + derive_child_pk = derive_child_sk.get_g1() + derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1() + pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk + pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened + sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk + sk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_sk_unhardened + + for sum_hint in signing_instructions.key_hints.sum_hints: + final_sk: PrivateKey = PrivateKey.from_bytes(bytes(32)) + for fingerprint in sum_hint.fingerprints: + fingerprint_as_int = int.from_bytes(fingerprint, "big") + if fingerprint_as_int not in pk_lookup: + if not partial_allowed: + raise ValueError( + "No pubkey found (or path hinted to) for " + f"fingerprint {int.from_bytes(fingerprint, 'big')}" + ) + else: + break + else: + final_sk = PrivateKey.aggregate([final_sk, sk_lookup[fingerprint_as_int]]) + else: # Only do this if we don't break + secret_exponent = int.from_bytes(bytes(final_sk), "big") + synthetic_secret_exponent = ( + secret_exponent + int.from_bytes(sum_hint.synthetic_offset, "big") + ) % GROUP_ORDER + blob = synthetic_secret_exponent.to_bytes(32, "big") + synthetic_sk = PrivateKey.from_bytes(blob) + synthetic_pk = synthetic_sk.get_g1() + pk_lookup[synthetic_pk.get_fingerprint()] = synthetic_pk + sk_lookup[synthetic_pk.get_fingerprint()] = synthetic_sk + + responses: List[SigningResponse] = [] + for target in signing_instructions.targets: + pk_fingerprint: int = G1Element.from_bytes(target.pubkey).get_fingerprint() + if pk_fingerprint not in sk_lookup: + if not partial_allowed: + raise ValueError(f"Pubkey {pk_fingerprint} not found (or path/sum hinted to)") + else: + continue + responses.append( + SigningResponse( + bytes(AugSchemeMPL.sign(sk_lookup[pk_fingerprint], target.message)), + target.hook, + ) + ) + + return responses + + async def apply_signatures( + self, spends: List[Spend], signing_responses: List[SigningResponse] + ) -> SignedTransaction: + return SignedTransaction( + TransactionInfo(spends), + [ + Signature( + "bls_12381_aug_scheme", + bytes( + AugSchemeMPL.aggregate( + [G2Element.from_bytes(signing_response.signature) for signing_response in signing_responses] + ) + ), + ) + ], + ) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 3193178aeffc..fee15cf22451 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -25,7 +25,7 @@ ) import aiosqlite -from chia_rs import G1Element, G2Element, PrivateKey +from chia_rs import AugSchemeMPL, 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 @@ -51,6 +51,7 @@ 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.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.db_synchronous import db_synchronous_on from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err @@ -120,6 +121,18 @@ 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.signer_protocol import ( + KeyHints, + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + SigningTarget, + Spend, + SumHint, + TransactionInfo, + UnsignedTransaction, +) 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 ( @@ -2533,3 +2546,103 @@ async def sign_transaction(self, coin_spends: List[CoinSpend]) -> SpendBundle: self.constants.MAX_BLOCK_COST_CLVM, [puzzle_hash_for_synthetic_public_key], ) + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + return await self.main_wallet.sum_hint_for_pubkey(pk) + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + return await self.main_wallet.path_hint_for_pubkey(pk) + + async def key_hints_for_pubkeys(self, pks: List[bytes]) -> KeyHints: + return KeyHints( + [sum_hint for pk in pks for sum_hint in (await self.sum_hint_for_pubkey(pk),) if sum_hint is not None], + [path_hint for pk in pks for path_hint in (await self.path_hint_for_pubkey(pk),) if path_hint is not None], + ) + + async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: + pks: List[bytes] = [] + signing_targets: List[SigningTarget] = [] + for coin_spend in coin_spends: + _coin_spend = coin_spend.as_coin_spend() + # Get AGG_SIG conditions + conditions_dict = conditions_dict_for_solution( + _coin_spend.puzzle_reveal, _coin_spend.solution, self.constants.MAX_BLOCK_COST_CLVM + ) + # Create signature + for pk_bytes, msg in pkm_pairs_for_conditions_dict( + conditions_dict, _coin_spend.coin, self.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + pks.append(pk_bytes) + signing_targets.append(SigningTarget(pk_bytes, msg, std_hash(pk_bytes + msg))) + + return SigningInstructions( + await self.key_hints_for_pubkeys(pks), + signing_targets, + ) + + async def gather_signing_info_for_bundles(self, bundles: List[SpendBundle]) -> List[UnsignedTransaction]: + utxs: List[UnsignedTransaction] = [] + for bundle in bundles: + signer_protocol_spends: List[Spend] = [Spend.from_coin_spend(spend) for spend in bundle.coin_spends] + utxs.append( + UnsignedTransaction( + TransactionInfo(signer_protocol_spends), await self.gather_signing_info(signer_protocol_spends) + ) + ) + + return utxs + + async def gather_signing_info_for_txs(self, txs: List[TransactionRecord]) -> List[UnsignedTransaction]: + return await self.gather_signing_info_for_bundles( + [tx.spend_bundle for tx in txs if tx.spend_bundle is not None] + ) + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> List[SigningResponse]: + return await self.main_wallet.execute_signing_instructions(signing_instructions, partial_allowed) + + async def apply_signatures( + self, spends: List[Spend], signing_responses: List[SigningResponse] + ) -> SignedTransaction: + return await self.main_wallet.apply_signatures(spends, signing_responses) + + def signed_tx_to_spendbundle(self, signed_tx: SignedTransaction) -> SpendBundle: + if len([_ for _ in signed_tx.signatures if _.type != "bls_12381_aug_scheme"]) > 0: + raise ValueError("Unable to handle signatures that are not bls_12381_aug_scheme") + return SpendBundle( + [spend.as_coin_spend() for spend in signed_tx.transaction_info.spends], + AugSchemeMPL.aggregate([G2Element.from_bytes(sig.signature) for sig in signed_tx.signatures]), + ) + + async def sign_transactions( + self, + tx_records: List[TransactionRecord], + additional_signing_responses: List[SigningResponse], + partial_allowed: bool = False, + ) -> Tuple[List[TransactionRecord], List[SigningResponse]]: + unsigned_txs: List[UnsignedTransaction] = await self.gather_signing_info_for_txs(tx_records) + new_txs: List[TransactionRecord] = [] + all_signing_responses: List[SigningResponse] = [] + for unsigned_tx, tx in zip( + unsigned_txs, [tx_record for tx_record in tx_records if tx_record.spend_bundle is not None] + ): + signing_responses: List[SigningResponse] = await self.execute_signing_instructions( + unsigned_tx.signing_instructions, partial_allowed=partial_allowed + ) + all_signing_responses.extend(signing_responses) + new_bundle = self.signed_tx_to_spendbundle( + await self.apply_signatures( + unsigned_tx.transaction_info.spends, + [*additional_signing_responses, *signing_responses], + ) + ) + new_txs.append(dataclasses.replace(tx, spend_bundle=new_bundle, name=new_bundle.name())) + new_txs.extend([tx_record for tx_record in tx_records if tx_record.spend_bundle is None]) + return new_txs, all_signing_responses + + async def submit_transactions(self, signed_txs: List[SignedTransaction]) -> List[bytes32]: + bundles: List[SpendBundle] = [self.signed_tx_to_spendbundle(tx) for tx in signed_txs] + for bundle in bundles: + await self.wallet_node.push_tx(bundle) + return [bundle.name() for bundle in bundles] diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 43c70f406d9e..fa92f8336230 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -39,7 +39,13 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle from chia.wallet.cat_wallet.cat_wallet import CATWallet -from chia.wallet.conditions import ConditionValidTimes, CreateCoinAnnouncement, CreatePuzzleAnnouncement, Remark +from chia.wallet.conditions import ( + ConditionValidTimes, + CreateCoinAnnouncement, + CreatePuzzleAnnouncement, + Remark, + conditions_to_json_dicts, +) from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.nft_wallet.nft_wallet import NFTWallet @@ -50,6 +56,7 @@ from chia.wallet.util.address_type import AddressType from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import AmountFilter, HashFilter, TransactionTypeFilter +from chia.wallet.util.signer_protocol import UnsignedTransaction from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import CoinType, WalletType @@ -301,19 +308,43 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment await client.send_transaction(1, uint64(100000000000000001), addr, DEFAULT_TX_CONFIG) # Tests sending a basic transaction - tx = await client.send_transaction( + extra_conditions = (Remark(Program.to(("test", None))),) + non_existent_coin = Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)) + tx_no_push = await client.send_transaction( 1, tx_amount, addr, memos=["this is a basic tx"], tx_config=DEFAULT_TX_CONFIG.override( excluded_coin_amounts=[uint64(250000000000)], - excluded_coin_ids=[bytes32([0] * 32)], + excluded_coin_ids=[non_existent_coin.name()], + reuse_puzhash=True, ), - extra_conditions=(Remark(Program.to(("test", None))),), + extra_conditions=extra_conditions, + push=False, ) + response = await client.fetch( + "send_transaction", + { + "wallet_id": 1, + "amount": tx_amount, + "address": addr, + "fee": 0, + "memos": ["this is a basic tx"], + "puzzle_decorator": None, + "extra_conditions": conditions_to_json_dicts(extra_conditions), + "exclude_coin_amounts": [250000000000], + "exclude_coins": [non_existent_coin.to_json_dict()], + "reuse_puzhash": True, + "jsonify_unsigned_txs": True, + "push": True, + }, + ) + assert response["success"] + tx = TransactionRecord.from_json_dict_convenience(response["transactions"][0]) + [UnsignedTransaction.from_json_dict(utx) for utx in response["unsigned_transactions"]] + assert tx == dataclasses.replace(tx_no_push, created_at_time=tx.created_at_time) transaction_id = tx.name - spend_bundle = tx.spend_bundle assert spend_bundle is not None diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index ad7c5598c60f..3e1bc654d057 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -1,9 +1,12 @@ from __future__ import annotations import dataclasses +from typing import List, Optional -from blspy import G1Element +import pytest +from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey +from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.types.blockchain_format.coin import Coin as ConsensusCoin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 @@ -11,18 +14,29 @@ from chia.util.ints import uint64 from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import AggSigMe +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + DEFAULT_HIDDEN_PUZZLE_HASH, + calculate_synthetic_offset, +) from chia.wallet.util.signer_protocol import ( KeyHints, + PathHint, SigningInstructions, + SigningResponse, SigningTarget, Spend, + SumHint, TransactionInfo, UnsignedTransaction, clvm_serialization_mode, ) +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_state_manager import WalletStateManager +from tests.wallet.conftest import WalletTestFramework -def test_signing_lifecycle() -> None: +def test_signing_serialization() -> None: pubkey: G1Element = G1Element() message: bytes = b"message" @@ -114,3 +128,135 @@ class TempStreamable(Streamable): == TempStreamable.from_bytes(inside_clvm_streamable_blob) == TempStreamable(tx.transaction_info.spends[0]) ) + + +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + "trusted": True, + "reuse_puzhash": True, + } + ], + indirect=True, +) +@pytest.mark.anyio +async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFramework) -> None: + wallet: Wallet = wallet_environments.environments[0].xch_wallet + wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager + wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client + + # Test first that we can properly examine and sign a regular transaction + puzzle: Program = await wallet.get_puzzle(new=False) + puzzle_hash: bytes32 = puzzle.get_tree_hash() + delegated_puzzle: Program = Program.to(None) + delegated_puzzle_hash: bytes32 = delegated_puzzle.get_tree_hash() + solution: Program = Program.to([None, None, None]) + + coin: ConsensusCoin = ConsensusCoin( + bytes32([0] * 32), + puzzle_hash, + uint64(0), + ) + coin_spend: CoinSpend = CoinSpend( + coin, + puzzle, + solution, + ) + + derivation_record: Optional[ + DerivationRecord + ] = await wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(puzzle_hash) + assert derivation_record is not None + pubkey: G1Element = derivation_record.pubkey + synthetic_pubkey: G1Element = G1Element.from_bytes(puzzle.uncurry()[1].at("f").atom) + message: bytes = delegated_puzzle_hash + coin.name() + wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + + utx: UnsignedTransaction = UnsignedTransaction( + TransactionInfo([Spend.from_coin_spend(coin_spend)]), + (await wallet_rpc.gather_signing_info(spends=[Spend.from_coin_spend(coin_spend)])).signing_instructions, + ) + assert utx.signing_instructions.key_hints.sum_hints == [ + SumHint( + [pubkey.get_fingerprint().to_bytes(4, "big")], + calculate_synthetic_offset(pubkey, DEFAULT_HIDDEN_PUZZLE_HASH).to_bytes(32, "big"), + ) + ] + assert utx.signing_instructions.key_hints.path_hints == [ + PathHint( + wallet_state_manager.private_key.get_g1().get_fingerprint().to_bytes(4, "big"), + [uint64(12381), uint64(8444), uint64(2), uint64(derivation_record.index)], + ) + ] + assert len(utx.signing_instructions.targets) == 1 + assert utx.signing_instructions.targets[0].pubkey == bytes(synthetic_pubkey) + assert utx.signing_instructions.targets[0].message == message + + signing_responses: List[SigningResponse] = await wallet_state_manager.execute_signing_instructions( + utx.signing_instructions + ) + assert len(signing_responses) == 1 + assert signing_responses[0].hook == utx.signing_instructions.targets[0].hook + assert AugSchemeMPL.verify(synthetic_pubkey, message, G2Element.from_bytes(signing_responses[0].signature)) + + # Now test that we can partially sign a transaction + ACS: Program = Program.to(1) + ACS_PH: Program = Program.to(1).get_tree_hash() + not_our_private_key: PrivateKey = PrivateKey.from_bytes(bytes(32)) + not_our_pubkey: G1Element = not_our_private_key.get_g1() + not_our_message: bytes = b"not our message" + not_our_coin: ConsensusCoin = ConsensusCoin( + bytes32([0] * 32), + ACS_PH, + uint64(0), + ) + not_our_coin_spend: CoinSpend = CoinSpend(not_our_coin, ACS, Program.to([[49, not_our_pubkey, not_our_message]])) + + not_our_utx: UnsignedTransaction = UnsignedTransaction( + TransactionInfo([Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)]), + ( + await wallet_rpc.gather_signing_info( + spends=[Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)] + ) + ).signing_instructions, + ) + assert not_our_utx.signing_instructions.key_hints == utx.signing_instructions.key_hints + assert len(not_our_utx.signing_instructions.targets) == 2 + assert not_our_utx.signing_instructions.targets[0].pubkey == bytes(synthetic_pubkey) + assert not_our_utx.signing_instructions.targets[0].message == bytes(message) + assert not_our_utx.signing_instructions.targets[1].pubkey == bytes(not_our_pubkey) + assert not_our_utx.signing_instructions.targets[1].message == bytes(not_our_message) + not_our_signing_instructions: SigningInstructions = not_our_utx.signing_instructions + with pytest.raises(ValueError, match=r"not found \(or path/sum hinted to\)"): + await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) + with pytest.raises(ValueError, match=r"No pubkey found \(or path hinted to\) for fingerprint"): + not_our_signing_instructions = dataclasses.replace( + not_our_signing_instructions, + key_hints=dataclasses.replace( + not_our_signing_instructions.key_hints, + sum_hints=[ + *not_our_signing_instructions.key_hints.sum_hints, + SumHint([bytes(not_our_pubkey)], b""), + ], + ), + ) + await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) + with pytest.raises(ValueError, match="No root pubkey for fingerprint"): + not_our_signing_instructions = dataclasses.replace( + not_our_signing_instructions, + key_hints=dataclasses.replace( + not_our_signing_instructions.key_hints, + path_hints=[ + *not_our_signing_instructions.key_hints.path_hints, + PathHint(bytes(not_our_pubkey), [uint64(0)]), + ], + ), + ) + await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) + signing_responses_2 = await wallet_state_manager.execute_signing_instructions( + not_our_signing_instructions, partial_allowed=True + ) + assert len(signing_responses_2) == 1 + assert signing_responses_2 == signing_responses From d8e5620dc54727726e1f2c8ddbc1d068a7354c19 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sun, 26 Nov 2023 09:04:22 +1300 Subject: [PATCH 021/274] secp assert coin_id and curry genesis challenge --- .../wallet/puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/p2_delegated_secp.clsp | 9 ++++++--- chia/wallet/puzzles/p2_delegated_secp.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 9 ++++----- tests/wallet/vault/test_vault_clsp.py | 18 +++++++----------- tests/wallet/vault/test_vault_lifecycle.py | 11 +++++++---- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 147d69e28309..416a3d5a44c4 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -45,7 +45,7 @@ "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", - "p2_delegated_secp": "05a1a73d30933e9a3236bde0e00a04ded7f2277cb6645d539bb3c8bb2e3ce50f", + "p2_delegated_secp": "282533d811a01d8cc2fcf3e7d93142c376c62b569ac6bdee4fea65d172d716e7", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", "p2_parent": "b10ce2d0b18dcf8c21ddfaf55d9b9f0adcbf1e0beb55b1a8b9cad9bbff4e5f22", "p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363", diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp b/chia/wallet/puzzles/p2_delegated_secp.clsp index 1f4be685fffc..b0ae2f8512b9 100644 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp @@ -1,15 +1,18 @@ ; p2_delegated with SECP256-R1 signature ; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) -(mod (SECP_PK delegated_puzzle delegated_solution signature coin_id genesis_challenge) +(mod (GENESIS_CHALLENGE SECP_PK delegated_puzzle delegated_solution signature coin_id) (include *standard-cl-21*) (include condition_codes.clib) (include sha256tree.clib) - (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id genesis_challenge) signature) + (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE) signature) ; secp256r1_verify returns 0 if successful. (x) ; this doesn't actually run because secp256_verify will raise on failure - (a delegated_puzzle delegated_solution) + (c + (list ASSERT_MY_COIN_ID coin_id) + (a delegated_puzzle delegated_solution) + ) ) ) diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex index 7e8d0f2222c1..03da697cdf99 100644 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ffff841c3a8f00ff05ffff0bffff02ff02ffff04ff02ffff04ff0bff80808080ff5fff8200bf80ff2f80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff02ff0bff1780ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 +ff02ffff01ff02ffff03ffff841c3a8f00ff0bffff0bffff02ff02ffff04ff02ffff04ff17ff80808080ff8200bfff0580ff5f80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ff8200bfffff01808080ffff02ff17ff2f8080ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 9751fa10a6cd..64885825fe86 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -2,7 +2,6 @@ from chia_rs import G1Element -from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint64 @@ -21,8 +20,8 @@ # PUZZLES -def construct_p2_delegated_secp(secp_pk: bytes) -> Program: - return P2_DELEGATED_SECP_MOD.curry(secp_pk) +def construct_p2_delegated_secp(secp_pk: bytes, genesis_challenge: bytes32) -> Program: + return P2_DELEGATED_SECP_MOD.curry(genesis_challenge, secp_pk) def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> Program: @@ -49,5 +48,5 @@ def get_vault_proof(merkle_tree: MerkleTree, puzzlehash: bytes32) -> Program: # SECP SIGNATURE -def construct_secp_message(delegated_puzzlehash: bytes32, coin_id: bytes32) -> bytes: - return delegated_puzzlehash + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE +def construct_secp_message(delegated_puzzlehash: bytes32, coin_id: bytes32, genesis_challenge: bytes32) -> bytes: + return delegated_puzzlehash + coin_id + genesis_challenge diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 7c16f09b5df1..0ae3b95f9a0f 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -44,7 +44,7 @@ def test_recovery_puzzles() -> None: coin_id = Program.to("coin_id").get_tree_hash() recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) - escape_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) escape_puzzlehash = escape_puzzle.get_tree_hash() finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) finish_puzzlehash = finish_puzzle.get_tree_hash() @@ -94,7 +94,7 @@ def test_recovery_puzzles() -> None: def test_p2_delegated_secp() -> None: secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") - secp_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) coin_id = Program.to("coin_id").get_tree_hash() delegated_puzzle = ACS @@ -103,12 +103,10 @@ def test_p2_delegated_secp() -> None: delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE ) - secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] - ) + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) _, conds = run_with_secp(secp_puzzle, secp_solution) - assert conds.at("frf").as_atom() == ACS_PH + assert conds.at("rfrf").as_atom() == ACS_PH # test that a bad secp sig fails sig_bytes = bytearray(signed_delegated_puzzle) @@ -127,7 +125,7 @@ def test_vault_root_puzzle() -> None: # secp puzzle secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") - secp_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) secp_puzzlehash = secp_puzzle.get_tree_hash() # recovery keys @@ -155,9 +153,7 @@ def test_vault_root_puzzle() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE ) - secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] - ) + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) secp_proof = Program.to((proof[0], proof[1][0])) vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) @@ -166,7 +162,7 @@ def test_vault_root_puzzle() -> None: # recovery spend path recovery_conditions = Program.to([[51, ACS_PH, amount]]) - curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry(secp_pk) + curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) recovery_merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) recovery_merkle_root = recovery_merkle_tree.calculate_root() diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index 9ed48592050f..c8256fcddc53 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -45,7 +45,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: sim.pass_blocks(DEFAULT_CONSTANTS.SOFT_FORK3_HEIGHT) # Make sure secp_verify is available # Setup puzzles - secp_puzzle = construct_p2_delegated_secp(SECP_PK) + secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE) secp_puzzlehash = secp_puzzle.get_tree_hash() p2_recovery_puzzle = construct_p2_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) p2_recovery_puzzlehash = p2_recovery_puzzle.get_tree_hash() @@ -65,7 +65,9 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) secp_signature = SECP_SK.sign_deterministic( - construct_secp_message(secp_delegated_puzzle.get_tree_hash(), vault_coin.name()) + construct_secp_message( + secp_delegated_puzzle.get_tree_hash(), vault_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ) ) secp_solution = Program.to( @@ -74,7 +76,6 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_solution, secp_signature, vault_coin.name(), - DEFAULT_CONSTANTS.GENESIS_CHALLENGE, ] ) @@ -149,7 +150,9 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) secp_signature = SECP_SK.sign_deterministic( - construct_secp_message(secp_delegated_puzzle.get_tree_hash(), recovery_coin.name()) + construct_secp_message( + secp_delegated_puzzle.get_tree_hash(), recovery_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ) ) secp_solution = Program.to( [ From 993619d0681c0f2ac022ceb9c3c0d81c8182bb0e Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Nov 2023 12:52:57 -0800 Subject: [PATCH 022/274] Remove blspy dependency --- tests/wallet/test_signer_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index ad7c5598c60f..ee46d84d986e 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -2,7 +2,7 @@ import dataclasses -from blspy import G1Element +from chia_rs import G1Element from chia.types.blockchain_format.coin import Coin as ConsensusCoin from chia.types.blockchain_format.program import Program From 17d776ca5cb81843e0494c92e712d5d169ebdefd Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Nov 2023 13:01:44 -0800 Subject: [PATCH 023/274] Fix PrivateKey aggregate and bad sign_transaction call --- chia/rpc/util.py | 2 +- chia/wallet/wallet.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index b488e34f4b37..e13d27198423 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -151,7 +151,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s new_txs: List[TransactionRecord] = [] if request.get("sign", self.service.config.get("auto_sign_txs", True)): - new_txs = await self.service.wallet_state_manager.sign_transactions( + new_txs, _ = await self.service.wallet_state_manager.sign_transactions( tx_records, response.get("signing_responses", []), "signing_responses" in response ) response["transactions"] = [ diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index d9b47144dbf4..cb978684af69 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -575,7 +575,7 @@ async def execute_signing_instructions( sk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_sk_unhardened for sum_hint in signing_instructions.key_hints.sum_hints: - final_sk: PrivateKey = PrivateKey.from_bytes(bytes(32)) + final_sk_int: int = 0 for fingerprint in sum_hint.fingerprints: fingerprint_as_int = int.from_bytes(fingerprint, "big") if fingerprint_as_int not in pk_lookup: @@ -587,11 +587,10 @@ async def execute_signing_instructions( else: break else: - final_sk = PrivateKey.aggregate([final_sk, sk_lookup[fingerprint_as_int]]) + final_sk_int += int.from_bytes(bytes(sk_lookup[fingerprint_as_int]), "big") else: # Only do this if we don't break - secret_exponent = int.from_bytes(bytes(final_sk), "big") synthetic_secret_exponent = ( - secret_exponent + int.from_bytes(sum_hint.synthetic_offset, "big") + final_sk_int + int.from_bytes(sum_hint.synthetic_offset, "big") ) % GROUP_ORDER blob = synthetic_secret_exponent.to_bytes(32, "big") synthetic_sk = PrivateKey.from_bytes(blob) From 4a832a8981e53abfdfd47bfc8c57813af5843ae2 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Nov 2023 13:25:31 -0800 Subject: [PATCH 024/274] test coverage --- chia/wallet/util/signer_protocol.py | 4 ++-- tests/wallet/test_signer_protocol.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/chia/wallet/util/signer_protocol.py b/chia/wallet/util/signer_protocol.py index 9a7209ed688f..58e2c048c420 100644 --- a/chia/wallet/util/signer_protocol.py +++ b/chia/wallet/util/signer_protocol.py @@ -52,11 +52,11 @@ def __init__(cls: ClvmStreamableMeta, *args: Any) -> None: class ClvmStreamable(Streamable, metaclass=ClvmStreamableMeta): def as_program(self) -> Program: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover @classmethod def from_program(cls: Type[_T_ClvmStreamable], prog: Program) -> _T_ClvmStreamable: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def stream(self, f: BinaryIO) -> None: global USE_CLVM_SERIALIZATION diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index ee46d84d986e..00a2ecf1fb84 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -2,6 +2,7 @@ import dataclasses +import pytest from chia_rs import G1Element from chia.types.blockchain_format.coin import Coin as ConsensusCoin @@ -9,7 +10,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.util.ints import uint64 -from chia.util.streamable import Streamable, streamable +from chia.util.streamable import ConversionError, Streamable, streamable from chia.wallet.conditions import AggSigMe from chia.wallet.util.signer_protocol import ( KeyHints, @@ -31,6 +32,7 @@ def test_signing_lifecycle() -> None: solution: Program = Program.to([AggSigMe(pubkey, message).to_program()]) coin_spend: CoinSpend = CoinSpend(coin, puzzle, solution) + assert Spend.from_coin_spend(coin_spend).as_coin_spend() == coin_spend tx: UnsignedTransaction = UnsignedTransaction( TransactionInfo([Spend.from_coin_spend(coin_spend)]), @@ -114,3 +116,10 @@ class TempStreamable(Streamable): == TempStreamable.from_bytes(inside_clvm_streamable_blob) == TempStreamable(tx.transaction_info.spends[0]) ) + + # Test some json loading errors + + with pytest.raises(ConversionError): + Spend.from_json_dict("blah") + with pytest.raises(ConversionError): + UnsignedTransaction.from_json_dict(streamable_blob.hex()) From 1aa60b1f693f3a9f8c8b7749f9585af4ebf501f0 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 16 Nov 2023 13:38:07 -0800 Subject: [PATCH 025/274] Remove signing from wallet --- chia/data_layer/data_layer_wallet.py | 26 +--- chia/pools/pool_wallet.py | 50 ++----- chia/rpc/util.py | 9 +- chia/rpc/wallet_rpc_api.py | 7 +- chia/simulator/full_node_simulator.py | 2 +- chia/wallet/cat_wallet/cat_wallet.py | 41 +---- chia/wallet/cat_wallet/dao_cat_wallet.py | 10 +- chia/wallet/did_wallet/did_wallet.py | 67 ++------- chia/wallet/nft_wallet/nft_wallet.py | 54 +------ chia/wallet/puzzles/tails.py | 6 +- chia/wallet/vc_wallet/cr_cat_wallet.py | 19 +-- chia/wallet/vc_wallet/vc_wallet.py | 4 +- chia/wallet/wallet.py | 7 +- chia/wallet/wallet_state_manager.py | 79 ++++++++-- tests/core/data_layer/test_data_rpc.py | 54 +++---- tests/core/full_node/test_full_node.py | 14 +- tests/core/full_node/test_transactions.py | 4 +- tests/core/mempool/test_mempool_manager.py | 5 + tests/simulation/test_simulation.py | 7 +- tests/simulation/test_simulator.py | 4 +- tests/wallet/cat_wallet/test_cat_wallet.py | 41 +++-- tests/wallet/cat_wallet/test_trades.py | 89 ++++++++--- tests/wallet/dao_wallet/test_dao_wallets.py | 141 +++++++++--------- tests/wallet/db_wallet/test_dl_offers.py | 38 +++-- tests/wallet/db_wallet/test_dl_wallet.py | 42 ++++-- tests/wallet/did_wallet/test_did.py | 35 +++-- tests/wallet/nft_wallet/test_nft_1_offers.py | 93 ++++++++---- tests/wallet/nft_wallet/test_nft_bulk_mint.py | 20 ++- tests/wallet/nft_wallet/test_nft_offers.py | 70 ++++++--- tests/wallet/nft_wallet/test_nft_wallet.py | 22 +-- tests/wallet/rpc/test_wallet_rpc.py | 2 +- .../simple_sync/test_simple_sync_protocol.py | 10 +- tests/wallet/sync/test_wallet_sync.py | 19 ++- tests/wallet/test_notifications.py | 2 +- tests/wallet/test_sign_coin_spends.py | 40 ++++- tests/wallet/test_wallet.py | 55 ++++--- tests/wallet/test_wallet_retry.py | 2 +- tests/wallet/vc_wallet/test_vc_wallet.py | 6 +- 38 files changed, 633 insertions(+), 563 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index 7114606656f9..1d7955fdf87b 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -408,7 +408,6 @@ async def create_update_state_spend( new_puz_hash: Optional[bytes32] = None, new_amount: Optional[uint64] = None, fee: uint64 = uint64(0), - sign: bool = True, add_pending_singleton: bool = True, announce_new_state: bool = False, extra_conditions: Tuple[Condition, ...] = tuple(), @@ -576,10 +575,7 @@ async def create_update_state_spend( SerializedProgram.from_program(full_sol), ) - if sign: - spend_bundle = await self.sign(coin_spend) - else: - spend_bundle = SpendBundle([coin_spend], G2Element()) + spend_bundle = SpendBundle([coin_spend], G2Element()) if announce_new_state: spend_bundle = dataclasses.replace(spend_bundle, coin_spends=[coin_spend, second_coin_spend]) @@ -640,9 +636,6 @@ async def generate_signed_transaction( ) -> List[TransactionRecord]: launcher_id: Optional[bytes32] = kwargs.get("launcher_id", None) new_root_hash: Optional[bytes32] = kwargs.get("new_root_hash", None) - sign: bool = kwargs.get( - "sign", True - ) # This only prevent signing of THIS wallet's part of the tx (fee will still be signed) add_pending_singleton: bool = kwargs.get("add_pending_singleton", True) announce_new_state: bool = kwargs.get("announce_new_state", False) # Figure out the launcher ID @@ -669,7 +662,6 @@ async def generate_signed_transaction( puzzle_hashes[0], amounts[0], fee, - sign, add_pending_singleton, announce_new_state, extra_conditions, @@ -790,7 +782,7 @@ async def delete_mirror( ] ), ) - mirror_bundle: SpendBundle = await self.sign(mirror_spend) + mirror_bundle: SpendBundle = SpendBundle([mirror_spend], G2Element()) txs = [ TransactionRecord( confirmed_at_height=uint32(0), @@ -1109,9 +1101,6 @@ async def get_pending_change_balance(self) -> uint64: async def get_max_send_amount(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128: return uint128(0) - async def sign(self, coin_spend: CoinSpend) -> SpendBundle: - return await self.wallet_state_manager.sign_transaction([coin_spend]) - def get_name(self) -> str: return self.wallet_info.name @@ -1183,7 +1172,6 @@ async def make_update_offer( fee=fee_left_to_pay, launcher_id=launcher, new_root_hash=new_root, - sign=False, add_pending_singleton=False, announce_new_state=True, extra_conditions=extra_conditions, @@ -1209,18 +1197,16 @@ async def make_update_offer( dl_spend, solution=SerializedProgram.from_program(new_solution), ) - signed_bundle = await dl_wallet.sign(new_spend) new_bundle: SpendBundle = dataclasses.replace( txs[0].spend_bundle, - coin_spends=all_other_spends, + coin_spends=[*all_other_spends, new_spend], ) - agg_bundle: SpendBundle = SpendBundle.aggregate([signed_bundle, new_bundle]) - all_bundles.append(agg_bundle) + all_bundles.append(new_bundle) all_transactions.append( dataclasses.replace( txs[0], - spend_bundle=agg_bundle, - name=agg_bundle.name(), + spend_bundle=new_bundle, + name=new_bundle.name(), ) ) all_transactions.extend(txs[1:]) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 1e853de58a56..200654a1edd0 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -46,8 +46,6 @@ from chia.util.ints import uint32, uint64, uint128 from chia.wallet.conditions import AssertCoinAnnouncement, Condition, ConditionValidTimes, parse_timelock_info from chia.wallet.derive_keys import find_owner_sk -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key -from chia.wallet.sign_coin_spends import sign_coin_spends from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, TXConfig @@ -479,36 +477,6 @@ async def _get_owner_key_cache(self) -> Tuple[PrivateKey, uint32]: async def get_pool_wallet_index(self) -> uint32: return (await self._get_owner_key_cache())[1] - async def sign(self, coin_spend: CoinSpend) -> SpendBundle: - async def pk_to_sk(pk: G1Element) -> PrivateKey: - s = find_owner_sk([self.wallet_state_manager.private_key], pk) - if s is None: # pragma: no cover - # No pool wallet transactions _should_ hit this, but it can't hurt to have a backstop - private_key = await self.wallet_state_manager.get_private_key_for_pubkey(pk) - if private_key is None: - raise ValueError(f"No private key for pubkey: {pk}") - return private_key - else: - # Note that pool_wallet_index may be from another wallet than self.wallet_id - owner_sk, pool_wallet_index = s - if owner_sk is None: # pragma: no cover - # TODO: this code is dead, per hinting at least - # No pool wallet transactions _should_ hit this, but it can't hurt to have a backstop - private_key = await self.wallet_state_manager.get_private_key_for_pubkey(pk) - if private_key is None: - raise ValueError(f"No private key for pubkey: {pk}") - return private_key - return owner_sk - - return await sign_coin_spends( - [coin_spend], - pk_to_sk, - self.wallet_state_manager.get_synthetic_private_key_for_puzzle_hash, - self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA, - self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) - async def generate_fee_transaction( self, fee: uint64, @@ -601,15 +569,15 @@ async def generate_travel_transactions( else: raise RuntimeError("Invalid state") - signed_spend_bundle = await self.sign(outgoing_coin_spend) - assert signed_spend_bundle.removals()[0].puzzle_hash == singleton.puzzle_hash - assert signed_spend_bundle.removals()[0].name() == singleton.name() - assert signed_spend_bundle is not None + unsigned_spend_bundle = SpendBundle([outgoing_coin_spend], G2Element()) + assert unsigned_spend_bundle.removals()[0].puzzle_hash == singleton.puzzle_hash + assert unsigned_spend_bundle.removals()[0].name() == singleton.name() + assert unsigned_spend_bundle is not None fee_tx: Optional[TransactionRecord] = None if fee > 0: fee_tx = await self.generate_fee_transaction(fee, tx_config) assert fee_tx.spend_bundle is not None - signed_spend_bundle = SpendBundle.aggregate([signed_spend_bundle, fee_tx.spend_bundle]) + unsigned_spend_bundle = SpendBundle.aggregate([unsigned_spend_bundle, fee_tx.spend_bundle]) fee_tx = dataclasses.replace(fee_tx, spend_bundle=None) tx_record = TransactionRecord( @@ -620,15 +588,15 @@ async def generate_travel_transactions( fee_amount=fee, confirmed=False, sent=uint32(0), - spend_bundle=signed_spend_bundle, - additions=signed_spend_bundle.additions(), - removals=signed_spend_bundle.removals(), + spend_bundle=unsigned_spend_bundle, + additions=unsigned_spend_bundle.additions(), + removals=unsigned_spend_bundle.removals(), wallet_id=self.id(), sent_to=[], trade_id=None, memos=[], type=uint32(TransactionType.OUTGOING_TX.value), - name=signed_spend_bundle.name(), + name=unsigned_spend_bundle.name(), valid_times=ConditionValidTimes(), ) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index e13d27198423..ca304077b063 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -151,17 +151,20 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s new_txs: List[TransactionRecord] = [] if request.get("sign", self.service.config.get("auto_sign_txs", True)): - new_txs, _ = await self.service.wallet_state_manager.sign_transactions( + new_txs, signing_responses = await self.service.wallet_state_manager.sign_transactions( tx_records, response.get("signing_responses", []), "signing_responses" in response ) response["transactions"] = [ TransactionRecord.to_json_dict_convenience(tx, self.service.config) for tx in new_txs ] + response["signing_responses"] = [bytes(r.as_program()).hex() for r in signing_responses] else: - new_txs = tx_records + new_txs = tx_records # pragma: no cover if request.get("push", push): - await self.service.wallet_state_manager.add_pending_transactions(new_txs, merge_spends=merge_spends) + await self.service.wallet_state_manager.add_pending_transactions( + new_txs, merge_spends=merge_spends, sign=False + ) # Some backwards compatibility code if "transaction" in response: diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 2023cfec2e6c..56796379ef89 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -612,7 +612,7 @@ async def push_transactions(self, request: Dict[str, Any]) -> EndpointResult: txs.append(tx) async with self.service.wallet_state_manager.lock: - await self.service.wallet_state_manager.add_pending_transactions(txs) + await self.service.wallet_state_manager.add_pending_transactions(txs, sign=request.get("sign", False)) return {} @@ -924,7 +924,6 @@ async def create_new_wallet( delayed_address, extra_conditions=extra_conditions, ) - await self.service.wallet_state_manager.add_pending_transactions([tr]) except Exception as e: raise ValueError(str(e)) return { @@ -3692,6 +3691,10 @@ async def nft_mint_bulk( for cs in sb.coin_spends: if cs.coin.puzzle_hash == nft_puzzles.LAUNCHER_PUZZLE_HASH: nft_id_list.append(encode_puzzle_hash(cs.coin.name(), AddressType.NFT.hrp(self.service.config))) + ### + # Temporary signing workaround (delete when minting functions return transaction records) + sb, _ = await self.service.wallet_state_manager.sign_bundle(sb.coin_spends) + ### return { "success": True, "spend_bundle": sb, diff --git a/chia/simulator/full_node_simulator.py b/chia/simulator/full_node_simulator.py index e6d7cb24a7b7..f55ed59c7873 100644 --- a/chia/simulator/full_node_simulator.py +++ b/chia/simulator/full_node_simulator.py @@ -648,7 +648,7 @@ async def create_coins_with_amounts( tx_config=DEFAULT_TX_CONFIG, primaries=outputs_group[1:], ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) transaction_records.append(tx) else: break diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index facc9ac0a552..8fd9c1cf8c60 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -6,7 +6,7 @@ import traceback from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Set, Tuple, cast -from chia_rs import AugSchemeMPL, G1Element, G2Element +from chia_rs import G1Element, G2Element from typing_extensions import Unpack from chia.consensus.default_constants import DEFAULT_CONSTANTS @@ -18,7 +18,6 @@ from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle from chia.util.byte_types import hexstr_to_bytes -from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.errors import Err, ValidationError from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 @@ -46,10 +45,6 @@ from chia.wallet.outer_puzzles import AssetType from chia.wallet.payment import Payment from chia.wallet.puzzle_drivers import PuzzleInfo -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( - DEFAULT_HIDDEN_PUZZLE_HASH, - calculate_synthetic_secret_key, -) from chia.wallet.puzzles.tails import ALL_LIMITATIONS_PROGRAMS from chia.wallet.transaction_record import TransactionRecord from chia.wallet.uncurried_puzzle import uncurry_puzzle @@ -200,8 +195,8 @@ async def create_new_cat_wallet( valid_times=ConditionValidTimes(), ) chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle, name=spend_bundle.name()) - await self.wallet_state_manager.add_pending_transactions([chia_tx, cat_record]) - return self, [chia_tx, cat_record] + tx_list = await self.wallet_state_manager.add_pending_transactions([chia_tx, cat_record]) + return self, tx_list @staticmethod async def get_or_create_wallet_for_cat( @@ -536,33 +531,6 @@ async def select_coins( assert sum(c.amount for c in coins) >= amount return coins - async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: - sigs: List[G2Element] = [] - for spend in spend_bundle.coin_spends: - args = match_cat_puzzle(uncurry_puzzle(spend.puzzle_reveal.to_program())) - if args is not None: - _, _, inner_puzzle = args - puzzle_hash = inner_puzzle.get_tree_hash() - private = await self.wallet_state_manager.get_private_key(puzzle_hash) - synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) - conditions = conditions_dict_for_solution( - spend.puzzle_reveal.to_program(), - spend.solution.to_program(), - self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - ) - synthetic_pk = synthetic_secret_key.get_g1() - for pk, msg in pkm_pairs_for_conditions_dict( - conditions, spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - try: - assert bytes(synthetic_pk) == pk - sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg)) - except AssertionError: - raise ValueError("This spend bundle cannot be signed by the CAT wallet") - - agg_sig = AugSchemeMPL.aggregate(sigs) - return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) - async def inner_puzzle_for_cat_puzhash(self, cat_hash: bytes32) -> Program: record: Optional[ DerivationRecord @@ -808,7 +776,7 @@ async def generate_signed_transaction( payments.append(Payment(puzhash, amount, memos_with_hint)) payment_sum = sum([p.amount for p in payments]) - unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( + spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( payments, tx_config, fee, @@ -816,7 +784,6 @@ async def generate_signed_transaction( coins=coins, extra_conditions=extra_conditions, ) - spend_bundle = await self.sign(unsigned_spend_bundle) # TODO add support for array in stored records tx_list = [ TransactionRecord( diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index 9c6cf88d48d9..790fe81a0da4 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -358,9 +358,7 @@ async def create_vote_spend( spendable_cat_list.append(new_spendable_cat) cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list) - spend_bundle = await self.wallet_state_manager.sign_transaction(cat_spend_bundle.coin_spends) - assert isinstance(spend_bundle, SpendBundle) - return spend_bundle + return cat_spend_bundle async def enter_dao_cat_voting_mode( self, @@ -452,8 +450,7 @@ async def exit_vote_state( spendable_cat_list.append(new_spendable_cat) spent_coins.append(coin) - cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list) - spend_bundle: SpendBundle = await self.wallet_state_manager.sign_transaction(cat_spend_bundle.coin_spends) + spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list) if fee > 0: # pragma: no cover chia_tx = await self.standard_wallet.create_tandem_xch_tx( @@ -558,8 +555,7 @@ async def remove_active_proposal( ) spendable_cat_list.append(new_spendable_cat) - cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list) - spend_bundle = await self.wallet_state_manager.sign_transaction(cat_spend_bundle.coin_spends) + spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list) if fee > 0: # pragma: no cover chia_tx = await self.standard_wallet.create_tandem_xch_tx(fee, tx_config=tx_config) diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index b755718a0061..87e67031628c 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -17,7 +17,6 @@ from chia.types.coin_spend import CoinSpend from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.types.spend_bundle import SpendBundle -from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.ints import uint16, uint32, uint64, uint128 from chia.wallet.conditions import ( AssertCoinAnnouncement, @@ -27,7 +26,6 @@ parse_timelock_info, ) from chia.wallet.derivation_record import DerivationRecord -from chia.wallet.derive_keys import master_sk_to_wallet_sk_unhardened from chia.wallet.did_wallet import did_wallet_puzzles from chia.wallet.did_wallet.did_info import DIDCoinData, DIDInfo from chia.wallet.did_wallet.did_wallet_puzzles import match_did_puzzle, uncurry_innerpuz @@ -607,8 +605,7 @@ async def create_update_spend( ) new_coin = Coin(coin.name(), new_full_puzzle.get_tree_hash(), coin.amount) list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol), CoinSpend(new_coin, new_full_puzzle, new_full_sol)] - unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - spend_bundle = await self.sign(unsigned_spend_bundle) + spend_bundle = SpendBundle(list_of_coinspends, G2Element()) if fee > 0: coin_name = coin.name() chia_tx = await self.standard_wallet.create_tandem_xch_tx( @@ -707,8 +704,7 @@ async def transfer_did( ] ) list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] - unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - spend_bundle = await self.sign(unsigned_spend_bundle) + spend_bundle = SpendBundle(list_of_coinspends, G2Element()) if fee > 0: coin_name = coin.name() chia_tx = await self.standard_wallet.create_tandem_xch_tx( @@ -797,7 +793,6 @@ async def create_message_spend( ) list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - signed_spend_bundle: SpendBundle = await self.sign(unsigned_spend_bundle) return TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -806,15 +801,15 @@ async def create_message_spend( fee_amount=uint64(0), confirmed=False, sent=uint32(0), - spend_bundle=signed_spend_bundle, - additions=signed_spend_bundle.additions(), + spend_bundle=unsigned_spend_bundle, + additions=unsigned_spend_bundle.additions(), removals=[coin], wallet_id=self.id(), sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=signed_spend_bundle.name(), - memos=list(compute_memos(signed_spend_bundle).items()), + name=unsigned_spend_bundle.name(), + memos=list(compute_memos(unsigned_spend_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ) @@ -848,8 +843,7 @@ async def create_exit_spend(self, puzhash: bytes32, tx_config: TXConfig) -> List ] ) list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] - unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - spend_bundle = await self.sign(unsigned_spend_bundle) + spend_bundle = SpendBundle(list_of_coinspends, G2Element()) did_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -930,8 +924,7 @@ async def create_attestment( list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] message_spend = did_wallet_puzzles.create_spend_for_message(coin.name(), recovering_coin_name, newpuz, pubkey) message_spend_bundle = SpendBundle([message_spend], AugSchemeMPL.aggregate([])) - unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - spend_bundle = await self.sign(unsigned_spend_bundle) + spend_bundle = SpendBundle(list_of_coinspends, G2Element()) did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -1045,20 +1038,7 @@ async def recovery_spend( ) list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] - index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - if index is None: - raise ValueError("Unknown pubkey.") - private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) - message = bytes(puzhash) - sigs = [AugSchemeMPL.sign(private, message)] - for _ in spend_bundle.coin_spends: - sigs.append(AugSchemeMPL.sign(private, message)) - aggsig = AugSchemeMPL.aggregate(sigs) - # assert AugSchemeMPL.verify(pubkey, message, aggsig) - if spend_bundle is None: - spend_bundle = SpendBundle(list_of_coinspends, aggsig) - else: - spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_coinspends, aggsig)]) + spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_coinspends, G2Element())]) did_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -1202,33 +1182,6 @@ async def sign_message(self, message: str, mode: SigningMode) -> Tuple[G1Element else: raise ValueError("Invalid inner DID puzzle.") - async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: - sigs: List[G2Element] = [] - for spend in spend_bundle.coin_spends: - puzzle_args = did_wallet_puzzles.match_did_puzzle(*spend.puzzle_reveal.to_program().uncurry()) - if puzzle_args is not None: - p2_puzzle, _, _, _, _ = puzzle_args - puzzle_hash = p2_puzzle.get_tree_hash() - private = await self.wallet_state_manager.get_private_key(puzzle_hash) - synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) - conditions = conditions_dict_for_solution( - spend.puzzle_reveal.to_program(), - spend.solution.to_program(), - self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - ) - synthetic_pk = synthetic_secret_key.get_g1() - for pk, msg in pkm_pairs_for_conditions_dict( - conditions, spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - try: - assert bytes(synthetic_pk) == pk - sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg)) - except AssertionError: - raise ValueError("This spend bundle cannot be signed by the DID wallet") - - agg_sig = AugSchemeMPL.aggregate(sigs) - return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) - async def generate_new_decentralised_id( self, amount: uint64, tx_config: TXConfig, fee: uint64 = uint64(0) ) -> List[TransactionRecord]: @@ -1349,7 +1302,7 @@ async def generate_eve_spend( ) list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - return await self.sign(unsigned_spend_bundle) + return unsigned_spend_bundle async def get_frozen_amount(self) -> uint64: return await self.wallet_state_manager.get_frozen_balance(self.wallet_info.id) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index d5fa9afa2e70..1a3ed43dd455 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -20,7 +20,6 @@ from chia.types.coin_spend import CoinSpend, compute_additions from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.types.spend_bundle import SpendBundle -from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash from chia.util.ints import uint16, uint32, uint64, uint128 from chia.wallet.conditions import ( @@ -443,44 +442,6 @@ async def generate_new_nft( txs.append(dataclasses.replace(tx_record, spend_bundle=None)) return txs - async def sign(self, spend_bundle: SpendBundle, puzzle_hashes: Optional[List[bytes32]] = None) -> SpendBundle: - if puzzle_hashes is None: - puzzle_hashes = [] - sigs: List[G2Element] = [] - for spend in spend_bundle.coin_spends: - pks = {} - if not puzzle_hashes: - uncurried_nft = UncurriedNFT.uncurry(*spend.puzzle_reveal.to_program().uncurry()) - if uncurried_nft is not None: - self.log.debug("Found a NFT state layer to sign") - puzzle_hashes.append(uncurried_nft.p2_puzzle.get_tree_hash()) - for ph in puzzle_hashes: - private = await self.wallet_state_manager.get_private_key(ph) - pks[bytes(private.get_g1())] = private - synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) - synthetic_pk = synthetic_secret_key.get_g1() - pks[bytes(synthetic_pk)] = synthetic_secret_key - conditions = conditions_dict_for_solution( - spend.puzzle_reveal.to_program(), - spend.solution.to_program(), - self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - ) - for pk, msg in pkm_pairs_for_conditions_dict( - conditions, spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - try: - sk = pks.get(pk) - if sk: - self.log.debug("Found key, signing for pk: %s", pk) - sigs.append(AugSchemeMPL.sign(sk, msg)) - else: - self.log.warning("Couldn't find key for: %s", pk) - except AssertionError: - raise ValueError("This spend bundle cannot be signed by the NFT wallet") - - agg_sig = AugSchemeMPL.aggregate(sigs) - return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) - async def update_metadata( self, nft_coin_info: NFTCoinInfo, @@ -651,8 +612,7 @@ async def generate_signed_transaction( metadata_update=metadata_update, extra_conditions=extra_conditions, ) - spend_bundle = await self.sign(unsigned_spend_bundle) - spend_bundle = SpendBundle.aggregate([spend_bundle] + additional_bundles) + spend_bundle = SpendBundle.aggregate([unsigned_spend_bundle] + additional_bundles) if chia_tx is not None and chia_tx.spend_bundle is not None: spend_bundle = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle]) chia_tx = dataclasses.replace(chia_tx, spend_bundle=None) @@ -1464,7 +1424,7 @@ async def mint_from_did( primaries=[], conditions=(AssertCoinAnnouncement(primary_announcement_hash),) ) xch_spends.append(CoinSpend(xch_coin, puzzle, solution)) - xch_spend = await self.wallet_state_manager.sign_transaction(xch_spends) + xch_spend = SpendBundle(xch_spends, G2Element()) # Create the DID spend using the announcements collected when making the intermediate launcher coins did_p2_solution = self.standard_wallet.make_solution( @@ -1508,10 +1468,9 @@ async def mint_from_did( # Collect up all the coin spends and sign them list_of_coinspends = [did_spend] + intermediate_coin_spends + launcher_spends unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - signed_spend_bundle = await did_wallet.sign(unsigned_spend_bundle) # Aggregate everything into a single spend bundle - total_spend = SpendBundle.aggregate([signed_spend_bundle, xch_spend, *eve_spends]) + total_spend = SpendBundle.aggregate([unsigned_spend_bundle, xch_spend, *eve_spends]) tx_record: TransactionRecord = dataclasses.replace(eve_txs[0], spend_bundle=total_spend) return [tx_record] @@ -1702,15 +1661,14 @@ async def mint_from_xch( else: solution = self.standard_wallet.make_solution(primaries=[], conditions=(primary_announcement,)) xch_spends.append(CoinSpend(xch_coin, puzzle, solution)) - xch_spend = await self.wallet_state_manager.sign_transaction(xch_spends) + xch_spend = SpendBundle(xch_spends, G2Element()) - # Collect up all the coin spends and sign them + # Collect up all the coin spends list_of_coinspends = intermediate_coin_spends + launcher_spends unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) - signed_spend_bundle = await self.sign(unsigned_spend_bundle) # Aggregate everything into a single spend bundle - total_spend = SpendBundle.aggregate([signed_spend_bundle, xch_spend, *eve_spends]) + total_spend = SpendBundle.aggregate([unsigned_spend_bundle, xch_spend, *eve_spends]) tx_record: TransactionRecord = dataclasses.replace(eve_txs[0], spend_bundle=total_spend) return [tx_record] diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py index e9cf83ea5941..c10e40f1cd8f 100644 --- a/chia/wallet/puzzles/tails.py +++ b/chia/wallet/puzzles/tails.py @@ -124,12 +124,11 @@ async def generate_issuance_bundle( ) ], ) - signed_eve_spend = await wallet.sign(eve_spend) if wallet.cat_info.my_tail is None: await wallet.save_info(CATInfo(tail.get_tree_hash(), tail)) - return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend]) + return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, eve_spend]) class GenesisByPuzhash(LimitationsProgram): @@ -287,12 +286,11 @@ async def generate_issuance_bundle( ) ], ) - signed_eve_spend = await wallet.sign(eve_spend) if wallet.cat_info.my_tail is None: await wallet.save_info(CATInfo(tail.get_tree_hash(), tail)) - return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend]) + return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, eve_spend]) # This should probably be much more elegant than just a dictionary with strings as identifiers diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index 00b7779d741a..38696b21dc3c 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -311,9 +311,6 @@ def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: # pragma: no cover async def get_new_cat_puzzle_hash(self) -> bytes32: # pragma: no cover raise NotImplementedError("get_new_cat_puzzle_hash is a legacy method and is not available on CR-CAT wallets") - async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: # pragma: no cover - raise NotImplementedError("get_new_cat_puzzle_hash is a legacy method and is not available on CR-CAT wallets") - async def inner_puzzle_for_cat_puzhash(self, cat_hash: bytes32) -> Program: # pragma: no cover raise NotImplementedError( "inner_puzzle_for_cat_puzhash is a legacy method and is not available on CR-CAT wallets" @@ -648,7 +645,7 @@ async def generate_signed_transaction( ) ) - unsigned_spend_bundle, other_txs = await self._generate_unsigned_spendbundle( + spend_bundle, other_txs = await self._generate_unsigned_spendbundle( payments, tx_config, fee, @@ -658,10 +655,6 @@ async def generate_signed_transaction( add_authorizations_to_cr_cats=add_authorizations_to_cr_cats, ) - signed_spend_bundle: SpendBundle = await self.wallet_state_manager.sign_transaction( - unsigned_spend_bundle.coin_spends - ) - other_tx_removals: Set[Coin] = {removal for tx in other_txs for removal in tx.removals} other_tx_additions: Set[Coin] = {removal for tx in other_txs for removal in tx.additions} tx_list = [ @@ -673,15 +666,15 @@ async def generate_signed_transaction( fee_amount=fee, confirmed=False, sent=uint32(0), - spend_bundle=signed_spend_bundle if i == 0 else None, - additions=list(set(signed_spend_bundle.additions()) - other_tx_additions) if i == 0 else [], - removals=list(set(signed_spend_bundle.removals()) - other_tx_removals) if i == 0 else [], + spend_bundle=spend_bundle if i == 0 else None, + additions=list(set(spend_bundle.additions()) - other_tx_additions) if i == 0 else [], + removals=list(set(spend_bundle.removals()) - other_tx_removals) if i == 0 else [], wallet_id=self.id(), sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=signed_spend_bundle.name(), - memos=list(compute_memos(signed_spend_bundle).items()), + name=spend_bundle.name(), + memos=list(compute_memos(spend_bundle).items()), valid_times=parse_timelock_info(extra_conditions), ) for i, payment in enumerate(payments) diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index 646c8e84d127..0a7644443b79 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -200,7 +200,7 @@ async def launch_new_vc( solution = solution_for_conditions(dpuz.rest()) original_puzzle = await self.standard_wallet.puzzle_for_puzzle_hash(original_coin.puzzle_hash) coin_spends.append(CoinSpend(original_coin, original_puzzle, solution)) - spend_bundle = await self.wallet_state_manager.sign_transaction(coin_spends) + spend_bundle = SpendBundle(coin_spends, G2Element()) now = uint64(int(time.time())) add_list: List[Coin] = list(spend_bundle.additions()) rem_list: List[Coin] = list(spend_bundle.removals()) @@ -298,7 +298,7 @@ async def generate_signed_transaction( conditions=extra_conditions, ) did_announcement, coin_spend, vc = vc_record.vc.do_spend(inner_puzzle, innersol, new_proof_hash) - spend_bundles = [await self.wallet_state_manager.sign_transaction([coin_spend])] + spend_bundles = [SpendBundle([coin_spend], G2Element())] tx_list: List[TransactionRecord] = [] if did_announcement is not None: # Need to spend DID diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index cb978684af69..e4d072ff7cd1 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -426,8 +426,7 @@ async def generate_signed_transaction( 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) + spend_bundle: SpendBundle = SpendBundle(transaction, G2Element()) now = uint64(int(time.time())) add_list: List[Coin] = list(spend_bundle.additions()) @@ -513,9 +512,7 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: pk_parsed: G1Element = G1Element.from_bytes(pk) - dr: Optional[ - DerivationRecord - ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( + dr: Optional[DerivationRecord] = await self.wallet_state_manager.puzzle_store.record_for_puzzle_hash( puzzle_hash_for_synthetic_public_key(pk_parsed) ) if dr is None: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index fee15cf22451..427063e87ee5 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -108,11 +108,10 @@ 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.offer import Offer from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.uncurried_puzzle import uncurry_puzzle @@ -982,7 +981,7 @@ async def spend_clawback_coins( 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) + spend_bundle: SpendBundle = SpendBundle(coin_spends, G2Element()) tx_list: List[TransactionRecord] = [] if fee > 0: chia_tx = await self.main_wallet.create_tandem_xch_tx( @@ -2213,12 +2212,18 @@ async def coin_added( await self.create_more_puzzle_hashes() async def add_pending_transactions( - self, tx_records: List[TransactionRecord], merge_spends: bool = True + self, + tx_records: List[TransactionRecord], + merge_spends: bool = True, + sign: Optional[bool] = None, + additional_signing_responses: List[SigningResponse] = [], ) -> List[TransactionRecord]: """ Add a list of transactions to be submitted to the full node. Aggregates the `spend_bundle` property for each transaction onto the first transaction in the list. """ + if sign is None: + sign = self.config.get("auto_sign_txs", True) agg_spend: SpendBundle = SpendBundle.aggregate( [tx.spend_bundle for tx in tx_records if tx.spend_bundle is not None] ) @@ -2232,6 +2237,12 @@ async def add_pending_transactions( ) for i, tx in enumerate(tx_records) ] + if sign: + tx_records, _ = await self.sign_transactions( + tx_records, + additional_signing_responses, + additional_signing_responses != [], + ) all_coins_names = [] async with self.db_wrapper.writer_maybe_transaction(): for tx_record in tx_records: @@ -2537,16 +2548,6 @@ async def get_or_create_vc_wallet(self) -> VCWallet: return vc_wallet - async def sign_transaction(self, coin_spends: List[CoinSpend]) -> SpendBundle: - return await sign_coin_spends( - coin_spends, - self.get_private_key_for_pubkey, - self.get_synthetic_private_key_for_puzzle_hash, - self.constants.AGG_SIG_ME_ADDITIONAL_DATA, - self.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) - async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: return await self.main_wallet.sum_hint_for_pubkey(pk) @@ -2597,6 +2598,9 @@ async def gather_signing_info_for_txs(self, txs: List[TransactionRecord]) -> Lis [tx.spend_bundle for tx in txs if tx.spend_bundle is not None] ) + async def gather_signing_info_for_trades(self, offers: List[Offer]) -> List[UnsignedTransaction]: + return await self.gather_signing_info_for_bundles([offer._bundle for offer in offers]) + async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: @@ -2618,12 +2622,12 @@ def signed_tx_to_spendbundle(self, signed_tx: SignedTransaction) -> SpendBundle: async def sign_transactions( self, tx_records: List[TransactionRecord], - additional_signing_responses: List[SigningResponse], + additional_signing_responses: List[SigningResponse] = [], partial_allowed: bool = False, ) -> Tuple[List[TransactionRecord], List[SigningResponse]]: unsigned_txs: List[UnsignedTransaction] = await self.gather_signing_info_for_txs(tx_records) new_txs: List[TransactionRecord] = [] - all_signing_responses: List[SigningResponse] = [] + all_signing_responses = additional_signing_responses.copy() for unsigned_tx, tx in zip( unsigned_txs, [tx_record for tx_record in tx_records if tx_record.spend_bundle is not None] ): @@ -2641,6 +2645,49 @@ async def sign_transactions( new_txs.extend([tx_record for tx_record in tx_records if tx_record.spend_bundle is None]) return new_txs, all_signing_responses + async def sign_offers( + self, + offers: List[Offer], + additional_signing_responses: List[SigningResponse] = [], + partial_allowed: bool = False, + ) -> Tuple[List[Offer], List[SigningResponse]]: + unsigned_txs: List[UnsignedTransaction] = await self.gather_signing_info_for_trades(offers) + new_offers: List[Offer] = [] + all_signing_responses = additional_signing_responses.copy() + for unsigned_tx, offer in zip(unsigned_txs, [offer for offer in offers]): + signing_responses: List[SigningResponse] = await self.execute_signing_instructions( + unsigned_tx.signing_instructions, partial_allowed=partial_allowed + ) + all_signing_responses.extend(signing_responses) + new_bundle = self.signed_tx_to_spendbundle( + await self.apply_signatures( + unsigned_tx.transaction_info.spends, + [*additional_signing_responses, *signing_responses], + ) + ) + new_offers.append(Offer(offer.requested_payments, new_bundle, offer.driver_dict)) + return new_offers, all_signing_responses + + async def sign_bundle( + self, + coin_spends: List[CoinSpend], + additional_signing_responses: List[SigningResponse] = [], + partial_allowed: bool = False, + ) -> Tuple[SpendBundle, List[SigningResponse]]: + [unsigned_tx] = await self.gather_signing_info_for_bundles([SpendBundle(coin_spends, G2Element())]) + signing_responses: List[SigningResponse] = await self.execute_signing_instructions( + unsigned_tx.signing_instructions, partial_allowed=partial_allowed + ) + return ( + self.signed_tx_to_spendbundle( + await self.apply_signatures( + unsigned_tx.transaction_info.spends, + [*additional_signing_responses, *signing_responses], + ) + ), + signing_responses, + ) + async def submit_transactions(self, signed_txs: List[SignedTransaction]) -> List[bytes32]: bundles: List[SpendBundle] = [self.signed_tx_to_spendbundle(tx) for tx in signed_txs] for bundle in bundles: diff --git a/tests/core/data_layer/test_data_rpc.py b/tests/core/data_layer/test_data_rpc.py index 66ab124b1122..8588f1ef4ad9 100644 --- a/tests/core/data_layer/test_data_rpc.py +++ b/tests/core/data_layer/test_data_rpc.py @@ -918,8 +918,8 @@ class MakeAndTakeReference: make_one_take_one_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "7a5870e08cda8fb9066d8e49aae73841c5926860d093433a7cc82764b87b1b56", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff808080a18f4f7f0ac07240de1477a4147ca1bf7afd88808fd88aabb0296ddcffd6cd3cee16696c5fad7fed5fa67699a4ec554717b58e15c12a8fcebddc70c52a4cee4c98b7210cfcb5eb2a1d8f75f2575c421474e767efef39e652c0d2ef14f5f433b8", # noqa + "trade_id": "b34b77304778961deca03bd5eb370ed35a7aa97c0e030b293d1285b74d1741f4", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080a18f4f7f0ac07240de1477a4147ca1bf7afd88808fd88aabb0296ddcffd6cd3cee16696c5fad7fed5fa67699a4ec554717b58e15c12a8fcebddc70c52a4cee4c98b7210cfcb5eb2a1d8f75f2575c421474e767efef39e652c0d2ef14f5f433b8", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -963,7 +963,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x10".hex(), "value": b"\x01\x10".hex()}], taker_inclusions=[{"key": b"\x10".hex(), "value": b"\x02\x10".hex()}], - trade_id="a86b08e21b7677783812969fd8f8a1442d4d265cbb0bd2727bf6c16858789f5b", + trade_id="ecc205b2ffe49b87b2f385f595a395ab13cf0e0627e028a1222a0b4d255bdc18", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("8e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea"), @@ -978,8 +978,8 @@ class MakeAndTakeReference: make_one_take_one_same_values_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "19030b2dcea7a44d36df43cd3c08309bc55f354be087433d47727b64da8ec43e", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa09502844b542b20256008242b5676246b765d0e4c82714466a1140489e07bf0e880ffff81e8ff0bffffffffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f9180ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0352ef238e8561d0bfc8f754683b4d23ef9f40f7c4b574e333bb52ec81aa0b868ff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f91ffa0352ef238e8561d0bfc8f754683b4d23ef9f40f7c4b574e333bb52ec81aa0b868808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa06cae87c81f9edf8516076a9eb56a354ced636998a9f40b278c7bfddfb655df568080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ed15ec60900d04972f30df7bde60487bd63d17889f405398376809fc975e3177c0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f9180ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f91ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff808080877a8d3055ad9746e85b725221c78616af1a7b223a32be868d7b9135fbb4da0379307fa3073a958782d5db985425974a125117ae957b9074c0ee548bd4dfdd7ed19cbe9974a838383b51e19060ab6c2a7a83f676346125a288a6b6519e7aa148", # noqa + "trade_id": "d2bebdb0ee1fdd4a38f7c8c5a25bf6839794268f955e469818998ad5dad92d4d", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ed15ec60900d04972f30df7bde60487bd63d17889f405398376809fc975e3177c0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f9180ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f91ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa09502844b542b20256008242b5676246b765d0e4c82714466a1140489e07bf0e880ffff81e8ff0bffffffffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f9180ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0352ef238e8561d0bfc8f754683b4d23ef9f40f7c4b574e333bb52ec81aa0b868ff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f91ffa0352ef238e8561d0bfc8f754683b4d23ef9f40f7c4b574e333bb52ec81aa0b868808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa06cae87c81f9edf8516076a9eb56a354ced636998a9f40b278c7bfddfb655df568080ff018080808080ffff80ff80ff80ff80ff8080808080877a8d3055ad9746e85b725221c78616af1a7b223a32be868d7b9135fbb4da0379307fa3073a958782d5db985425974a125117ae957b9074c0ee548bd4dfdd7ed19cbe9974a838383b51e19060ab6c2a7a83f676346125a288a6b6519e7aa148", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1023,7 +1023,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x10".hex(), "value": b"\x05\x10".hex()}], taker_inclusions=[{"key": b"\x10".hex(), "value": b"\x05\x10".hex()}], - trade_id="09c4af6aa770f6797516dbae697a5efead5a1eaa29295fcc314b3f2f48fd9fe9", + trade_id="57e31189153fca54e70207c02bf27b8e271fcbfa1fac8076474a9a1cc04d3b63", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("1d1eb374688e3033cbce2514e4fded10ceffe068e663718b8a20716a65019f91"), @@ -1038,8 +1038,8 @@ class MakeAndTakeReference: make_two_take_one_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "651bc5d812aa3e72f63963504ced83da0a7c924ea5910b361dbbb58839ccfc92", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0890bd0a05cc152c27a3a72348d59e9c5fb46c18da8f32948f1f2143b35014aca80ffff81e8ff0bffffffffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70e80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa09130956ec241f3c4f8807f6889e65025947fbd7bb757d8df0ba2640e293bcc60ff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70effa09130956ec241f3c4f8807f6889e65025947fbd7bb757d8df0ba2640e293bcc60808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071e69c05748eab24bce907ef7840c14873f1e668bda10c7aab57e25bb7895a88db20000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70e80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70effa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff808080952dd3e68ea36950708547ddc57bea7a97d9e7a2d5c7921be7c72be43abc432406b6b1786e3a52b6a5dc53159409e55703d00eb60ca407a2712a5b637bc3599a0073d7c9a6009c16547f5fae0f8932002d5318fa4f2f4a9f19ab3f7362118230", # noqa + "trade_id": "04ea501a3344c1c4c7aedf50ec2751d69d2ff09bd6caeb1ea529071225f413bc", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071e69c05748eab24bce907ef7840c14873f1e668bda10c7aab57e25bb7895a88db20000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70e80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70effa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0890bd0a05cc152c27a3a72348d59e9c5fb46c18da8f32948f1f2143b35014aca80ffff81e8ff0bffffffffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70e80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa09130956ec241f3c4f8807f6889e65025947fbd7bb757d8df0ba2640e293bcc60ff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70effa09130956ec241f3c4f8807f6889e65025947fbd7bb757d8df0ba2640e293bcc60808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080952dd3e68ea36950708547ddc57bea7a97d9e7a2d5c7921be7c72be43abc432406b6b1786e3a52b6a5dc53159409e55703d00eb60ca407a2712a5b637bc3599a0073d7c9a6009c16547f5fae0f8932002d5318fa4f2f4a9f19ab3f7362118230", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1113,7 +1113,7 @@ class MakeAndTakeReference: {"key": b"\x11".hex(), "value": b"\x01\x11".hex()}, ], taker_inclusions=[{"key": b"\x10".hex(), "value": b"\x02\x10".hex()}], - trade_id="9c407637b889be3b61b3f5599b7391ee6edbf69a0c8c954656231c0bfb710b08", + trade_id="4f5412917a22233fa6186013392144bc469d23576d108b98faa9c7e76d036af4", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("043fed6d67961e36db2900b6aab24aa68be529c4e632aace486fbea1b26dc70e"), @@ -1128,8 +1128,8 @@ class MakeAndTakeReference: make_one_take_two_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "d92dc63d042226f6151b830edf79d58f075c3eb005cf9fab40d8f744bb9851c7", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab5ffa05eadd0f5982411ec074786cb6e2e37880d2ea1f007b47bc50a1b36cc2c61ba098080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808097ff2e118c10f392eb2e53be370c1a9226862b59ab0dca4e1751b2196cfdc301f71fddd152d1324e7f3d32800df7c5af14cd0e4ad5389bea2a0f10330916fc29d2debf7d8ab4c3ac496b5d301de36ebb7973888443e13244c68246e54377b775", # noqa + "trade_id": "fa88e673fd1235efd43c1ef4f2957c88f7dcd0b2cfd5bde54a65d52d148ce670", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab5ffa05eadd0f5982411ec074786cb6e2e37880d2ea1f007b47bc50a1b36cc2c61ba098080ff018080808080ffff80ff80ff80ff80ff808080808097ff2e118c10f392eb2e53be370c1a9226862b59ab0dca4e1751b2196cfdc301f71fddd152d1324e7f3d32800df7c5af14cd0e4ad5389bea2a0f10330916fc29d2debf7d8ab4c3ac496b5d301de36ebb7973888443e13244c68246e54377b775", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1176,7 +1176,7 @@ class MakeAndTakeReference: {"key": b"\x10".hex(), "value": b"\x02\x10".hex()}, {"key": b"\x11".hex(), "value": b"\x02\x11".hex()}, ], - trade_id="d53d08a6951849cd33de3a703bc133a2ae973a34ce4527e19e233fb5cb57bbe3", + trade_id="48efd113518a57895d45f5cde246d9e088e08d96a562e6013eb4c04527e4d5ba", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("8e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea"), @@ -1191,8 +1191,8 @@ class MakeAndTakeReference: make_one_existing_take_one_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "2dbea3ff730677d4ddb9ae30691e629d183f9103f08c3c7e849bff9ad94168a4", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa06766ecb6a87bcab8829fc9b3f08c8b5a83ecb7c5597c6a13ec346bfcafc1aab880ffff33ffa09b077471a29fd048bf897998e3f73ee5215345cd4943441e7c654dc11f2c579eff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27ffa09b077471a29fd048bf897998e3f73ee5215345cd4943441e7c654dc11f2c579e808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ef1932b0458af07a67925e9e0d5eca3ae137ba72bc689bd9b7b00bd0508ee6be80000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff8080809200d622b5462e5c56846dfbf374090fe9afef15ade6b26cac913dce6005df00550a26f624807adb98240ea731fa37d2054f37d4f2feb416b110c4cdf190ed5393e059c871b7e56897bf65aa4c1b95b31d2f16e41600d8f74372ee41cc11b39b", # noqa + "trade_id": "1efc47397715da444017864d15b92676bc416afcb7aba14047c6edfd6f4fa766", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ef1932b0458af07a67925e9e0d5eca3ae137ba72bc689bd9b7b00bd0508ee6be80000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa06766ecb6a87bcab8829fc9b3f08c8b5a83ecb7c5597c6a13ec346bfcafc1aab880ffff33ffa09b077471a29fd048bf897998e3f73ee5215345cd4943441e7c654dc11f2c579eff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27ffa09b077471a29fd048bf897998e3f73ee5215345cd4943441e7c654dc11f2c579e808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff80808080809200d622b5462e5c56846dfbf374090fe9afef15ade6b26cac913dce6005df00550a26f624807adb98240ea731fa37d2054f37d4f2feb416b110c4cdf190ed5393e059c871b7e56897bf65aa4c1b95b31d2f16e41600d8f74372ee41cc11b39b", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1241,7 +1241,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x09".hex(), "value": b"\x01\x09".hex()}], taker_inclusions=[{"key": b"\x10".hex(), "value": b"\x02\x10".hex()}], - trade_id="74ce97a6154467ca1a868e546a5d9e15e1e61c386aa27cb3686b198613972606", + trade_id="faea189031da8557299173e6731dafea53d85e116485ffaa8ff2070278145608", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), ], @@ -1255,8 +1255,8 @@ class MakeAndTakeReference: make_one_take_one_existing_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "d4387ed635c1ce7276dfee12f2a52bea3919291b15d126e6f4727af6944091a0", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa09dd8b0a6d67ee56221d0fe6bb131eb30d17c098c7548a78a962836011ea465bb8080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff808080a01626555f71b404f754061c4de0143b82248008fd1920466eee86ce447db889cf4287b8363a4e0bfdb628decddf19ea074c313ca4ce097f61ba67120e319481ccacbd1adcff2d1d95b859ff516691bca16e2614a56cbd3c1f1ec5580da269ab", # noqa + "trade_id": "b8576f50159e5e9eae5de00b8986084705f0796317d5da8cd0e968f2c1a7893e", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa09dd8b0a6d67ee56221d0fe6bb131eb30d17c098c7548a78a962836011ea465bb8080ff018080808080ffff80ff80ff80ff80ff8080808080a01626555f71b404f754061c4de0143b82248008fd1920466eee86ce447db889cf4287b8363a4e0bfdb628decddf19ea074c313ca4ce097f61ba67120e319481ccacbd1adcff2d1d95b859ff516691bca16e2614a56cbd3c1f1ec5580da269ab", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1300,7 +1300,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x10".hex(), "value": b"\x01\x10".hex()}], taker_inclusions=[{"key": b"\x09".hex(), "value": b"\x02\x09".hex()}], - trade_id="be67400ac9856e7aa1ec96071457bda73f7304115902ac387ad2b1e085115956", + trade_id="c390d8ff22ae5c6fe4559a01f713703aa023e6b8f2e89ec4340da5237d4d9c95", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("8e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea"), @@ -1314,8 +1314,8 @@ class MakeAndTakeReference: make_one_upsert_take_one_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "86a86e28563d06d2b28c49b7a16110fc9944c52004e9b3e5d81cce6138184caf", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0214dc115c3f3a3444619449b297fe03521f85c8cc12be80d8de35bb9cfb29e6d80ffff81e8ff0bffffffffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c680ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa05ef937e981ce68f2fa71e00b139acb3352b5ec32e7d6bc160874a456f106016cff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c6ffa05ef937e981ce68f2fa71e00b139acb3352b5ec32e7d6bc160874a456f106016c808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071e3a306420fb91e6a9c25cb93938b5c9e164ce761abc2ab967f8545cdcdc9e6e6d0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c680ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c6ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff808080a389748450bfea49b130c33fd987561f90eef2ffb8d5aac36590fa1d724f5d8bc0b86f23f0538b35922a098427a5afee0280ed240c03fcd9d736c7e752152f4387a023222ba5711c05417f369aeda2827cef78793acfd79dd7960c80932ccc89", # noqa + "trade_id": "99cac76f3180c06126087b1188f3a5cd6a5f5f2830ff639e7fca9e11afe2b477", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071e3a306420fb91e6a9c25cb93938b5c9e164ce761abc2ab967f8545cdcdc9e6e6d0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c680ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c6ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0214dc115c3f3a3444619449b297fe03521f85c8cc12be80d8de35bb9cfb29e6d80ffff81e8ff0bffffffffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c680ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa05ef937e981ce68f2fa71e00b139acb3352b5ec32e7d6bc160874a456f106016cff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa03761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c6ffa05ef937e981ce68f2fa71e00b139acb3352b5ec32e7d6bc160874a456f106016c808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff8080808080a389748450bfea49b130c33fd987561f90eef2ffb8d5aac36590fa1d724f5d8bc0b86f23f0538b35922a098427a5afee0280ed240c03fcd9d736c7e752152f4387a023222ba5711c05417f369aeda2827cef78793acfd79dd7960c80932ccc89", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1359,7 +1359,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x09".hex(), "value": b"\x01\x10".hex()}], taker_inclusions=[{"key": b"\x10".hex(), "value": b"\x02\x10".hex()}], - trade_id="72232956344e9f12eec28635e9299d367e9fd9c4a8759db0f8f110c872919ff0", + trade_id="25651c321494cee55394b73a69d638df34026d999887a88b154696880231df72", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("3761921b9b0520458995bb0ec353ea28d36efa2a7cfc3aba6772f005f7dd34c6"), @@ -1374,8 +1374,8 @@ class MakeAndTakeReference: make_one_take_one_upsert_reference = MakeAndTakeReference( entries_to_insert=10, make_offer_response={ - "trade_id": "51b7769fbf845f8c42b6c31dafb6247e2af958681972d2125f70d9157082c26b", - "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa05743f9c9e6f3ebd1506342bbf0a6bfb9dc68b58b3e7f6f32da759fb0fb74fe0e8080ff018080808080ffff80ff80ff80ff80ff8080808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff808080a7db721cb3da800e516ac87dace73404ad29cf68377a3af155e1ee1205dc34c07cefdbef7e3a9de7bf492a840c74883c039698b68f2d4d42062f5a2fd8c99da29226771305720f305cde56c417d0c5de53a9322fd3cedc4150d42540863edeaa", # noqa + "trade_id": "d9d985e9bc941df8f5718f31b597a061e99fb39d430def2b3d8bc289b0b1020e", + "offer": "00000003000000000000000000000000000000000000000000000000000000000000000052eba05592a7cbe77b4b1552cacec440b20d523d08a6be917c9213dc34f3033a0000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa042f08ebc0578f2cec7a9ad1c3038e74e0f30eba5c2f4cb1ee1c8fdb682c19dbb80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080ca2e21c90d263e63b73d449a3f8d57b9458846f7af27d9a61a515395fa14071ea55bba78c76265b4bac257251b1e89dd13637a7c18e8dcb03e092dfb7eb5a84a0000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2aff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0c842b1a384b8633ac25d0f12bd7b614f86a77642ab6426418750f2b0b86bab2a8080ffff3eff248080ff018080808080ff01808080ffffa032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243ffa08c4aebb18e8ce08405083c3d90a29f30239865142e2dcbca5393f40df9e3821dff0180ff01ffff80808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa243aa064e96a86637d8f5ebe153dc8645d29f43bee762d5ec10d06c8617fa60b8c50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa06661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e2780ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a132fae32c98cbb7d8f5814c49ee3f0ba6ec2172c5e5f6900655a65cd2157a06a1c6eb89c68c8d2cdcee9506c2217978ff018080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0bd7aa54c5f93ef1738439aa60b471ce2aa4c62fb18a7943aa10061f00dbdb83680ffff81e8ff0bffffffffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea80ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10deff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa08e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65eaffa0ca77e42ac3b3375edc54af271f21d075afd02d72969cababeec63e22f7ab10de808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa05743f9c9e6f3ebd1506342bbf0a6bfb9dc68b58b3e7f6f32da759fb0fb74fe0e8080ff018080808080ffff80ff80ff80ff80ff8080808080a7db721cb3da800e516ac87dace73404ad29cf68377a3af155e1ee1205dc34c07cefdbef7e3a9de7bf492a840c74883c039698b68f2d4d42062f5a2fd8c99da29226771305720f305cde56c417d0c5de53a9322fd3cedc4150d42540863edeaa", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1419,7 +1419,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x10".hex(), "value": b"\x01\x10".hex()}], taker_inclusions=[{"key": b"\x09".hex(), "value": b"\x02\x10".hex()}], - trade_id="399511c325cc7ac6df2e195271f9001f965d25327e46f89049aec1e286252746", + trade_id="515ecf094f1a4439faa9d64b8101b68df01536588ebbc9970c41c3f11ad0d602", maker_root_history=[ bytes32.from_hexstr("6661ea6604b491118b0f49c932c0f0de2ad815a57b54b6ec8fdbd1b408ae7e27"), bytes32.from_hexstr("8e54f5066aa7999fc1561a56df59d11ff01f7df93cadf49a61adebf65dec65ea"), @@ -1434,8 +1434,8 @@ class MakeAndTakeReference: make_one_take_one_unpopulated_reference = MakeAndTakeReference( entries_to_insert=0, make_offer_response={ - "trade_id": "94fd7c3078efd1f3fd378c72a2b2b8a2c44bb4ed00e22f42e5ce4c06db8f8ba1", - "offer": "000000030000000000000000000000000000000000000000000000000000000000000000785bba8a904677219cb83f053b1bcf3b5ed13d35895c5babb007c67727e743590000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0000000000000000000000000000000000000000000000000000000000000000080ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff80808080a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa478416871446884ef363bd105960c464b4208a293b348f0f1c2e12140df38469450000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0000000000000000000000000000000000000000000000000000000000000000080ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a3b0219722055ac0a66cd9de5cd3e86962d8c8ec6abb801b57e5c77ed98453b02ceae0e19548f6d4fc20b3a2ec82aa90ff018080ff018080808080ff01808080ffffa09563629e653a9fc3c65f55947883a47e062e6b67394091228ec01352ff78f333ff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0fccf087e5b81be2137cfaa35e65cc4e4a25183108907dad33c6d622e8e78349e80ffff81e8ff0bffffffffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c5880ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0b6565d3afb87a60cfdf66bc56cca80b14afc2be649971c8df647ce617b442e6eff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c58ffa0b6565d3afb87a60cfdf66bc56cca80b14afc2be649971c8df647ce617b442e6e808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff808080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa24386ab01fbd8342f8e1dac10d6e906cef3892857bd1865b6fd7ed4b01b39d568b50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c5880ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0846b58db3bd246785e202eeddfbb46acaf267f011307437cd4e0841f3da751f6ff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c58ffa0846b58db3bd246785e202eeddfbb46acaf267f011307437cd4e0841f3da751f68080ffff3eff248080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffff80808090ce896a92f47ab8adec0d7e977795762d85660495f2a6a0c025aec987faac7ec2bd78c17fdcfe126c35fb52ebe75c610df4013ecc5036e95168aba697678d89ab7718664439302d32f1eb85bf263461855c7f3da9050ba7c8da4abb01321836", # noqa + "trade_id": "120a94680f0cff61cdf3b123260a98253649a2b9762b7d2dead60caad96276e4", + "offer": "000000030000000000000000000000000000000000000000000000000000000000000000785bba8a904677219cb83f053b1bcf3b5ed13d35895c5babb007c67727e743590000000000000000ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0000000000000000000000000000000000000000000000000000000000000000080ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff15ff29ff8080ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff088080ff0180ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ff018080808080ff01808080ffffa00000000000000000000000000000000000000000000000000000000000000000ffffa00000000000000000000000000000000000000000000000000000000000000000ff01ff8080808032dbe6d545f24635c7871ea53c623c358d7cea8f5e27a983ba6e5c0bf35fa24386ab01fbd8342f8e1dac10d6e906cef3892857bd1865b6fd7ed4b01b39d568b50000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c5880ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff01ffff33ffa0846b58db3bd246785e202eeddfbb46acaf267f011307437cd4e0841f3da751f6ff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c58ffa0846b58db3bd246785e202eeddfbb46acaf267f011307437cd4e0841f3da751f68080ffff3eff248080ff018080808080ff01808080ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa01804338c97f989c78d88716206c0f27315f3eb7d59417ab2eacee20f0a7ff60bff0180ff01ffff808080a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa478416871446884ef363bd105960c464b4208a293b348f0f1c2e12140df38469450000000000000001ff02ffff01ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080ffff04ffff01ffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9ffff04ffff01ff02ffff01ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080ffff04ffff01a0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2ffff04ffff01ffa0000000000000000000000000000000000000000000000000000000000000000080ffff04ffff01a057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a9ffff04ffff01ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a3b0219722055ac0a66cd9de5cd3e86962d8c8ec6abb801b57e5c77ed98453b02ceae0e19548f6d4fc20b3a2ec82aa90ff018080ff018080808080ff01808080ffffa09563629e653a9fc3c65f55947883a47e062e6b67394091228ec01352ff78f333ff0180ff01ffffff80ffff02ffff01ff02ffff01ff02ffff03ff5fffff01ff02ff3affff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ffff02ff05ff8205ff80ff8080808080808080808080ffff01ff04ffff04ff10ffff01ff81ff8080ffff02ff05ff8205ff808080ff0180ffff04ffff01ffffff49ff3f02ff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff05ffff01ff02ffff03ffff02ff3effff04ff02ffff04ff82011fffff04ff27ffff04ff4fff808080808080ffff01ff02ff3affff04ff02ffff04ff0dffff04ff1bffff04ff37ffff04ff6fffff04ff81dfffff04ff8201bfffff04ff82037fffff04ffff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff11ffff04ffff02ff26ffff04ff02ffff04ff13ffff04ff82027fffff04ffff02ff36ffff04ff02ffff04ff82013fff80808080ffff04ffff02ff36ffff04ff02ffff04ff819fff80808080ffff04ffff02ff36ffff04ff02ffff04ff13ff80808080ff8080808080808080ffff04ffff02ff36ffff04ff02ffff04ff09ff80808080ff808080808080ffff012480ff808080ff8202ff80ff8080808080808080808080ffff01ff088080ff0180ffff018202ff80ff0180ffffff0bff12ffff0bff2cff3880ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff1bffff01ff02ff2effff04ff02ffff04ffff02ffff03ffff18ffff0101ff1380ffff01ff0bffff0102ff2bff0580ffff01ff0bffff0102ff05ff2b8080ff0180ffff04ffff04ffff17ff13ffff0181ff80ff3b80ff8080808080ffff010580ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff05ffff02ff2effff04ff02ffff04ff13ffff04ff27ff808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff37ff808080808080ffff01ff088080ff0180ffff01ff010180ff0180ff018080ffff04ffff01ff01ffff3fffa0fccf087e5b81be2137cfaa35e65cc4e4a25183108907dad33c6d622e8e78349e80ffff81e8ff0bffffffffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c5880ffa057bfd1cb0adda3d94315053fda723f2028320faa8338225d99f629e3d46d43a980ff808080ffff33ffa0b6565d3afb87a60cfdf66bc56cca80b14afc2be649971c8df647ce617b442e6eff01ffffa0a14daf55d41ced6419bcd011fbc1f74ab9567fe55340d88435aa6493d628fa47ffa0de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c58ffa0b6565d3afb87a60cfdf66bc56cca80b14afc2be649971c8df647ce617b442e6e808080ffff04ffff01ffffa07faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9fffa07acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3a0eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da980ffff04ffff01ffa0a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c280ffff04ffff01ffffa07f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab58080ff018080808080ffff80ff80ff80ff80ff808080808090ce896a92f47ab8adec0d7e977795762d85660495f2a6a0c025aec987faac7ec2bd78c17fdcfe126c35fb52ebe75c610df4013ecc5036e95168aba697678d89ab7718664439302d32f1eb85bf263461855c7f3da9050ba7c8da4abb01321836", # noqa "taker": [ { "store_id": "7acfcbd1ed73bfe2b698508f4ea5ed353c60ace154360272ce91f9ab0c8423c3", @@ -1458,7 +1458,7 @@ class MakeAndTakeReference: }, maker_inclusions=[{"key": b"\x10".hex(), "value": b"\x01\x10".hex()}], taker_inclusions=[{"key": b"\x10".hex(), "value": b"\x02\x10".hex()}], - trade_id="728e96f4404b6b7e7eb09df29b2ce77d9319fc796a0ea2d0d553c1904acf6cc2", + trade_id="2dc357c29da4362c6c63dbad128086062d53180395ae86b485dd429ce3791c37", maker_root_history=[bytes32.from_hexstr("de4ec93c032f5117d8af076dfc86faa5987a6c0b1d52ffc9cf0dfa43989d8c58")], taker_root_history=[bytes32.from_hexstr("7f3e180acdf046f955d3440bb3a16dfd6f5a46c809cee98e7514127327b1cab5")], ) diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 482d8b61c0a3..f4f096203a5e 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -184,7 +184,7 @@ async def test_block_compression( ph, DEFAULT_TX_CONFIG, ) - await wallet.wallet_state_manager.add_pending_transactions([tr]) + [tr] = await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -223,7 +223,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.wallet_state_manager.add_pending_transactions([tr]) + [tr] = await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -266,7 +266,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.wallet_state_manager.add_pending_transactions([tr]) + [tr] = await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -278,7 +278,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.wallet_state_manager.add_pending_transactions([tr]) + [tr] = await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -291,7 +291,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.wallet_state_manager.add_pending_transactions([tr]) + [tr] = await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -304,7 +304,7 @@ async def check_transaction_confirmed(transaction) -> bool: ph, DEFAULT_TX_CONFIG, ) - await wallet.wallet_state_manager.add_pending_transactions([tr]) + [tr] = await wallet.wallet_state_manager.add_pending_transactions([tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, @@ -402,7 +402,7 @@ async def check_transaction_confirmed(transaction) -> bool: additions=new_spend_bundle.additions(), removals=new_spend_bundle.removals(), ) - await wallet.wallet_state_manager.add_pending_transactions([new_tr]) + [new_tr] = await wallet.wallet_state_manager.add_pending_transactions([new_tr]) await time_out_assert( 10, full_node_2.full_node.mempool_manager.get_spendbundle, diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index ba1be456e64e..e6093f2101b3 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -86,7 +86,7 @@ async def peak_height(fna: FullNodeAPI): [tx] = await wallet_0.wallet_state_manager.main_wallet.generate_signed_transaction( 10, ph1, DEFAULT_TX_CONFIG, 0 ) - await wallet_0.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_0.wallet_state_manager.add_pending_transactions([tx]) await time_out_assert( 10, @@ -160,7 +160,7 @@ async def test_mempool_tx_sync(self, three_nodes_two_wallets, self_hostname, see [tx] = await wallet_0.wallet_state_manager.main_wallet.generate_signed_transaction( 10, bytes32.random(seeded_random), DEFAULT_TX_CONFIG, 0 ) - await wallet_0.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_0.wallet_state_manager.add_pending_transactions([tx]) await time_out_assert( 10, diff --git a/tests/core/mempool/test_mempool_manager.py b/tests/core/mempool/test_mempool_manager.py index 143a3a87b668..194db4ce986b 100644 --- a/tests/core/mempool/test_mempool_manager.py +++ b/tests/core/mempool/test_mempool_manager.py @@ -1439,6 +1439,7 @@ async def make_setup_and_coins( [tx] = await wallet.generate_signed_transaction( uint64(200), phs[0], DEFAULT_TX_CONFIG, primaries=other_recipients ) + [tx], _ = await wallet.wallet_state_manager.sign_transactions([tx]) assert tx.spend_bundle is not None await send_to_mempool(full_node_api, tx.spend_bundle) await farm_a_block(full_node_api, wallet_node, ph) @@ -1456,6 +1457,7 @@ async def make_setup_and_coins( [tx_a] = await wallet.generate_signed_transaction(uint64(30), ph, DEFAULT_TX_CONFIG, coins={coins[0].coin}) [tx_b] = await wallet.generate_signed_transaction(uint64(30), ph, DEFAULT_TX_CONFIG, coins={coins[1].coin}) [tx_c] = await wallet.generate_signed_transaction(uint64(30), ph, DEFAULT_TX_CONFIG, coins={coins[2].coin}) + [tx_a, tx_b, tx_c], _ = await wallet.wallet_state_manager.sign_transactions([tx_a, tx_b, tx_c]) assert tx_a.spend_bundle is not None assert tx_b.spend_bundle is not None assert tx_c.spend_bundle is not None @@ -1472,6 +1474,7 @@ async def make_setup_and_coins( [tx] = await wallet.generate_signed_transaction( uint64(200), IDENTITY_PUZZLE_HASH, DEFAULT_TX_CONFIG, coins={coins[3].coin} ) + [tx], _ = await wallet.wallet_state_manager.sign_transactions([tx]) assert tx.spend_bundle is not None await send_to_mempool(full_node_api, tx.spend_bundle) await farm_a_block(full_node_api, wallet_node, ph) @@ -1509,6 +1512,7 @@ async def make_setup_and_coins( coins={coins[5].coin}, extra_conditions=(e_announcement,), ) + [tx_d, tx_f], _ = await wallet.wallet_state_manager.sign_transactions([tx_d, tx_f]) assert tx_d.spend_bundle is not None assert tx_f.spend_bundle is not None # Create transaction E now that spends e_coin to create another eligible @@ -1537,6 +1541,7 @@ async def make_setup_and_coins( [tx_g] = await wallet.generate_signed_transaction( uint64(13), ph, DEFAULT_TX_CONFIG, coins={g_coin}, extra_conditions=(e_announcement,) ) + [tx_g], _ = await wallet.wallet_state_manager.sign_transactions([tx_g]) assert tx_g.spend_bundle is not None sb_e2g = SpendBundle.aggregate([sb_e2, tx_g.spend_bundle]) sb_e2g_name = sb_e2g.name() diff --git a/tests/simulation/test_simulation.py b/tests/simulation/test_simulation.py index 9afcdfe24aae..f10fcc1d65c3 100644 --- a/tests/simulation/test_simulation.py +++ b/tests/simulation/test_simulation.py @@ -220,7 +220,7 @@ async def test_simulator_auto_farm_and_get_coins( DEFAULT_TX_CONFIG, uint64(0), ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) # wait till out of mempool await time_out_assert(10, full_node_api.full_node.mempool_manager.get_spendbundle, None, tx.name) # wait until the transaction is confirmed @@ -394,7 +394,7 @@ async def test_wait_transaction_records_entered_mempool( tx_config=DEFAULT_TX_CONFIG, coins={coin}, ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert tx.spend_bundle is not None @@ -446,7 +446,8 @@ async def test_process_transactions( ] for tx in transactions: assert tx.spend_bundle is not None, "the above created transaction is missing the expected spend bundle" - await wallet.wallet_state_manager.add_pending_transactions([tx]) + + transactions = await wallet.wallet_state_manager.add_pending_transactions(transactions) if records_or_bundles_or_coins == "records": await full_node_api.process_transaction_records(records=transactions) diff --git a/tests/simulation/test_simulator.py b/tests/simulation/test_simulator.py index 9c2c540a4fa2..2bea8663892e 100644 --- a/tests/simulation/test_simulator.py +++ b/tests/simulation/test_simulator.py @@ -133,7 +133,7 @@ async def test_wait_transaction_records_entered_mempool( tx_config=DEFAULT_TX_CONFIG, coins={coin}, ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert tx.spend_bundle is not None @@ -168,7 +168,7 @@ async def test_process_transaction_records( tx_config=DEFAULT_TX_CONFIG, coins={coin}, ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.process_transaction_records(records=[tx]) assert full_node_api.full_node.coin_store.get_coin_record(coin.name()) is not None diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 15656eb3842d..4a4144ced759 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -29,7 +29,11 @@ from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import _derive_path_unhardened, master_sk_to_wallet_sk_unhardened_intermediate from chia.wallet.lineage_proof import LineageProof -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_pk +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + puzzle_hash_for_pk, + puzzle_hash_for_synthetic_public_key, +) +from chia.wallet.sign_coin_spends import sign_coin_spends from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import WalletType @@ -234,7 +238,7 @@ async def test_cat_spend(self, self_hostname, two_wallet_nodes, trusted): [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG, fee=uint64(1) ) tx_id = None - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet.wallet_state_manager.add_pending_transactions(tx_records) for tx_record in tx_records: if tx_record.wallet_id is cat_wallet.id(): tx_id = tx_record.name.hex() @@ -266,7 +270,7 @@ async def test_cat_spend(self, self_hostname, two_wallet_nodes, trusted): assert list(memos[tx_id].values())[0][0] == cat_2_hash.hex() cat_hash = await cat_wallet.get_new_inner_hash() tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash], DEFAULT_TX_CONFIG) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet2.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node_api.txs_in_mempool, True, tx_records) @@ -345,7 +349,7 @@ async def test_cat_reuse_address(self, self_hostname, two_wallet_nodes, trusted) tx_records = await cat_wallet.generate_signed_transaction( [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG.override(reuse_puzhash=True), fee=uint64(1) ) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet.wallet_state_manager.add_pending_transactions(tx_records) for tx_record in tx_records: if tx_record.wallet_id is cat_wallet.id(): assert tx_record.to_puzzle_hash == cat_2_hash @@ -373,7 +377,7 @@ async def test_cat_reuse_address(self, self_hostname, two_wallet_nodes, trusted) cat_hash = await cat_wallet.get_new_inner_hash() tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash], DEFAULT_TX_CONFIG) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet2.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(15, full_node_api.txs_in_mempool, True, tx_records) @@ -508,7 +512,7 @@ async def test_cat_doesnt_see_eve(self, self_hostname, two_wallet_nodes, trusted tx_records = await cat_wallet.generate_signed_transaction( [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG, fee=uint64(1) ) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) await time_out_assert(30, wallet.get_confirmed_balance, funds - 101) @@ -524,7 +528,7 @@ async def test_cat_doesnt_see_eve(self, self_hostname, two_wallet_nodes, trusted [tx_record] = await wallet.wallet_state_manager.main_wallet.generate_signed_transaction( 10, cc2_ph, DEFAULT_TX_CONFIG, 0 ) - await wallet.wallet_state_manager.add_pending_transactions([tx_record]) + [tx_record] = await wallet.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) id = cat_wallet_2.id() @@ -612,7 +616,7 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust tx_records = await cat_wallet_0.generate_signed_transaction( [uint64(60), uint64(20)], [cat_1_hash, cat_2_hash], DEFAULT_TX_CONFIG ) - await wallet_0.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_0.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) await time_out_assert(20, cat_wallet_0.get_confirmed_balance, 20) @@ -627,10 +631,10 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust cat_hash = await cat_wallet_0.get_new_inner_hash() tx_records = await cat_wallet_1.generate_signed_transaction([uint64(15)], [cat_hash], DEFAULT_TX_CONFIG) - await wallet_1.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_1.wallet_state_manager.add_pending_transactions(tx_records) tx_records_2 = await cat_wallet_2.generate_signed_transaction([uint64(20)], [cat_hash], DEFAULT_TX_CONFIG) - await wallet_2.wallet_state_manager.add_pending_transactions(tx_records_2) + tx_records_2 = await wallet_2.wallet_state_manager.add_pending_transactions(tx_records_2) await full_node_api.process_transaction_records(records=[*tx_records, *tx_records_2]) @@ -654,7 +658,7 @@ async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trust [uint64(30)], [cat_hash], DEFAULT_TX_CONFIG, memos=[[b"too"], [b"many"], [b"memos"]] ) - await wallet_1.wallet_state_manager.add_pending_transactions(tx_records_3) + tx_records_3 = await wallet_1.wallet_state_manager.add_pending_transactions(tx_records_3) await time_out_assert(15, full_node_api.txs_in_mempool, True, tx_records_3) txs = await wallet_1.wallet_state_manager.tx_store.get_transactions_between(cat_wallet_1.id(), 0, 100000) for tx in txs: @@ -726,7 +730,7 @@ async def test_cat_max_amount_send(self, self_hostname, two_wallet_nodes, truste tx_records = await cat_wallet.generate_signed_transaction( amounts, puzzle_hashes, DEFAULT_TX_CONFIG, coins={spent_coint} ) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) await asyncio.sleep(2) @@ -834,7 +838,7 @@ async def test_cat_hint(self, self_hostname, two_wallet_nodes, trusted, autodisc [uint64(60)], [cat_2_hash], DEFAULT_TX_CONFIG, memos=[[cat_2_hash]] ) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) @@ -865,7 +869,7 @@ async def check_wallets(node): [uint64(10)], [cat_2_hash], DEFAULT_TX_CONFIG, memos=[[cat_2_hash]] ) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) @@ -882,7 +886,7 @@ async def check_wallets(node): cat_hash = await cat_wallet.get_new_inner_hash() tx_records = await cat_wallet_2.generate_signed_transaction([uint64(5)], [cat_hash], DEFAULT_TX_CONFIG) - await wallet.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet2.wallet_state_manager.add_pending_transactions(tx_records) await full_node_api.process_transaction_records(records=tx_records) @@ -977,7 +981,7 @@ async def test_cat_change_detection( ).get_tree_hash(), cat_amount_0, ) - eve_spend = await wallet_node_0.wallet_state_manager.sign_transaction( + eve_spend = await sign_coin_spends( [ CoinSpend( cat_coin, @@ -1036,6 +1040,11 @@ async def test_cat_change_detection( ), ), ], + wallet_node_0.wallet_state_manager.get_private_key_for_pubkey, + wallet_node_0.wallet_state_manager.get_synthetic_private_key_for_puzzle_hash, + wallet_node_0.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA, + wallet_node_0.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + [puzzle_hash_for_synthetic_public_key], ) await client_0.push_tx(eve_spend) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, eve_spend.name()) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index e111e7fdba0e..4c15879bbefb 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -450,13 +450,18 @@ async def test_cat_trades( assert trade_make is not None peer = wallet_node_taker.get_full_node_peer() + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, wallet_environments.tx_config, fee=uint64(1), ) - await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert trade_take is not None assert tx_records is not None @@ -657,12 +662,17 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): assert success is True assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, wallet_environments.tx_config, ) - await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert trade_take is not None assert tx_records is not None @@ -774,12 +784,17 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): assert error is None assert success is True assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, wallet_environments.tx_config, ) - await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -963,12 +978,17 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): assert success is True assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, wallet_environments.tx_config, ) - await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -1203,12 +1223,17 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): assert error is None assert success is True assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, wallet_environments.tx_config, ) - await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -1329,12 +1354,17 @@ async def assert_trade_tx_number(wallet_node, trade_id, number): assert success is True assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, wallet_environments.tx_config, ) - await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) assert trade_take is not None assert tx_records is not None @@ -1577,10 +1607,16 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: # Due to current mempool rules, trying to force a take out of the mempool with a cancel will not work. # Uncomment this when/if it does + # [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + # [Offer.from_bytes(trade_make.offer)] + # ) # trade_take, tx_records = await trade_manager_taker.respond_to_offer( - # Offer.from_bytes(trade_make.offer), + # maker_offer, + # ) + # tx_records = await wallet_taker.wallet_state_manager.add_pending_transactions( + # tx_records, + # additional_signing_responses=signing_response, # ) - # await wallet_taker.wallet_state_manager.add_pending_transactions(tx_records) # await time_out_assert(15, full_node.txs_in_mempool, True, tx_records) # assert trade_take is not None # assert tx_records is not None @@ -1597,7 +1633,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=fee, secure=True ) - await wallet_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_maker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -1638,7 +1674,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=uint64(0), secure=True ) - await wallet_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_maker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -1692,7 +1728,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=uint64(0), secure=True ) - await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node.process_transaction_records(records=txs) @@ -1750,15 +1786,21 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: assert trade_make is not None peer = wallet_node_taker.get_full_node_peer() offer = Offer.from_bytes(trade_make.offer) + [offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers([offer]) tr1, txs1 = await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1) + txs1 = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + txs1, additional_signing_responses=signing_response + ) # we shouldn't be able to respond to a duplicate offer with pytest.raises(ValueError): await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CONFIRM, trade_manager_taker, tr1) # pushing into mempool while already in it should fail + [offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers([offer]) tr2, txs2 = await trade_manager_trader.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs2) + txs2 = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + txs2, additional_signing_responses=signing_response + ) assert await trade_manager_trader.get_coins_of_interest() offer_tx_records: List[TransactionRecord] = await wallet_node_maker.wallet_state_manager.tx_store.get_not_sent() await full_node.process_transaction_records(records=offer_tx_records) @@ -1816,7 +1858,7 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: bundle = dataclasses.replace(offer._bundle, aggregated_signature=G2Element()) offer = dataclasses.replace(offer, _bundle=bundle) tr1, txs1 = await trade_manager_taker.respond_to_offer(offer, peer, DEFAULT_TX_CONFIG, fee=uint64(10)) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1) + txs1 = await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1, sign=False) wallet_node_taker.wallet_tx_resend_timeout_secs = 0 # don't wait for resend def check_wallet_cache_empty() -> bool: @@ -1878,10 +1920,15 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: assert trade_make is not None peer = wallet_node_taker.get_full_node_peer() offer = Offer.from_bytes(trade_make.offer) + [offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) tr1, txs1 = await trade_manager_taker.respond_to_offer( offer, peer, DEFAULT_TX_CONFIG, fee=uint64(1000000000000) ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(txs1) + txs1 = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + txs1, additional_signing_responses=signing_response + ) await full_node.process_transaction_records(records=txs1) await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, tr1) diff --git a/tests/wallet/dao_wallet/test_dao_wallets.py b/tests/wallet/dao_wallet/test_dao_wallets.py index e0e4589caf95..2a07d36fcb00 100644 --- a/tests/wallet/dao_wallet/test_dao_wallets.py +++ b/tests/wallet/dao_wallet/test_dao_wallets.py @@ -161,7 +161,7 @@ async def test_dao_creation( ) assert dao_wallet_0 is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) await full_node_api.wait_transaction_records_entered_mempool(records=tx_queue, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -213,7 +213,7 @@ async def test_dao_creation( # Send some cats to the dao_cat lockup dao_cat_amt = uint64(100) txs = await dao_wallet_0.enter_dao_cat_voting_mode(dao_cat_amt, DEFAULT_TX_CONFIG) - await dao_wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await dao_wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -233,7 +233,7 @@ async def test_dao_creation( # send some cats from wallet_0 to wallet_1 so we can test voting cat_txs = await cat_wallet_0.generate_signed_transaction([cat_amt], [ph_1], DEFAULT_TX_CONFIG) - await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_txs) + cat_txs = await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_txs) await full_node_api.wait_transaction_records_entered_mempool(records=cat_txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -340,7 +340,7 @@ async def test_dao_funding( treasury_id = dao_wallet_0.dao_info.treasury_id # Get the full node sim to process the wallet creation spend - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) await full_node_api.wait_transaction_records_entered_mempool(records=tx_queue, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -354,7 +354,7 @@ async def test_dao_funding( xch_funds = uint64(500000) cat_funds = uint64(100000) funding_tx = await dao_wallet_0.create_add_funds_to_treasury_spend(xch_funds, DEFAULT_TX_CONFIG) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) + [funding_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[funding_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -365,7 +365,7 @@ async def test_dao_funding( cat_funding_tx = await dao_wallet_0.create_add_funds_to_treasury_spend( cat_funds, DEFAULT_TX_CONFIG, funding_wallet_id=cat_wallet_0.id() ) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([cat_funding_tx]) + [cat_funding_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([cat_funding_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[cat_funding_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -381,6 +381,7 @@ async def test_dao_funding( dao_cat_wallet_0 = dao_wallet_0.wallet_state_manager.wallets[dao_wallet_0.dao_info.dao_cat_wallet_id] dao_cat_0_bal = await dao_cat_wallet_0.get_votable_balance() txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dao_cat_0_bal, DEFAULT_TX_CONFIG) + txs = await dao_cat_wallet_0.wallet_state_manager.add_pending_transactions(txs) await dao_cat_wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -397,7 +398,7 @@ async def test_dao_funding( [None], ) proposal_tx = await dao_wallet_0.generate_new_proposal(xch_proposal_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -409,7 +410,7 @@ async def test_dao_funding( prop_0 = dao_wallet_0.dao_info.proposals_list[0] close_tx_0 = await dao_wallet_0.create_proposal_close_spend(prop_0.proposal_id, DEFAULT_TX_CONFIG) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([close_tx_0]) + [close_tx_0] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([close_tx_0]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx_0], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -535,7 +536,7 @@ async def test_dao_proposals( ) assert dao_wallet_0 is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) await full_node_api.wait_transaction_records_entered_mempool(records=tx_queue, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -579,7 +580,7 @@ async def test_dao_proposals( cat_tx = await cat_wallet_0.generate_signed_transaction( [cat_amt, cat_amt], [ph_1, ph_2], DEFAULT_TX_CONFIG, fee=base_fee ) - await wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) + cat_tx = await wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) await full_node_api.wait_transaction_records_entered_mempool(records=cat_tx, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -587,13 +588,14 @@ async def test_dao_proposals( # Lockup voting cats for all wallets dao_cat_0_bal = await dao_cat_wallet_0.get_votable_balance() txs_0 = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dao_cat_0_bal, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions(txs_0) + txs_0 = await wallet_0.wallet_state_manager.add_pending_transactions(txs_0) await full_node_api.wait_transaction_records_entered_mempool(records=txs_0, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) dao_cat_1_bal = await dao_cat_wallet_1.get_votable_balance() txs_1 = await dao_cat_wallet_1.enter_dao_cat_voting_mode(dao_cat_1_bal, DEFAULT_TX_CONFIG) + txs_1 = await wallet_1.wallet_state_manager.add_pending_transactions(txs_1) await wallet_1.wallet_state_manager.add_pending_transactions(txs_1) await full_node_api.wait_transaction_records_entered_mempool(records=txs_1, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) @@ -603,6 +605,7 @@ async def test_dao_proposals( dao_cat_2_bal = await dao_cat_wallet_2.get_votable_balance() txs_2 = await dao_cat_wallet_2.enter_dao_cat_voting_mode(dao_cat_2_bal, DEFAULT_TX_CONFIG) + txs_2 = await wallet_2.wallet_state_manager.add_pending_transactions(txs_2) await wallet_2.wallet_state_manager.add_pending_transactions(txs_2) await full_node_api.wait_transaction_records_entered_mempool(records=txs_2, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_2, timeout=60) @@ -617,7 +620,7 @@ async def test_dao_proposals( # Create funding spend so the treasury holds some XCH xch_funds = uint64(500000) funding_tx = await dao_wallet_0.create_add_funds_to_treasury_spend(xch_funds, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) + [funding_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[funding_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -643,7 +646,7 @@ async def test_dao_proposals( proposal_tx = await dao_wallet_0.generate_new_proposal( xch_proposal_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal, fee=base_fee ) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -667,7 +670,7 @@ async def test_dao_proposals( proposal_tx = await dao_wallet_0.generate_new_proposal( mint_proposal_inner, DEFAULT_TX_CONFIG, vote_amount=dao_cat_0_bal, fee=base_fee ) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -691,7 +694,7 @@ async def test_dao_proposals( assert current_innerpuz is not None update_inner = await generate_update_proposal_innerpuz(current_innerpuz, new_dao_rules) proposal_tx = await dao_wallet_0.generate_new_proposal(update_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -709,7 +712,7 @@ async def test_dao_proposals( proposal_tx = await dao_wallet_0.generate_new_proposal( xch_proposal_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal, fee=base_fee ) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -724,7 +727,7 @@ async def test_dao_proposals( proposal_tx = await dao_wallet_0.generate_new_proposal( xch_proposal_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal, fee=base_fee ) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -740,7 +743,7 @@ async def test_dao_proposals( vote_tx_1 = await dao_wallet_1.generate_proposal_vote_spend( prop_0.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG ) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_1]) + [vote_tx_1] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_1]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx_1], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -750,7 +753,7 @@ async def test_dao_proposals( vote_tx_2 = await dao_wallet_2.generate_proposal_vote_spend( prop_0.proposal_id, dao_cat_2_bal, False, DEFAULT_TX_CONFIG ) - await wallet_2.wallet_state_manager.add_pending_transactions([vote_tx_2]) + [vote_tx_2] = await wallet_2.wallet_state_manager.add_pending_transactions([vote_tx_2]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx_2], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -788,7 +791,7 @@ async def test_dao_proposals( # Proposal 0: Close close_tx_0 = await dao_wallet_0.create_proposal_close_spend(prop_0.proposal_id, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_0]) + [close_tx_0] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_0]) close_sb_0 = close_tx_0.spend_bundle assert close_sb_0 is not None await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, close_sb_0.name()) @@ -810,7 +813,7 @@ async def test_dao_proposals( vote_tx_1 = await dao_wallet_1.generate_proposal_vote_spend( prop_1.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG ) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_1]) + [vote_tx_1] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_1]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx_1], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -828,7 +831,7 @@ async def test_dao_proposals( assert prop_1_state["closable"] close_tx_1 = await dao_wallet_0.create_proposal_close_spend(prop_1.proposal_id, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_1]) + [close_tx_1] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_1]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx_1], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -841,7 +844,7 @@ async def test_dao_proposals( vote_tx_2 = await dao_wallet_1.generate_proposal_vote_spend( prop_2.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG ) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_2]) + [vote_tx_2] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_2]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx_2], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -859,7 +862,7 @@ async def test_dao_proposals( assert prop_2_state["closable"] close_tx_2 = await dao_wallet_0.create_proposal_close_spend(prop_2.proposal_id, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_2]) + [close_tx_2] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_2]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx_2], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -874,7 +877,7 @@ async def test_dao_proposals( vote_tx_3 = await dao_wallet_1.generate_proposal_vote_spend( prop_3.proposal_id, dao_cat_1_bal, False, DEFAULT_TX_CONFIG ) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_3]) + [vote_tx_3] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_3]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx_3], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -892,7 +895,7 @@ async def test_dao_proposals( assert prop_3_state["closable"] close_tx_3 = await dao_wallet_0.create_proposal_close_spend(prop_3.proposal_id, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_3]) + [close_tx_3] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_3]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx_3], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -911,7 +914,7 @@ async def test_dao_proposals( vote_tx_4 = await dao_wallet_1.generate_proposal_vote_spend( prop_4.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG ) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_4]) + [vote_tx_4] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx_4]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx_4], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -935,7 +938,7 @@ async def test_dao_proposals( close_tx_4 = await dao_wallet_0.create_proposal_close_spend( prop_4.proposal_id, DEFAULT_TX_CONFIG, self_destruct=True ) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_4]) + [close_tx_4] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx_4]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx_4], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -952,7 +955,7 @@ async def test_dao_proposals( await time_out_assert(20, len, 5, dao_wallet_0.dao_info.proposals_list) await dao_wallet_0.clear_finished_proposals_from_memory() free_tx = await dao_wallet_0.free_coins_from_finished_proposals(DEFAULT_TX_CONFIG, fee=uint64(100)) - await wallet_0.wallet_state_manager.add_pending_transactions([free_tx]) + [free_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([free_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[free_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -1039,7 +1042,7 @@ async def test_dao_proposal_partial_vote( assert dao_wallet_0 is not None # Get the full node sim to process the wallet creation spend - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) tx_record = tx_queue[0] await full_node_api.process_transaction_records(records=[tx_record]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash_0)) @@ -1069,7 +1072,7 @@ async def test_dao_proposal_partial_vote( xch_funds, DEFAULT_TX_CONFIG, ) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) + [funding_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) assert isinstance(funding_tx, TransactionRecord) funding_sb = funding_tx.spend_bundle assert isinstance(funding_sb, SpendBundle) @@ -1089,8 +1092,8 @@ async def test_dao_proposal_partial_vote( assert dao_cat_wallet_1 cat_tx = await cat_wallet_0.generate_signed_transaction([100000], [ph_1], DEFAULT_TX_CONFIG) + cat_tx = await wallet.wallet_state_manager.add_pending_transactions(cat_tx) cat_sb = cat_tx[0].spend_bundle - await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, cat_sb.name()) await full_node_api.process_transaction_records(records=cat_tx) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash_0)) @@ -1100,7 +1103,7 @@ async def test_dao_proposal_partial_vote( # Create dao cats for voting dao_cat_0_bal = await dao_cat_wallet_0.get_votable_balance() txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dao_cat_0_bal, DEFAULT_TX_CONFIG) - await dao_cat_wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await dao_cat_wallet_0.wallet_state_manager.add_pending_transactions(txs) dao_cat_sb = txs[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, dao_cat_sb.name()) await full_node_api.process_transaction_records(records=txs) @@ -1122,7 +1125,7 @@ async def test_dao_proposal_partial_vote( proposal_tx = await dao_wallet_0.generate_new_proposal( mint_proposal_inner, DEFAULT_TX_CONFIG, vote_amount=vote_amount, fee=uint64(1000) ) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) assert isinstance(proposal_tx, TransactionRecord) proposal_sb = proposal_tx.spend_bundle assert isinstance(proposal_sb, SpendBundle) @@ -1144,7 +1147,7 @@ async def test_dao_proposal_partial_vote( # Create votable dao cats and add a new vote dao_cat_1_bal = await dao_cat_wallet_1.get_votable_balance() txs = await dao_cat_wallet_1.enter_dao_cat_voting_mode(dao_cat_1_bal, DEFAULT_TX_CONFIG) - await wallet_1.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_1.wallet_state_manager.add_pending_transactions(txs) dao_cat_sb = txs[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, dao_cat_sb.name()) await full_node_api.process_transaction_records(records=txs) @@ -1155,7 +1158,7 @@ async def test_dao_proposal_partial_vote( vote_tx = await dao_wallet_1.generate_proposal_vote_spend( prop.proposal_id, dao_cat_1_bal // 2, True, DEFAULT_TX_CONFIG ) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx]) + [vote_tx] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx]) vote_sb = vote_tx.spend_bundle assert vote_sb is not None await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, vote_sb.name()) @@ -1175,7 +1178,7 @@ async def test_dao_proposal_partial_vote( try: close_tx = await dao_wallet_0.create_proposal_close_spend(prop.proposal_id, DEFAULT_TX_CONFIG, fee=uint64(100)) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([close_tx]) + [close_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([close_tx]) close_sb = close_tx.spend_bundle except Exception as e: # pragma: no cover print(e) @@ -1197,8 +1200,8 @@ async def test_dao_proposal_partial_vote( old_balance = await cat_wallet_0.get_spendable_balance() ph_0 = await cat_wallet_0.get_new_inner_hash() cat_tx = await cat_wallet_1.generate_signed_transaction([balance + new_mint_amount], [ph_0], DEFAULT_TX_CONFIG) + cat_tx = await wallet_1.wallet_state_manager.add_pending_transactions(cat_tx) cat_sb = cat_tx[0].spend_bundle - await wallet_1.wallet_state_manager.add_pending_transactions(cat_tx) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, cat_sb.name()) await full_node_api.process_transaction_records(records=cat_tx) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash_0)) @@ -2641,7 +2644,7 @@ async def test_dao_concurrency( assert dao_wallet_0 is not None # Get the full node sim to process the wallet creation spend - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) await full_node_api.wait_transaction_records_entered_mempool(records=tx_queue, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -2668,7 +2671,7 @@ async def test_dao_concurrency( # Create funding spends for xch xch_funds = uint64(500000) funding_tx = await dao_wallet_0.create_add_funds_to_treasury_spend(xch_funds, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) + [funding_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[funding_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -2697,7 +2700,7 @@ async def test_dao_concurrency( assert dao_cat_wallet_2 cat_tx = await cat_wallet_0.generate_signed_transaction([100000, 100000], [ph_1, ph_2], DEFAULT_TX_CONFIG) - await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) + cat_tx = await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) await full_node_api.wait_transaction_records_entered_mempool(records=cat_tx, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -2713,7 +2716,7 @@ async def test_dao_concurrency( dao_cat_0_bal = await dao_cat_wallet_0.get_votable_balance() assert dao_cat_0_bal == 100000 txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dao_cat_0_bal, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -2731,7 +2734,7 @@ async def test_dao_concurrency( proposal_tx = await dao_wallet_0.generate_new_proposal( xch_proposal_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal, uint64(1000) ) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -2756,7 +2759,7 @@ async def test_dao_concurrency( # Create votable dao cats and add a new vote dao_cat_1_bal = await dao_cat_wallet_1.get_votable_balance() txs = await dao_cat_wallet_1.enter_dao_cat_voting_mode(dao_cat_1_bal, DEFAULT_TX_CONFIG) - await wallet_1.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_1.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -2772,13 +2775,13 @@ async def test_dao_concurrency( await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=30) vote_tx = await dao_wallet_1.generate_proposal_vote_spend(prop.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx]) + [vote_tx] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx]) vote_sb = vote_tx.spend_bundle assert vote_sb is not None vote_tx_2 = await dao_wallet_2.generate_proposal_vote_spend( prop.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG ) - await wallet_2.wallet_state_manager.add_pending_transactions([vote_tx_2]) + [vote_tx_2] = await wallet_2.wallet_state_manager.add_pending_transactions([vote_tx_2]) vote_2 = vote_tx_2.spend_bundle assert vote_2 is not None await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, vote_sb.name()) @@ -3074,7 +3077,7 @@ async def test_dao_reorgs( assert dao_wallet_0 is not None # Get the full node sim to process the wallet creation spend - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) await full_node_api.wait_transaction_records_entered_mempool(records=tx_queue, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3116,7 +3119,7 @@ async def test_dao_reorgs( xch_funds, DEFAULT_TX_CONFIG, ) - await dao_wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) + [funding_tx] = await dao_wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[funding_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3147,7 +3150,7 @@ async def test_dao_reorgs( [ph_1], DEFAULT_TX_CONFIG, ) - await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) + cat_tx = await cat_wallet_0.wallet_state_manager.add_pending_transactions(cat_tx) await full_node_api.wait_transaction_records_entered_mempool(records=cat_tx, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3160,7 +3163,7 @@ async def test_dao_reorgs( dao_cat_0_bal = await dao_cat_wallet_0.get_votable_balance() assert dao_cat_0_bal == 200000 txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dao_cat_0_bal, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3178,7 +3181,7 @@ async def test_dao_reorgs( proposal_tx = await dao_wallet_0.generate_new_proposal( xch_proposal_inner, DEFAULT_TX_CONFIG, dao_cat_0_bal, uint64(1000) ) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3213,14 +3216,14 @@ async def test_dao_reorgs( # Create votable dao cats and add a new vote dao_cat_1_bal = await dao_cat_wallet_1.get_votable_balance() txs = await dao_cat_wallet_1.enter_dao_cat_voting_mode(dao_cat_1_bal, DEFAULT_TX_CONFIG) - await wallet_1.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_1.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=30) vote_tx = await dao_wallet_1.generate_proposal_vote_spend(prop.proposal_id, dao_cat_1_bal, True, DEFAULT_TX_CONFIG) - await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx]) + [vote_tx] = await wallet_1.wallet_state_manager.add_pending_transactions([vote_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3249,7 +3252,7 @@ async def test_dao_reorgs( await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=30) close_tx = await dao_wallet_0.create_proposal_close_spend(prop.proposal_id, DEFAULT_TX_CONFIG, fee=uint64(100)) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx]) + [close_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3350,7 +3353,7 @@ async def test_dao_votes( ) assert dao_wallet_0 is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) + tx_queue = await wallet_node_0.wallet_state_manager.add_pending_transactions(tx_queue) await full_node_api.wait_transaction_records_entered_mempool(records=tx_queue, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3371,31 +3374,31 @@ async def test_dao_votes( # Lockup voting cats for all wallets txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dc_1, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dc_2, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dc_3, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dc_4, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) txs = await dao_cat_wallet_0.enter_dao_cat_voting_mode(dc_5, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3405,7 +3408,7 @@ async def test_dao_votes( # Create funding spend so the treasury holds some XCH xch_funds = uint64(500000) funding_tx = await dao_wallet_0.create_add_funds_to_treasury_spend(xch_funds, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) + [funding_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([funding_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[funding_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3427,7 +3430,7 @@ async def test_dao_votes( vote_2 = uint64(150000) proposal_tx = await dao_wallet_0.generate_new_proposal(xch_proposal_inner, DEFAULT_TX_CONFIG, vote_1, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3438,7 +3441,7 @@ async def test_dao_votes( prop_0 = dao_wallet_0.dao_info.proposals_list[0] proposal_tx = await dao_wallet_0.generate_new_proposal(xch_proposal_inner, DEFAULT_TX_CONFIG, vote_2, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3448,7 +3451,7 @@ async def test_dao_votes( vote_3 = uint64(30000) vote_tx = await dao_wallet_0.generate_proposal_vote_spend(prop_0.proposal_id, vote_3, True, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([vote_tx]) + [vote_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([vote_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3457,7 +3460,7 @@ async def test_dao_votes( vote_4 = uint64(60000) vote_tx = await dao_wallet_0.generate_proposal_vote_spend(prop_0.proposal_id, vote_4, True, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([vote_tx]) + [vote_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([vote_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3466,7 +3469,7 @@ async def test_dao_votes( vote_5 = uint64(1) proposal_tx = await dao_wallet_0.generate_new_proposal(xch_proposal_inner, DEFAULT_TX_CONFIG, vote_5, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3478,7 +3481,7 @@ async def test_dao_votes( vote_6 = uint64(20000) for i in range(10): vote_tx = await dao_wallet_0.generate_proposal_vote_spend(prop_2.proposal_id, vote_6, True, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([vote_tx]) + [vote_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([vote_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[vote_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) @@ -3486,13 +3489,13 @@ async def test_dao_votes( assert dao_wallet_0.dao_info.proposals_list[2].amount_voted == 200001 close_tx = await dao_wallet_0.create_proposal_close_spend(prop_0.proposal_id, DEFAULT_TX_CONFIG) - await wallet_0.wallet_state_manager.add_pending_transactions([close_tx]) + [close_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([close_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[close_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) proposal_tx = await dao_wallet_0.generate_new_proposal(xch_proposal_inner, DEFAULT_TX_CONFIG, fee=base_fee) - await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) + [proposal_tx] = await wallet_0.wallet_state_manager.add_pending_transactions([proposal_tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[proposal_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) diff --git a/tests/wallet/db_wallet/test_dl_offers.py b/tests/wallet/db_wallet/test_dl_offers.py index c35bf4f33d4e..0be5fbcefbc7 100644 --- a/tests/wallet/db_wallet/test_dl_offers.py +++ b/tests/wallet/db_wallet/test_dl_offers.py @@ -68,7 +68,7 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: maker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_maker.get_latest_singleton(launcher_id_maker) is not None - await wsm_maker.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_maker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) maker_funds -= fee maker_funds -= 1 @@ -78,7 +78,7 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: taker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_taker.get_latest_singleton(launcher_id_taker) is not None - await wsm_taker.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_taker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) taker_funds -= fee taker_funds -= 1 @@ -141,8 +141,11 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ] } + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(offer_maker.offer)] + ) offer_taker, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(offer_maker.offer), + maker_offer, peer, DEFAULT_TX_CONFIG, solver=Solver( @@ -172,7 +175,9 @@ async def test_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ), fee=fee, ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert offer_taker is not None assert tx_records is not None @@ -231,7 +236,7 @@ async def is_singleton_generation(wallet: DataLayerWallet, launcher_id: bytes32, await time_out_assert(15, is_singleton_generation, True, dl_wallet_taker, launcher_id_taker, 2) txs = await dl_wallet_taker.create_update_state_spend(launcher_id_taker, bytes32([2] * 32), DEFAULT_TX_CONFIG) - await wallet_node_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_node_taker.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) @@ -253,11 +258,11 @@ async def test_dl_offer_cancellation(wallets_prefarm: Any, trusted: bool) -> Non dl_record, std_record, launcher_id = await dl_wallet.generate_new_reporter(root, DEFAULT_TX_CONFIG) assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wsm.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed_and_root, True, dl_wallet, launcher_id, root) dl_record_2, std_record_2, launcher_id_2 = await dl_wallet.generate_new_reporter(root, DEFAULT_TX_CONFIG) - await wsm.add_pending_transactions([dl_record_2, std_record_2]) + [dl_record_2, std_record_2] = await wsm.add_pending_transactions([dl_record_2, std_record_2]) await full_node_api.process_transaction_records(records=[dl_record_2, std_record_2]) trade_manager = wsm.trade_manager @@ -291,7 +296,7 @@ async def test_dl_offer_cancellation(wallets_prefarm: Any, trusted: bool) -> Non cancellation_txs = await trade_manager.cancel_pending_offers( [offer.trade_id], DEFAULT_TX_CONFIG, fee=uint64(2_000_000_000_000), secure=True ) - await trade_manager.wallet_state_manager.add_pending_transactions(cancellation_txs) + cancellation_txs = await trade_manager.wallet_state_manager.add_pending_transactions(cancellation_txs) assert len(cancellation_txs) == 2 await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager, offer) await full_node_api.process_transaction_records(records=cancellation_txs) @@ -328,7 +333,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: maker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_maker.get_latest_singleton(launcher_id_maker_1) is not None - await wsm_maker.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_maker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) maker_funds -= fee maker_funds -= 1 @@ -337,7 +342,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: maker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_maker.get_latest_singleton(launcher_id_maker_2) is not None - await wsm_maker.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_maker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) maker_funds -= fee maker_funds -= 1 @@ -347,7 +352,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: taker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_taker.get_latest_singleton(launcher_id_taker_1) is not None - await wsm_taker.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_taker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) taker_funds -= fee taker_funds -= 1 @@ -356,7 +361,7 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: taker_root, DEFAULT_TX_CONFIG, fee=fee ) assert await dl_wallet_taker.get_latest_singleton(launcher_id_taker_2) is not None - await wsm_taker.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_taker.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) taker_funds -= fee taker_funds -= 1 @@ -421,8 +426,11 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: assert success is True assert offer_maker is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(offer_maker.offer)] + ) offer_taker, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(offer_maker.offer), + maker_offer, peer, DEFAULT_TX_CONFIG, solver=Solver( @@ -465,7 +473,9 @@ async def test_multiple_dl_offers(wallets_prefarm: Any, trusted: bool) -> None: ), fee=fee, ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert offer_taker is not None assert tx_records is not None diff --git a/tests/wallet/db_wallet/test_dl_wallet.py b/tests/wallet/db_wallet/test_dl_wallet.py index bfd2a47bc720..9cfb97b77392 100644 --- a/tests/wallet/db_wallet/test_dl_wallet.py +++ b/tests/wallet/db_wallet/test_dl_wallet.py @@ -80,7 +80,9 @@ async def test_initial_creation( assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wallet_node_0.wallet_state_manager.add_pending_transactions( + [dl_record, std_record] + ) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -132,7 +134,9 @@ async def test_get_owned_singletons( assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wallet_node_0.wallet_state_manager.add_pending_transactions( + [dl_record, std_record] + ) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -190,7 +194,9 @@ async def test_tracking_non_owned( assert await dl_wallet_0.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wallet_node_0.wallet_state_manager.add_pending_transactions( + [dl_record, std_record] + ) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet_0, launcher_id) @@ -204,7 +210,7 @@ async def test_tracking_non_owned( new_root = MerkleTree([Program.to("root").get_tree_hash()]).calculate_root() txs = await dl_wallet_0.create_update_state_spend(launcher_id, new_root, DEFAULT_TX_CONFIG) - await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet_0, launcher_id) @@ -260,7 +266,9 @@ async def test_lifecycle( assert await dl_wallet.get_latest_singleton(launcher_id) is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wallet_node_0.wallet_state_manager.add_pending_transactions( + [dl_record, std_record] + ) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -295,7 +303,7 @@ async def test_lifecycle( assert new_record != previous_record assert not new_record.confirmed - await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -316,7 +324,7 @@ async def test_lifecycle( assert new_record != previous_record assert not new_record.confirmed - await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, is_singleton_confirmed, True, dl_wallet, launcher_id) @@ -381,7 +389,9 @@ async def is_singleton_confirmed(wallet: DataLayerWallet, lid: bytes32) -> bool: initial_record = await dl_wallet_0.get_latest_singleton(launcher_id) assert initial_record is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wallet_node_0.wallet_state_manager.add_pending_transactions( + [dl_record, std_record] + ) await asyncio.wait_for( full_node_api.process_transaction_records(records=[dl_record, std_record]), timeout=adjusted_timeout(timeout=15), @@ -412,14 +422,14 @@ async def is_singleton_confirmed(wallet: DataLayerWallet, lid: bytes32) -> bool: assert initial_record != record_0 assert record_0 != record_1 - await wallet_node_1.wallet_state_manager.add_pending_transactions(report_txs) + report_txs = await wallet_node_1.wallet_state_manager.add_pending_transactions(report_txs) await asyncio.wait_for( full_node_api.wait_transaction_records_entered_mempool(records=report_txs), timeout=adjusted_timeout(timeout=15), ) - await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs) + update_txs = await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs) await asyncio.wait_for( full_node_api.process_transaction_records(records=report_txs), timeout=adjusted_timeout(timeout=15) @@ -464,7 +474,7 @@ async def is_singleton_generation(wallet: DataLayerWallet, launcher_id: bytes32, ) record_1 = await dl_wallet_0.get_latest_singleton(launcher_id) assert record_1 is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs_1) + update_txs_1 = await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs_1) await full_node_api.wait_transaction_records_entered_mempool(update_txs_1) # Delete any trace of that update @@ -477,7 +487,7 @@ async def is_singleton_generation(wallet: DataLayerWallet, launcher_id: bytes32, assert record_0 is not None assert record_0 != record_1 - await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs_0) + update_txs_0 = await wallet_node_0.wallet_state_manager.add_pending_transactions(update_txs_0) await asyncio.wait_for( full_node_api.process_transaction_records(records=update_txs_1), timeout=adjusted_timeout(timeout=15) @@ -542,13 +552,13 @@ async def test_mirrors(wallets_prefarm: Any, trusted: bool) -> None: dl_record, std_record, launcher_id_1 = await dl_wallet_1.generate_new_reporter(bytes32([0] * 32), DEFAULT_TX_CONFIG) assert await dl_wallet_1.get_latest_singleton(launcher_id_1) is not None - await wsm_1.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_1.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed_and_root, True, dl_wallet_1, launcher_id_1, bytes32([0] * 32)) dl_record, std_record, launcher_id_2 = await dl_wallet_2.generate_new_reporter(bytes32([0] * 32), DEFAULT_TX_CONFIG) assert await dl_wallet_2.get_latest_singleton(launcher_id_2) is not None - await wsm_2.add_pending_transactions([dl_record, std_record]) + [dl_record, std_record] = await wsm_2.add_pending_transactions([dl_record, std_record]) await full_node_api.process_transaction_records(records=[dl_record, std_record]) await time_out_assert(15, is_singleton_confirmed_and_root, True, dl_wallet_2, launcher_id_2, bytes32([0] * 32)) @@ -563,7 +573,7 @@ async def test_mirrors(wallets_prefarm: Any, trusted: bool) -> None: launcher_id_2, uint64(3), [b"foo", b"bar"], DEFAULT_TX_CONFIG, fee=uint64(1_999_999_999_999) ) additions: List[Coin] = [] - await wsm_1.add_pending_transactions(txs) + txs = await wsm_1.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: additions.extend(tx.spend_bundle.additions()) @@ -579,7 +589,7 @@ async def test_mirrors(wallets_prefarm: Any, trusted: bool) -> None: ) txs = await dl_wallet_1.delete_mirror(mirror.coin_id, peer_1, DEFAULT_TX_CONFIG, fee=uint64(2_000_000_000_000)) - await wsm_1.add_pending_transactions(txs) + txs = await wsm_1.add_pending_transactions(txs) await full_node_api.process_transaction_records(records=txs) await time_out_assert(15, dl_wallet_1.get_mirrors_for_launcher, [], launcher_id_2) diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index abf17724abb2..9f7dce4c0f1c 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -210,7 +210,7 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes message_tx, message_spend_bundle, attest_data = await did_wallet_0.create_attestment( did_wallet_2.did_info.temp_coin.name(), newpuzhash, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet_0.wallet_state_manager.add_pending_transactions([message_tx]) + [message_tx] = await did_wallet_0.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_0.id() @@ -235,7 +235,7 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes test_message_spend_bundle, ) assert txs[0].spend_bundle is not None - await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 5, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() @@ -251,7 +251,7 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes some_ph = 32 * b"\2" txs = await did_wallet_2.create_exit_spend(some_ph, DEFAULT_TX_CONFIG) - await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_2.id() @@ -378,7 +378,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w message_tx, message_spend_bundle, attest1 = await did_wallet.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet.wallet_state_manager.add_pending_transactions([message_tx]) + [message_tx] = await did_wallet.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -387,7 +387,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w message_tx2, message_spend_bundle2, attest2 = await did_wallet_2.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet_2.wallet_state_manager.add_pending_transactions([message_tx2]) + [message_tx2] = await did_wallet_2.wallet_state_manager.add_pending_transactions([message_tx2]) assert message_spend_bundle2 is not None spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_2.id() @@ -407,7 +407,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w await time_out_assert(15, did_wallet_4.get_confirmed_balance, 0) await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 0) txs = await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, message_spend_bundle) - await did_wallet_4.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_4.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_4.id() ) @@ -538,7 +538,7 @@ async def test_did_find_lost_did(self, self_hostname, two_wallet_nodes, trusted) await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list txs = await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) - await did_wallet.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) spend_bundle = spend_bundle_list[0].spend_bundle await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) @@ -619,7 +619,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list txs = await did_wallet.create_update_spend(DEFAULT_TX_CONFIG) - await did_wallet.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -649,7 +649,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, message_tx, message_spend_bundle, attest_data = await did_wallet.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet.wallet_state_manager.add_pending_transactions([message_tx]) + [message_tx] = await did_wallet.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) @@ -662,7 +662,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, message_spend_bundle, ) = await did_wallet_3.load_attest_files_for_recovery_spend([attest_data]) txs = await did_wallet_3.recovery_spend(coin, new_ph, info, pubkey, message_spend_bundle) - await did_wallet_3.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_3.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_3.id() ) @@ -692,7 +692,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, message_tx, message_spend_bundle, attest1 = await did_wallet_3.create_attestment( coin.name(), new_ph, pubkey, DEFAULT_TX_CONFIG ) - await did_wallet_3.wallet_state_manager.add_pending_transactions([message_tx]) + [message_tx] = await did_wallet_3.wallet_state_manager.add_pending_transactions([message_tx]) assert message_spend_bundle is not None spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_3.id() @@ -707,7 +707,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, test_message_spend_bundle, ) = await did_wallet_4.load_attest_files_for_recovery_spend([attest1]) txs = await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, test_message_spend_bundle) - await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_2.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_4.id() @@ -783,8 +783,7 @@ async def test_did_transfer(self, self_hostname, two_wallet_nodes, with_recovery # Transfer DID new_puzhash = await wallet2.get_new_puzzlehash() txs = await did_wallet_1.transfer_did(new_puzhash, fee, with_recovery, DEFAULT_TX_CONFIG) - for tx in txs: - await did_wallet_1.wallet_state_manager.add_pending_transactions([tx]) + txs = await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) @@ -861,7 +860,7 @@ async def test_update_recovery_list(self, self_hostname, two_wallet_nodes, trust await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) await did_wallet_1.update_recovery_list([bytes(ph)], 1) txs = await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG) - await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) await full_node_api.farm_blocks_to_wallet(1, wallet) await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) @@ -946,7 +945,7 @@ async def test_get_info(self, self_hostname, two_wallet_nodes, trusted): ), fee, ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.process_transaction_records(records=[tx]) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=15) @@ -1064,7 +1063,7 @@ async def test_update_metadata(self, self_hostname, two_wallet_nodes, trusted): metadata["Twitter"] = "http://www.twitter.com" await did_wallet_1.update_metadata(metadata) txs = await did_wallet_1.create_update_spend(DEFAULT_TX_CONFIG, fee) - await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) transaction_records = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) @@ -1338,7 +1337,7 @@ async def test_did_resync(self, self_hostname, two_wallet_nodes, trusted) -> Non # Transfer DID new_puzhash = await wallet2.get_new_puzzlehash() txs = await did_wallet_1.transfer_did(new_puzhash, fee, True, tx_config=DEFAULT_TX_CONFIG) - await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet_1.wallet_state_manager.add_pending_transactions(txs) spend_bundle_list = await wallet_node_1.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( did_wallet_1.id() ) diff --git a/tests/wallet/nft_wallet/test_nft_1_offers.py b/tests/wallet/nft_wallet/test_nft_1_offers.py index ce81cff18104..11d533b4f211 100644 --- a/tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/tests/wallet/nft_wallet/test_nft_1_offers.py @@ -135,7 +135,7 @@ async def test_nft_offer_sell_nft( royalty_basis_pts, did_id, ) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -180,10 +180,15 @@ async def test_nft_offer_sell_nft( assert not mempool_not_empty(full_node_api) peer = wallet_node_taker.get_full_node_peer() + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + maker_offer, peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) @@ -275,7 +280,7 @@ async def test_nft_offer_request_nft( royalty_basis_pts, did_id, ) - await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -322,10 +327,15 @@ async def test_nft_offer_request_nft( taker_fee = 1 peer = wallet_node_taker.get_full_node_peer() + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + maker_offer, peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None @@ -414,7 +424,7 @@ async def test_nft_offer_sell_did_to_did( royalty_basis_pts, did_id, ) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -476,10 +486,15 @@ async def test_nft_offer_sell_did_to_did( taker_fee = 1 peer = wallet_node_taker.get_full_node_peer() + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + maker_offer, peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -574,7 +589,7 @@ async def test_nft_offer_sell_nft_for_cat( royalty_basis_pts, did_id, ) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -634,7 +649,7 @@ async def test_nft_offer_sell_nft_for_cat( DEFAULT_TX_CONFIG, memos=[[ph_taker_cat_1], [ph_taker_cat_2]], ) - await wallet_maker.wallet_state_manager.add_pending_transactions(cat_tx_records) + cat_tx_records = await wallet_maker.wallet_state_manager.add_pending_transactions(cat_tx_records) await full_node_api.process_transaction_records(records=cat_tx_records) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -661,10 +676,15 @@ async def test_nft_offer_sell_nft_for_cat( taker_fee = 1 peer = wallet_node_taker.get_full_node_peer() + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + maker_offer, peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -756,7 +776,7 @@ async def test_nft_offer_request_nft_for_cat( royalty_basis_pts, did_id, ) - await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -820,7 +840,7 @@ async def test_nft_offer_request_nft_for_cat( amounts.append(uint64(extra_change)) puzzle_hashes.append(ph_taker_cat_1) cat_tx_records = await cat_wallet_maker.generate_signed_transaction(amounts, puzzle_hashes, DEFAULT_TX_CONFIG) - await wallet_maker.wallet_state_manager.add_pending_transactions(cat_tx_records) + cat_tx_records = await wallet_maker.wallet_state_manager.add_pending_transactions(cat_tx_records) await full_node_api.process_transaction_records(records=cat_tx_records) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=20) @@ -852,10 +872,15 @@ async def test_nft_offer_request_nft_for_cat( taker_fee = 1 peer = wallet_node_taker.get_full_node_peer() + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + maker_offer, peer, DEFAULT_TX_CONFIG, fee=uint64(taker_fee) + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) await time_out_assert(20, mempool_not_empty, True, full_node_api) assert trade_take is not None assert tx_records is not None @@ -950,7 +975,7 @@ async def test_nft_offer_sell_cancel( royalty_basis_pts, did_id, ) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -984,7 +1009,7 @@ async def test_nft_offer_sell_cancel( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=FEE, secure=True ) - await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) @@ -1070,7 +1095,7 @@ async def test_nft_offer_sell_cancel_in_batch( royalty_basis_pts, did_id, ) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -1105,7 +1130,7 @@ async def test_nft_offer_sell_cancel_in_batch( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=FEE, secure=True ) - await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus: trade_rec = await trade_manager.get_trade_by_id(trade.trade_id) @@ -1270,7 +1295,7 @@ async def test_complex_nft_offer( uint16(royalty_basis_pts_maker), did_id_maker, ) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -1285,7 +1310,7 @@ async def test_complex_nft_offer( royalty_basis_pts_taker_1, did_id_taker, ) - await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -1312,7 +1337,7 @@ async def test_complex_nft_offer( royalty_basis_pts_taker_2, did_id_taker, ) - await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -1369,10 +1394,13 @@ async def test_complex_nft_offer( assert success assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) if royalty_basis_pts_maker == 10000: with pytest.raises(ValueError): trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, wallet_node_taker.get_full_node_peer(), DEFAULT_TX_CONFIG, fee=FEE, @@ -1381,12 +1409,14 @@ async def test_complex_nft_offer( return else: trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, wallet_node_taker.get_full_node_peer(), DEFAULT_TX_CONFIG, fee=FEE, ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert trade_take is not None assert tx_records is not None await full_node_api.process_transaction_records(records=tx_records) @@ -1482,13 +1512,18 @@ async def get_cat_wallet_and_check_balance(asset_id: str, wsm: Any) -> uint128: assert success assert trade_make is not None + [maker_offer], signing_response = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, wallet_node_taker.get_full_node_peer(), DEFAULT_TX_CONFIG, fee=uint64(0), ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert trade_take is not None assert tx_records is not None await time_out_assert(20, mempool_not_empty, True, full_node_api) diff --git a/tests/wallet/nft_wallet/test_nft_bulk_mint.py b/tests/wallet/nft_wallet/test_nft_bulk_mint.py index e2d0ae649240..95f2f4870307 100644 --- a/tests/wallet/nft_wallet/test_nft_bulk_mint.py +++ b/tests/wallet/nft_wallet/test_nft_bulk_mint.py @@ -118,9 +118,10 @@ async def test_nft_mint_from_did( sb = tx_records[0].spend_bundle assert sb is not None - await api_0.push_tx({"spend_bundle": bytes(sb).hex()}) + bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) + await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await time_out_assert(30, nft_count, mint_total, nft_wallet_taker) @@ -617,9 +618,10 @@ async def test_nft_mint_from_did_multiple_xch( sb = tx_records[0].spend_bundle assert sb is not None - await api_0.push_tx({"spend_bundle": bytes(sb).hex()}) + bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) + await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await time_out_assert(30, nft_count, mint_total, nft_wallet_taker) @@ -718,9 +720,10 @@ async def test_nft_mint_from_xch( sb = tx_records[0].spend_bundle assert sb is not None - await api_0.push_tx({"spend_bundle": bytes(sb).hex()}) + bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) + await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await time_out_assert(30, nft_count, mint_total, nft_wallet_taker) @@ -1033,9 +1036,10 @@ async def test_nft_mint_from_xch_multiple_xch( sb = tx_records[0].spend_bundle assert sb is not None - await api_0.push_tx({"spend_bundle": bytes(sb).hex()}) + bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb) + await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) - await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) await time_out_assert(30, nft_count, mint_total, nft_wallet_taker) diff --git a/tests/wallet/nft_wallet/test_nft_offers.py b/tests/wallet/nft_wallet/test_nft_offers.py index 683ffea27025..dd707fe111cf 100644 --- a/tests/wallet/nft_wallet/test_nft_offers.py +++ b/tests/wallet/nft_wallet/test_nft_offers.py @@ -103,7 +103,7 @@ async def test_nft_offer_with_fee( ) txs = await nft_wallet_maker.generate_new_nft(metadata, tx_config) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -144,14 +144,19 @@ async def test_nft_offer_with_fee( taker_fee = uint64(1) + [maker_offer], signing_response = await wallet_node_0.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) peer = wallet_node_1.get_full_node_peer() trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, tx_config, fee=taker_fee, ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert trade_take is not None assert tx_records is not None @@ -216,10 +221,13 @@ async def test_nft_offer_with_fee( taker_fee = uint64(1) - trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, tx_config, fee=taker_fee + [maker_offer], signing_response = await wallet_node_0.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) + trade_take, tx_records = await trade_manager_taker.respond_to_offer(maker_offer, peer, tx_config, fee=taker_fee) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -300,7 +308,7 @@ async def test_nft_offer_cancellations( ) txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -340,7 +348,7 @@ async def test_nft_offer_cancellations( txs = await trade_manager_maker.cancel_pending_offers( [trade_make.trade_id], DEFAULT_TX_CONFIG, fee=cancel_fee, secure=True ) - await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await trade_manager_maker.wallet_state_manager.add_pending_transactions(txs) await time_out_assert(20, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make) await full_node_api.process_transaction_records(records=txs) @@ -422,7 +430,7 @@ async def test_nft_offer_with_metadata_update( ) txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -442,7 +450,7 @@ async def test_nft_offer_with_metadata_update( fee_for_update = uint64(10) txs = await nft_wallet_maker.update_metadata(nft_to_update, key, url_to_add, DEFAULT_TX_CONFIG, fee=fee_for_update) mempool_mgr = full_node_api.full_node.mempool_manager - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none(20, mempool_mgr.get_spendbundle, tx.spend_bundle.name()) @@ -478,11 +486,16 @@ async def test_nft_offer_with_metadata_update( taker_fee = uint64(1) + [maker_offer], signing_response = await wallet_node_0.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) peer = wallet_node_1.get_full_node_peer() trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=taker_fee + maker_offer, peer, DEFAULT_TX_CONFIG, fee=taker_fee + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -569,7 +582,7 @@ async def test_nft_offer_nft_for_cat( ) txs = await nft_wallet_maker.generate_new_nft(metadata, tx_config) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -653,14 +666,19 @@ async def test_nft_offer_nft_for_cat( taker_fee = uint64(1) + [maker_offer], signing_response = await wallet_node_0.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) peer = wallet_node_1.get_full_node_peer() trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), + maker_offer, peer, tx_config, fee=taker_fee, ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response + ) assert trade_take is not None assert tx_records is not None @@ -737,10 +755,13 @@ async def test_nft_offer_nft_for_cat( taker_fee = uint64(1) - trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, tx_config, fee=taker_fee + [maker_offer], signing_response = await wallet_node_0.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) + trade_take, tx_records = await trade_manager_taker.respond_to_offer(maker_offer, peer, tx_config, fee=taker_fee) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None @@ -829,7 +850,7 @@ async def test_nft_offer_nft_for_nft( ) txs = await nft_wallet_maker.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -844,7 +865,7 @@ async def test_nft_offer_nft_for_nft( ) txs = await nft_wallet_taker.generate_new_nft(metadata_2, DEFAULT_TX_CONFIG) - await nft_wallet_maker.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_taker.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -887,11 +908,16 @@ async def test_nft_offer_nft_for_nft( taker_fee = uint64(1) + [maker_offer], signing_response = await wallet_node_0.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make.offer)] + ) peer = wallet_node_1.get_full_node_peer() trade_take, tx_records = await trade_manager_taker.respond_to_offer( - Offer.from_bytes(trade_make.offer), peer, DEFAULT_TX_CONFIG, fee=taker_fee + maker_offer, peer, DEFAULT_TX_CONFIG, fee=taker_fee + ) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, additional_signing_responses=signing_response ) - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) assert trade_take is not None assert tx_records is not None diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py index 953e7f8e7c20..9f25cf83c60d 100644 --- a/tests/wallet/nft_wallet/test_nft_wallet.py +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -142,7 +142,7 @@ async def test_nft_wallet_creation_automatically(self_hostname: str, two_wallet_ ) txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: await time_out_assert_not_none( @@ -160,8 +160,8 @@ async def test_nft_wallet_creation_automatically(self_hostname: str, two_wallet_ [uint64(coins[0].coin.amount)], [ph1], DEFAULT_TX_CONFIG, coins={coins[0].coin} ) assert len(txs) == 1 + txs = await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) assert txs[0].spend_bundle is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() ) @@ -241,7 +241,7 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n await time_out_assert(30, wallet_0.get_unconfirmed_balance, 2000000000000) await time_out_assert(30, wallet_0.get_confirmed_balance, 2000000000000) txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -277,7 +277,7 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n await time_out_assert(10, wallet_0.get_confirmed_balance, 4000000000000) txs = await nft_wallet_0.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) + txs = await nft_wallet_0.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -300,8 +300,8 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n [uint64(coins[1].coin.amount)], [ph1], DEFAULT_TX_CONFIG, coins={coins[1].coin} ) assert len(txs) == 1 + txs = await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) assert txs[0].spend_bundle is not None - await wallet_node_0.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() ) @@ -322,8 +322,8 @@ async def test_nft_wallet_creation_and_transfer(self_hostname: str, two_wallet_n [uint64(coins[0].coin.amount)], [ph], DEFAULT_TX_CONFIG, coins={coins[0].coin} ) assert len(txs) == 1 + txs = await wallet_node_1.wallet_state_manager.add_pending_transactions(txs) assert txs[0].spend_bundle is not None - await wallet_node_1.wallet_state_manager.add_pending_transactions(txs) await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() ) @@ -1012,8 +1012,8 @@ async def test_nft_transfer_nft_with_did(self_hostname: str, two_wallet_nodes: A await full_node_api.wait_for_wallet_synced(wallet_node_1, 20) # transfer DID to the other wallet txs = await did_wallet.transfer_did(ph1, uint64(0), True, DEFAULT_TX_CONFIG) + txs = await did_wallet.wallet_state_manager.add_pending_transactions(txs) for tx in txs: - await did_wallet.wallet_state_manager.add_pending_transactions(txs) if tx.spend_bundle is not None: await time_out_assert_not_none( 30, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() @@ -1064,7 +1064,7 @@ async def test_nft_transfer_nft_with_did(self_hostname: str, two_wallet_nodes: A dict(wallet_id=nft_wallet_id_1, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex(), fee=fee) ) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - await did_wallet.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_node_1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( @@ -1635,7 +1635,7 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A dict(wallet_id=nft_wallet_0_id, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex()) ) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - await did_wallet1.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( 30, api_0.nft_get_by_did, [dict(did_id=hmr_did_id)], lambda x: x.get("wallet_id", 0) > 0 @@ -1667,7 +1667,7 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A dict(wallet_id=nft_wallet_1_id, did_id=hmr_did_id, nft_coin_id=nft_coin_id.hex()) ) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - await did_wallet1.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) coins_response = await wait_rpc_state_condition( 30, api_0.nft_get_by_did, [dict(did_id=hmr_did_id)], lambda x: x.get("wallet_id") is not None @@ -1692,7 +1692,7 @@ async def test_nft_set_did(self_hostname: str, two_wallet_nodes: Any, trusted: A # Test set DID2 -> None resp = await api_0.nft_set_nft_did(dict(wallet_id=nft_wallet_2_id, nft_coin_id=nft_coin_id.hex())) txs = [TransactionRecord.from_json_dict_convenience(tx) for tx in resp["transactions"]] - await did_wallet1.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet1.wallet_state_manager.add_pending_transactions(txs) await make_new_block_with(resp, full_node_api, ph) # Check NFT DID diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index fa92f8336230..67d0731c8967 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -457,7 +457,7 @@ async def test_get_farmed_amount_with_fee(wallet_rpc_environment: WalletRpcTestE tx_config=DEFAULT_TX_CONFIG, fee=uint64(fee_amount), ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) our_ph = await wallet.get_new_puzzlehash() await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index a73c20a23b10..18c08d3b29ab 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -191,7 +191,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): spent_coin = tx_record.spend_bundle.removals()[0] assert spent_coin.puzzle_hash == puzzle_hash - await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) + [tx_record] = await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -203,7 +203,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): [tx_record] = await wallet.generate_signed_transaction( uint64(10), SINGLETON_LAUNCHER_HASH, DEFAULT_TX_CONFIG, uint64(0) ) - await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) + [tx_record] = await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -211,7 +211,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): # Send a transaction to make sure the wallet is still running [tx_record] = await wallet.generate_signed_transaction(uint64(10), junk_ph, DEFAULT_TX_CONFIG, uint64(0)) - await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) + [tx_record] = await wallet_node.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -273,7 +273,7 @@ async def test_subscribe_for_coin_id(self, simulator_and_wallet, self_hostname): [tx_record] = await standard_wallet.generate_signed_transaction( uint64(10), puzzle_hash, DEFAULT_TX_CONFIG, uint64(0), coins=coins ) - await standard_wallet.wallet_state_manager.add_pending_transactions([tx_record]) + [tx_record] = await standard_wallet.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) @@ -312,7 +312,7 @@ async def test_subscribe_for_coin_id(self, simulator_and_wallet, self_hostname): data_response: RespondToCoinUpdates = RespondToCoinUpdates.from_bytes(msg_response.data) assert len(data_response.coin_states) == 0 - await standard_wallet.wallet_state_manager.add_pending_transactions([tx_record]) + [tx_record] = await standard_wallet.wallet_state_manager.add_pending_transactions([tx_record]) await full_node_api.process_transaction_records(records=[tx_record]) diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index ff60d0569795..63ece68a1a38 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -520,8 +520,9 @@ async def test_request_additions_success(self, simulator_and_wallet, self_hostna payees.append(Payment(payee_ph, uint64(i + 200))) [tx] = await wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) - + await full_node_api.wait_transaction_records_entered_mempool([tx]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) last_block: Optional[BlockRecord] = full_node_api.full_node.blockchain.get_peak() @@ -727,6 +728,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) await full_node_api.wait_transaction_records_entered_mempool([tx]) await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20) @@ -785,6 +787,7 @@ async def test_dusted_wallet( if dust_remaining % 100 == 0 and dust_remaining != new_dust: # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -805,6 +808,7 @@ async def test_dusted_wallet( if new_dust >= 1: # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -852,6 +856,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -891,6 +896,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -950,6 +956,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -986,6 +993,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await dust_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await dust_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -1029,6 +1037,7 @@ async def test_dusted_wallet( if coins_remaining % 100 == 0 and coins_remaining != spam_filter_after_n_txs: # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) await full_node_api.wait_transaction_records_entered_mempool([tx]) await full_node_api.wait_for_wallets_synced( @@ -1045,6 +1054,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await farm_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await farm_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -1074,6 +1084,7 @@ async def test_dusted_wallet( # construct and send tx [tx] = await dust_wallet.generate_signed_transaction(uint64(0), ph, DEFAULT_TX_CONFIG, primaries=payees) + [tx] = await dust_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.send_transaction(SendTransaction(tx.spend_bundle)) # advance the chain and sync both wallets @@ -1122,7 +1133,7 @@ async def test_dusted_wallet( ] ) txs = await farm_nft_wallet.generate_new_nft(metadata, DEFAULT_TX_CONFIG) - await farm_nft_wallet.wallet_state_manager.add_pending_transactions(txs) + txs = await farm_nft_wallet.wallet_state_manager.add_pending_transactions(txs) for tx in txs: if tx.spend_bundle is not None: assert compute_memos(tx.spend_bundle) @@ -1157,7 +1168,7 @@ async def test_dusted_wallet( ) assert len(txs) == 1 assert txs[0].spend_bundle is not None - await farm_wallet_node.wallet_state_manager.add_pending_transactions(txs) + txs = await farm_wallet_node.wallet_state_manager.add_pending_transactions(txs) assert compute_memos(txs[0].spend_bundle) # Farm a new block. @@ -1292,7 +1303,7 @@ async def assert_coin_state_retry() -> None: [tx] = await wallet.generate_signed_transaction( 1_000_000_000_000, bytes32([0] * 32), DEFAULT_TX_CONFIG, memos=[ph] ) - await wallet_node.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_node.wallet_state_manager.add_pending_transactions([tx]) async def tx_in_mempool(): return full_node_api.full_node.mempool_manager.get_spendbundle(tx.name) is not None diff --git a/tests/wallet/test_notifications.py b/tests/wallet/test_notifications.py index 58bed62e63da..05292fbacc7f 100644 --- a/tests/wallet/test_notifications.py +++ b/tests/wallet/test_notifications.py @@ -138,7 +138,7 @@ async def track_coin_state(*args: Any) -> bool: if case == "allow_larger": allow_larger_height = peak.height + 1 tx = await notification_manager_1.send_new_notification(ph_2, msg, AMOUNT, DEFAULT_TX_CONFIG, fee=FEE) - await wsm_1.add_pending_transactions([tx]) + [tx] = await wsm_1.add_pending_transactions([tx]) await time_out_assert_not_none( 5, full_node_api.full_node.mempool_manager.get_spendbundle, diff --git a/tests/wallet/test_sign_coin_spends.py b/tests/wallet/test_sign_coin_spends.py index fbf6b3c6123c..16a14402d2b1 100644 --- a/tests/wallet/test_sign_coin_spends.py +++ b/tests/wallet/test_sign_coin_spends.py @@ -158,7 +158,14 @@ async def test_wsm_sign_transaction() -> None: wsm.private_key = top_sk with pytest.raises(ValueError, match="no secret key"): - await wsm.sign_transaction([spend_h]) + await sign_coin_spends( + [spend_h], + wsm.get_private_key_for_pubkey, + wsm.get_synthetic_private_key_for_puzzle_hash, + wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, + wsm.constants.MAX_BLOCK_COST_CLVM, + [puzzle_hash_for_synthetic_public_key], + ) await wsm.puzzle_store.add_derivation_paths( [ @@ -186,7 +193,16 @@ async def test_wsm_sign_transaction() -> None: ] ) - signature: G2Element = (await wsm.sign_transaction([spend_h])).aggregated_signature + signature: G2Element = ( + await sign_coin_spends( + [spend_h], + wsm.get_private_key_for_pubkey, + wsm.get_synthetic_private_key_for_puzzle_hash, + wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, + wsm.constants.MAX_BLOCK_COST_CLVM, + [puzzle_hash_for_synthetic_public_key], + ) + ).aggregated_signature assert signature == AugSchemeMPL.aggregate( [ AugSchemeMPL.sign(sk1_h, msg1), @@ -195,7 +211,14 @@ async def test_wsm_sign_transaction() -> None: ) with pytest.raises(ValueError, match="no secret key"): - await wsm.sign_transaction([spend_u]) + await sign_coin_spends( + [spend_u], + wsm.get_private_key_for_pubkey, + wsm.get_synthetic_private_key_for_puzzle_hash, + wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, + wsm.constants.MAX_BLOCK_COST_CLVM, + [puzzle_hash_for_synthetic_public_key], + ) await wsm.puzzle_store.add_derivation_paths( [ @@ -222,7 +245,16 @@ async def test_wsm_sign_transaction() -> None: ) ] ) - signature2: G2Element = (await wsm.sign_transaction([spend_u])).aggregated_signature + signature2: G2Element = ( + await sign_coin_spends( + [spend_u], + wsm.get_private_key_for_pubkey, + wsm.get_synthetic_private_key_for_puzzle_hash, + wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, + wsm.constants.MAX_BLOCK_COST_CLVM, + [puzzle_hash_for_synthetic_public_key], + ) + ).aggregated_signature assert signature2 == AugSchemeMPL.aggregate( [ AugSchemeMPL.sign(sk1_u, msg1), diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index f3edc665b54a..8b1091f3259f 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -26,6 +26,8 @@ from chia.wallet.conditions import ConditionValidTimes from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.payment import Payment +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key +from chia.wallet.sign_coin_spends import sign_coin_spends from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import TransactionTypeFilter @@ -121,7 +123,7 @@ async def test_wallet_make_transaction( DEFAULT_TX_CONFIG, uint64(0), ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet.get_confirmed_balance() == expected_confirmed_balance @@ -177,7 +179,7 @@ async def test_wallet_reuse_address( assert len(tx.spend_bundle.coin_spends) == 1 new_puzhash = [c.puzzle_hash.hex() for c in tx.additions] assert tx.spend_bundle.coin_spends[0].coin.puzzle_hash.hex() in new_puzhash - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet.get_confirmed_balance() == expected_confirmed_balance @@ -232,7 +234,7 @@ async def test_wallet_clawback_claim_auto( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 10}], ) - await wallet.wallet_state_manager.add_pending_transactions([tx1]) + [tx1] = await wallet.wallet_state_manager.add_pending_transactions([tx1]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx1]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) await time_out_assert( @@ -248,7 +250,7 @@ async def test_wallet_clawback_claim_auto( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 10}], ) - await wallet.wallet_state_manager.add_pending_transactions([tx2]) + [tx2] = await wallet.wallet_state_manager.add_pending_transactions([tx2]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx2]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet_1) await time_out_assert( @@ -264,7 +266,7 @@ async def test_wallet_clawback_claim_auto( uint64(0), puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 10}], ) - await wallet.wallet_state_manager.add_pending_transactions([tx3]) + [tx3] = await wallet.wallet_state_manager.add_pending_transactions([tx3]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx3]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) await time_out_assert( @@ -344,7 +346,7 @@ async def test_wallet_clawback_clawback( memos=[b"Test"], ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -467,7 +469,7 @@ async def test_wallet_clawback_sent_self( memos=[b"Test"], ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -553,7 +555,7 @@ async def test_wallet_clawback_claim_manual( puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -643,7 +645,7 @@ async def test_wallet_clawback_reorg( puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -741,7 +743,7 @@ async def test_get_clawback_coins( puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 500}], ) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx]) expected_confirmed_balance += await full_node_api.farm_blocks_to_wallet(count=num_blocks, wallet=wallet) # Check merkle coins @@ -801,7 +803,7 @@ async def test_clawback_resync( clawback_coin_id_1 = tx1.additions[0].name() assert tx1.spend_bundle is not None - await wallet_1.wallet_state_manager.add_pending_transactions([tx1]) + [tx1] = await wallet_1.wallet_state_manager.add_pending_transactions([tx1]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx1]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) # Check merkle coins @@ -820,7 +822,7 @@ async def test_clawback_resync( ) clawback_coin_id_2 = tx2.additions[0].name() assert tx2.spend_bundle is not None - await wallet_1.wallet_state_manager.add_pending_transactions([tx2]) + [tx2] = await wallet_1.wallet_state_manager.add_pending_transactions([tx2]) await full_node_api.wait_transaction_records_entered_mempool(records=[tx2]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32))) # Check merkle coins @@ -1048,7 +1050,7 @@ async def test_wallet_send_to_three_peers( uint64(0), ) assert tx.spend_bundle is not None - await wallet_0.wallet_state_manager.main_wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_0.wallet_state_manager.main_wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api_0.wait_transaction_records_entered_mempool(records=[tx]) # wallet0 <-> sever1 @@ -1105,7 +1107,7 @@ async def test_wallet_make_transaction_hop( uint64(0), ) - await wallet_0.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_0.wallet_state_manager.add_pending_transactions([tx]) await full_node_api_0.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet_0.get_confirmed_balance() == expected_confirmed_balance @@ -1123,7 +1125,7 @@ async def test_wallet_make_transaction_hop( [tx] = await wallet_1.generate_signed_transaction( uint64(tx_amount), await wallet_0.get_new_puzzlehash(), DEFAULT_TX_CONFIG, uint64(0) ) - await wallet_1.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_1.wallet_state_manager.add_pending_transactions([tx]) await full_node_api_0.wait_transaction_records_entered_mempool(records=[tx]) await full_node_api_0.farm_blocks_to_puzzlehash(count=4, guarantee_transaction_blocks=True) @@ -1186,7 +1188,7 @@ async def test_wallet_make_transaction_with_fee( fees = estimate_fees(tx.spend_bundle) assert fees == tx_fee - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_1.wait_transaction_records_entered_mempool(records=[tx]) assert await wallet.get_confirmed_balance() == expected_confirmed_balance @@ -1247,13 +1249,13 @@ async def test_wallet_make_transaction_with_memo( [tx] = await wallet.generate_signed_transaction( uint64(tx_amount), ph_2, DEFAULT_TX_CONFIG, uint64(tx_fee), memos=[ph_2] ) - tx_id = tx.name.hex() assert tx.spend_bundle is not None fees = estimate_fees(tx.spend_bundle) assert fees == tx_fee - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) + tx_id = tx.name.hex() await full_node_1.wait_transaction_records_entered_mempool(records=[tx]) memos = await api_0.get_transaction_memo(dict(transaction_id=tx_id)) # test json serialization @@ -1315,7 +1317,7 @@ async def test_wallet_create_hit_max_send_amount( ) assert tx_split_coins.spend_bundle is not None - await wallet.wallet_state_manager.add_pending_transactions([tx_split_coins]) + [tx_split_coins] = await wallet.wallet_state_manager.add_pending_transactions([tx_split_coins]) await full_node_1.process_transaction_records(records=[tx_split_coins]) await wait_for_coins_in_wallet(coins=set(tx_split_coins.additions), wallet=wallet, timeout=20) @@ -1408,7 +1410,14 @@ async def test_wallet_prevent_fee_theft( if compute_additions(cs) == []: stolen_cs = cs # get a legit signature - stolen_sb = await wallet_node.wallet_state_manager.sign_transaction([stolen_cs]) + stolen_sb = await sign_coin_spends( + [stolen_cs], + wallet_node.wallet_state_manager.get_private_key_for_pubkey, + wallet_node.wallet_state_manager.get_synthetic_private_key_for_puzzle_hash, + wallet_node.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA, + wallet_node.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + [puzzle_hash_for_synthetic_public_key], + ) now = uint64(int(time.time())) add_list = list(stolen_sb.additions()) rem_list = list(stolen_sb.removals()) @@ -1432,7 +1441,7 @@ async def test_wallet_prevent_fee_theft( memos=list(compute_memos(stolen_sb).items()), valid_times=ConditionValidTimes(), ) - await wallet.wallet_state_manager.add_pending_transactions([stolen_tx]) + [stolen_tx] = await wallet.wallet_state_manager.add_pending_transactions([stolen_tx]) await time_out_assert(20, wallet.get_confirmed_balance, expected_confirmed_balance) await time_out_assert(20, wallet.get_unconfirmed_balance, expected_confirmed_balance - stolen_cs.coin.amount) @@ -1492,7 +1501,7 @@ async def test_wallet_tx_reorg( [tx] = await wallet.generate_signed_transaction(uint64(tx_amount), ph2, DEFAULT_TX_CONFIG, coins={coin}) assert tx.spend_bundle is not None - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await full_node_api.process_transaction_records(records=[tx]) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node, wallet_node_2], timeout=20) @@ -1745,7 +1754,7 @@ async def test_wallet_transaction_options( assert tx.spend_bundle is not None paid_coin = [coin for coin in tx.spend_bundle.additions() if coin.amount == AMOUNT_TO_SEND][0] assert paid_coin.parent_coin_info == coin_list[2].name() - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) await time_out_assert(20, wallet.get_confirmed_balance, expected_confirmed_balance) await time_out_assert(20, wallet.get_unconfirmed_balance, expected_confirmed_balance - AMOUNT_TO_SEND) diff --git a/tests/wallet/test_wallet_retry.py b/tests/wallet/test_wallet_retry.py index ba08f1c0a873..b6048c25fb48 100644 --- a/tests/wallet/test_wallet_retry.py +++ b/tests/wallet/test_wallet_retry.py @@ -64,9 +64,9 @@ async def test_wallet_tx_retry( await full_node_1.wait_for_wallet_synced(wallet_node=wallet_node_1, timeout=wait_secs) [transaction] = await wallet_1.generate_signed_transaction(uint64(100), reward_ph, DEFAULT_TX_CONFIG) + [transaction] = await wallet_1.wallet_state_manager.add_pending_transactions([transaction]) sb1: Optional[SpendBundle] = transaction.spend_bundle assert sb1 is not None - await wallet_1.wallet_state_manager.add_pending_transactions([transaction]) async def sb_in_mempool() -> bool: return full_node_1.full_node.mempool_manager.get_spendbundle(transaction.name) == transaction.spend_bundle diff --git a/tests/wallet/vc_wallet/test_vc_wallet.py b/tests/wallet/vc_wallet/test_vc_wallet.py index 54610aad7cd9..1854df0c46a8 100644 --- a/tests/wallet/vc_wallet/test_vc_wallet.py +++ b/tests/wallet/vc_wallet/test_vc_wallet.py @@ -343,7 +343,7 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: uint64(2000000000), memos=["hey"], ) - await wallet_node_0.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_node_0.wallet_state_manager.add_pending_transactions([tx]) await wallet_environments.process_pending_states( [ WalletStateTransition( @@ -514,7 +514,7 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: uint64(0), cat_discrepancy=(-50, Program.to(None), Program.to(None)), ) - await wallet_node_1.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet_node_1.wallet_state_manager.add_pending_transactions([tx]) await wallet_environments.process_pending_states( [ WalletStateTransition(), @@ -671,7 +671,7 @@ async def test_self_revoke(wallet_environments: WalletTestFramework) -> None: # Send the DID to oblivion txs = await did_wallet.transfer_did(bytes32([0] * 32), uint64(0), False, wallet_environments.tx_config) - await did_wallet.wallet_state_manager.add_pending_transactions(txs) + txs = await did_wallet.wallet_state_manager.add_pending_transactions(txs) await wallet_environments.process_pending_states( [ WalletStateTransition( From 0fa77d5e8f98bfaa161fcabe28dd02d9a56ecba0 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 08:08:44 -0800 Subject: [PATCH 026/274] test coverage --- chia/simulator/full_node_simulator.py | 26 ++++++++++++++ tests/wallet/test_signer_protocol.py | 49 ++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/chia/simulator/full_node_simulator.py b/chia/simulator/full_node_simulator.py index e6d7cb24a7b7..336bcc615447 100644 --- a/chia/simulator/full_node_simulator.py +++ b/chia/simulator/full_node_simulator.py @@ -461,6 +461,32 @@ async def wait_transaction_records_entered_mempool( await asyncio.sleep(backoff) + async def wait_bundle_ids_in_mempool( + self, + bundle_ids: Collection[bytes32], + timeout: Union[None, float] = 5, + ) -> None: + """Wait until the ids of specific spend bundles have entered the mempool. + + Arguments: + records: The bundle ids to wait for. + """ + with anyio.fail_after(delay=adjusted_timeout(timeout)): + ids_to_check: Set[bytes32] = set(bundle_ids) + + for backoff in backoff_times(): + found = set() + for spend_bundle_name in ids_to_check: + tx = self.full_node.mempool_manager.get_spendbundle(spend_bundle_name) + if tx is not None: + found.add(spend_bundle_name) + ids_to_check = ids_to_check.difference(found) + + if len(ids_to_check) == 0: + return + + await asyncio.sleep(backoff) + async def wait_transaction_records_marked_as_in_mempool( self, record_ids: Collection[bytes32], diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 2b6cd07f35b1..40375ed8308c 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -11,6 +11,7 @@ from chia.types.blockchain_format.program import Program 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.ints import uint64 from chia.util.streamable import ConversionError, Streamable, streamable from chia.wallet.conditions import AggSigMe @@ -22,6 +23,7 @@ from chia.wallet.util.signer_protocol import ( KeyHints, PathHint, + SignedTransaction, SigningInstructions, SigningResponse, SigningTarget, @@ -31,9 +33,10 @@ UnsignedTransaction, clvm_serialization_mode, ) +from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager -from tests.wallet.conftest import WalletTestFramework +from tests.wallet.conftest import WalletStateTransition, WalletTestFramework def test_signing_serialization() -> None: @@ -157,17 +160,12 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client # Test first that we can properly examine and sign a regular transaction - puzzle: Program = await wallet.get_puzzle(new=False) - puzzle_hash: bytes32 = puzzle.get_tree_hash() + [coin] = await wallet.select_coins(uint64(0), DEFAULT_COIN_SELECTION_CONFIG) + puzzle: Program = await wallet.puzzle_for_puzzle_hash(coin.puzzle_hash) delegated_puzzle: Program = Program.to(None) delegated_puzzle_hash: bytes32 = delegated_puzzle.get_tree_hash() solution: Program = Program.to([None, None, None]) - coin: ConsensusCoin = ConsensusCoin( - bytes32([0] * 32), - puzzle_hash, - uint64(0), - ) coin_spend: CoinSpend = CoinSpend( coin, puzzle, @@ -176,7 +174,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram derivation_record: Optional[ DerivationRecord - ] = await wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(puzzle_hash) + ] = await wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(coin.puzzle_hash) assert derivation_record is not None pubkey: G1Element = derivation_record.pubkey synthetic_pubkey: G1Element = G1Element.from_bytes(puzzle.uncurry()[1].at("f").atom) @@ -268,3 +266,36 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ) assert len(signing_responses_2) == 1 assert signing_responses_2 == signing_responses + + signed_txs: List[SignedTransaction] = ( + await wallet_rpc.apply_signatures( + spends=[Spend.from_coin_spend(coin_spend)], signing_responses=signing_responses + ) + ).signed_transactions + await wallet_rpc.submit_transactions(signed_transactions=signed_txs) + await wallet_environments.full_node.wait_bundle_ids_in_mempool( + [ + SpendBundle( + [spend.as_coin_spend() for tx in signed_txs for spend in tx.transaction_info.spends], + G2Element.from_bytes(signing_responses[0].signature), + ).name() + ] + ) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + # We haven't submitted a TransactionRecord so the wallet won't know about this until confirmed + pre_block_balance_updates={}, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -1 * coin.amount, + "unconfirmed_wallet_balance": -1 * coin.amount, + "spendable_balance": -1 * coin.amount, + "max_send_amount": -1 * coin.amount, + "unspent_coin_count": -1, + }, + }, + ), + ] + ) From 0361ef014905188207c9bcb28738b20d4f25c71b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 08:37:02 -0800 Subject: [PATCH 027/274] Fix aggregated trade test --- tests/wallet/cat_wallet/test_trades.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 4c15879bbefb..111bd26bd44a 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -1993,7 +1993,13 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: assert success is True assert trade_make_2 is not None - agg_offer = Offer.aggregate([Offer.from_bytes(trade_make_1.offer), Offer.from_bytes(trade_make_2.offer)]) + [offer_1], signing_response_1 = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make_1.offer)] + ) + [offer_2], signing_response_2 = await wallet_node_maker.wallet_state_manager.sign_offers( + [Offer.from_bytes(trade_make_2.offer)] + ) + agg_offer = Offer.aggregate([offer_1, offer_2]) peer = wallet_node_taker.get_full_node_peer() trade_take, tx_records = await trade_manager_taker.respond_to_offer( @@ -2004,7 +2010,10 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: assert trade_take is not None assert tx_records is not None - await trade_manager_taker.wallet_state_manager.add_pending_transactions(tx_records) + tx_records = await trade_manager_taker.wallet_state_manager.add_pending_transactions( + tx_records, + additional_signing_responses=[*signing_response_1, *signing_response_2], + ) await full_node.process_transaction_records(records=tx_records) await full_node.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=60) From dea7db00ee74fa8cbf07cca4c2533b4baf1fc1ac Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 10:20:01 -0800 Subject: [PATCH 028/274] Fix tests --- tests/wallet/dao_wallet/test_dao_wallets.py | 2 +- tests/wallet/nft_wallet/test_nft_bulk_mint.py | 8 ++++---- tests/wallet/test_wallet_node.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/wallet/dao_wallet/test_dao_wallets.py b/tests/wallet/dao_wallet/test_dao_wallets.py index 2a07d36fcb00..d3c3436ed09c 100644 --- a/tests/wallet/dao_wallet/test_dao_wallets.py +++ b/tests/wallet/dao_wallet/test_dao_wallets.py @@ -2767,7 +2767,7 @@ async def test_dao_concurrency( await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_2, timeout=30) txs = await dao_cat_wallet_2.enter_dao_cat_voting_mode(dao_cat_1_bal, DEFAULT_TX_CONFIG) - await wallet_2.wallet_state_manager.add_pending_transactions(txs) + txs = await wallet_2.wallet_state_manager.add_pending_transactions(txs) await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_2, timeout=60) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=30) diff --git a/tests/wallet/nft_wallet/test_nft_bulk_mint.py b/tests/wallet/nft_wallet/test_nft_bulk_mint.py index 95f2f4870307..d23cca630733 100644 --- a/tests/wallet/nft_wallet/test_nft_bulk_mint.py +++ b/tests/wallet/nft_wallet/test_nft_bulk_mint.py @@ -118,7 +118,7 @@ async def test_nft_mint_from_did( sb = tx_records[0].spend_bundle assert sb is not None - bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) + bundle, _ = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) @@ -618,7 +618,7 @@ async def test_nft_mint_from_did_multiple_xch( sb = tx_records[0].spend_bundle assert sb is not None - bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) + bundle, _ = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) @@ -720,7 +720,7 @@ async def test_nft_mint_from_xch( sb = tx_records[0].spend_bundle assert sb is not None - bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) + bundle, _ = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) @@ -1036,7 +1036,7 @@ async def test_nft_mint_from_xch_multiple_xch( sb = tx_records[0].spend_bundle assert sb is not None - bundle = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb) + bundle, _ = await nft_wallet_maker.wallet_state_manager.sign_bundle(sb.coin_spends) await api_0.push_tx({"spend_bundle": bytes(bundle).hex()}) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, bundle.name()) diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index 51520ce6380b..35b8aafd851a 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -541,7 +541,7 @@ async def send_transaction( # Generate the transaction [tx] = await wallet.generate_signed_transaction(uint64(0), bytes32([0] * 32), DEFAULT_TX_CONFIG) - await wallet.wallet_state_manager.add_pending_transactions([tx]) + [tx] = await wallet.wallet_state_manager.add_pending_transactions([tx]) # Make sure it is sent to the peer await wallet_node._resend_queue() From 817ce5bfcc8f3b7a024e7479b5760fd86c7a169f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 10:24:34 -0800 Subject: [PATCH 029/274] coverage ignores --- chia/rpc/util.py | 2 +- chia/wallet/wallet_state_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index e13d27198423..d426697cf908 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -158,7 +158,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s TransactionRecord.to_json_dict_convenience(tx, self.service.config) for tx in new_txs ] else: - new_txs = tx_records + new_txs = tx_records # pragma: no cover if request.get("push", push): await self.service.wallet_state_manager.add_pending_transactions(new_txs, merge_spends=merge_spends) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index fee15cf22451..27737e1e0c53 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -2609,7 +2609,7 @@ async def apply_signatures( def signed_tx_to_spendbundle(self, signed_tx: SignedTransaction) -> SpendBundle: if len([_ for _ in signed_tx.signatures if _.type != "bls_12381_aug_scheme"]) > 0: - raise ValueError("Unable to handle signatures that are not bls_12381_aug_scheme") + raise ValueError("Unable to handle signatures that are not bls_12381_aug_scheme") # pragma: no cover return SpendBundle( [spend.as_coin_spend() for spend in signed_tx.transaction_info.spends], AugSchemeMPL.aggregate([G2Element.from_bytes(sig.signature) for sig in signed_tx.signatures]), From 64e5a9d7de8594fd067041042b9ed67fc02ffdc2 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Nov 2023 13:10:29 -0800 Subject: [PATCH 030/274] fix benchmark test --- tests/core/mempool/test_mempool_performance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/core/mempool/test_mempool_performance.py b/tests/core/mempool/test_mempool_performance.py index 9d0460ed1609..24974029a420 100644 --- a/tests/core/mempool/test_mempool_performance.py +++ b/tests/core/mempool/test_mempool_performance.py @@ -52,6 +52,7 @@ async def test_mempool_update_performance( ph = await wallet.get_new_puzzlehash() [big_transaction] = await wallet.generate_signed_transaction(send_amount, ph, DEFAULT_TX_CONFIG, fee_amount) + [big_transaction], _ = await wallet.wallet_state_manager.sign_transactions([big_transaction]) assert big_transaction.spend_bundle is not None status, err = await full_node.add_transaction( big_transaction.spend_bundle, big_transaction.spend_bundle.name(), test=True From 5d92ebb298f7ace0a9d557d979bfbe360faa57b0 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 12:51:33 -0800 Subject: [PATCH 031/274] Bring forward some changes left out --- chia/wallet/sign_coin_spends.py | 71 --------- tests/wallet/cat_wallet/test_cat_wallet.py | 13 +- tests/wallet/test_sign_coin_spends.py | 167 ++++----------------- tests/wallet/test_wallet.py | 11 +- 4 files changed, 34 insertions(+), 228 deletions(-) delete mode 100644 chia/wallet/sign_coin_spends.py diff --git a/chia/wallet/sign_coin_spends.py b/chia/wallet/sign_coin_spends.py deleted file mode 100644 index 4d5714f1fc6c..000000000000 --- a/chia/wallet/sign_coin_spends.py +++ /dev/null @@ -1,71 +0,0 @@ -from __future__ import annotations - -import inspect -from typing import Any, Callable, List - -from chia_rs import AugSchemeMPL, G1Element, G2Element - -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.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict - - -async def sign_coin_spends( - coin_spends: List[CoinSpend], - secret_key_for_public_key_f: Any, # Potentially awaitable function from G1Element => Optional[PrivateKey] - secret_key_for_puzzle_hash: Any, # Potentially awaitable function from bytes32 => Optional[PrivateKey] - additional_data: bytes, - max_cost: int, - potential_derivation_functions: List[Callable[[G1Element], bytes32]], -) -> SpendBundle: - """ - Sign_coin_spends runs the puzzle code with the given argument and searches the - result for an AGG_SIG_ME condition, which it attempts to sign by requesting a - matching PrivateKey corresponding with the given G1Element (public key) specified - in the resulting condition output. - - It's important to note that as mentioned in the documentation about the standard - spend that the public key presented to the secret_key_for_public_key_f function - provided to sign_coin_spends must be prepared to do the key derivations required - by the coin types it's allowed to spend (at least the derivation of the standard - spend as done by calculate_synthetic_secret_key with DEFAULT_PUZZLE_HASH). - - If a coin performed a different key derivation, the pk presented to this function - would be similarly alien, and would need to be tried against the first stage - derived keys (those returned by master_sk_to_wallet_sk from the ['sk'] member of - wallet rpc's get_private_key method). - """ - signatures: List[G2Element] = [] - pk_list: List[G1Element] = [] - msg_list: List[bytes] = [] - for coin_spend in coin_spends: - # Get AGG_SIG conditions - conditions_dict = conditions_dict_for_solution(coin_spend.puzzle_reveal, coin_spend.solution, max_cost) - # Create signature - for pk_bytes, msg in pkm_pairs_for_conditions_dict(conditions_dict, coin_spend.coin, additional_data): - pk = G1Element.from_bytes(pk_bytes) - pk_list.append(pk) - msg_list.append(msg) - if inspect.iscoroutinefunction(secret_key_for_public_key_f): - secret_key = await secret_key_for_public_key_f(pk) - else: - secret_key = secret_key_for_public_key_f(pk) - if secret_key is None or secret_key.get_g1() != pk: - for derive in potential_derivation_functions: - if inspect.iscoroutinefunction(secret_key_for_puzzle_hash): - secret_key = await secret_key_for_puzzle_hash(derive(pk)) - else: - secret_key = secret_key_for_puzzle_hash(derive(pk)) - if secret_key is not None and secret_key.get_g1() == pk: - break - else: - raise ValueError(f"no secret key for {pk}") - signature = AugSchemeMPL.sign(secret_key, msg) - assert AugSchemeMPL.verify(pk, msg, signature) - signatures.append(signature) - - # Aggregate signatures - aggsig = AugSchemeMPL.aggregate(signatures) - assert AugSchemeMPL.aggregate_verify(pk_list, msg_list, aggsig) - return SpendBundle(coin_spends, aggsig) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 4a4144ced759..b428018891f9 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -29,11 +29,7 @@ from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import _derive_path_unhardened, master_sk_to_wallet_sk_unhardened_intermediate from chia.wallet.lineage_proof import LineageProof -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( - puzzle_hash_for_pk, - puzzle_hash_for_synthetic_public_key, -) -from chia.wallet.sign_coin_spends import sign_coin_spends +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_pk from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import WalletType @@ -981,7 +977,7 @@ async def test_cat_change_detection( ).get_tree_hash(), cat_amount_0, ) - eve_spend = await sign_coin_spends( + eve_spend, _ = await wallet_node_0.wallet_state_manager.sign_bundle( [ CoinSpend( cat_coin, @@ -1040,11 +1036,6 @@ async def test_cat_change_detection( ), ), ], - wallet_node_0.wallet_state_manager.get_private_key_for_pubkey, - wallet_node_0.wallet_state_manager.get_synthetic_private_key_for_puzzle_hash, - wallet_node_0.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA, - wallet_node_0.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], ) await client_0.push_tx(eve_spend) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, eve_spend.name()) diff --git a/tests/wallet/test_sign_coin_spends.py b/tests/wallet/test_sign_coin_spends.py index 16a14402d2b1..d20527a22d7a 100644 --- a/tests/wallet/test_sign_coin_spends.py +++ b/tests/wallet/test_sign_coin_spends.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional +import re import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey @@ -21,24 +21,25 @@ calculate_synthetic_secret_key, puzzle_hash_for_synthetic_public_key, ) -from chia.wallet.sign_coin_spends import sign_coin_spends from chia.wallet.util.wallet_types import WalletType +from chia.wallet.wallet import Wallet from chia.wallet.wallet_puzzle_store import WalletPuzzleStore from chia.wallet.wallet_state_manager import WalletStateManager +from chia.wallet.wallet_user_store import WalletUserStore top_sk: PrivateKey = PrivateKey.from_bytes(bytes([1] * 32)) sk1_h: PrivateKey = master_sk_to_wallet_sk(top_sk, uint32(1)) -sk2_h: PrivateKey = calculate_synthetic_secret_key( - master_sk_to_wallet_sk(top_sk, uint32(2)), DEFAULT_HIDDEN_PUZZLE_HASH -) +sk2_h: PrivateKey = master_sk_to_wallet_sk(top_sk, uint32(2)) +sk2_h_synth: PrivateKey = calculate_synthetic_secret_key(sk2_h, DEFAULT_HIDDEN_PUZZLE_HASH) sk1_u: PrivateKey = master_sk_to_wallet_sk_unhardened(top_sk, uint32(1)) -sk2_u: PrivateKey = calculate_synthetic_secret_key( - master_sk_to_wallet_sk_unhardened(top_sk, uint32(2)), DEFAULT_HIDDEN_PUZZLE_HASH -) +sk2_u: PrivateKey = master_sk_to_wallet_sk(top_sk, uint32(2)) +sk2_u_synth: PrivateKey = calculate_synthetic_secret_key(sk2_u, DEFAULT_HIDDEN_PUZZLE_HASH) pk1_h: G1Element = sk1_h.get_g1() pk2_h: G1Element = sk2_h.get_g1() +pk2_h_synth: G1Element = sk2_h_synth.get_g1() pk1_u: G1Element = sk1_u.get_g1() pk2_u: G1Element = sk2_u.get_g1() +pk2_u_synth: G1Element = sk2_u_synth.get_g1() msg1: bytes = b"msg1" msg2: bytes = b"msg2" @@ -47,10 +48,10 @@ coin: Coin = Coin(bytes32([0] * 32), bytes32([0] * 32), 0) puzzle = SerializedProgram.from_bytes(b"\x01") solution_h = SerializedProgram.from_program( - Program.to([[ConditionOpcode.AGG_SIG_UNSAFE, pk1_h, msg1], [ConditionOpcode.AGG_SIG_ME, pk2_h, msg2]]) + Program.to([[ConditionOpcode.AGG_SIG_UNSAFE, pk1_h, msg1], [ConditionOpcode.AGG_SIG_ME, pk2_h_synth, msg2]]) ) solution_u = SerializedProgram.from_program( - Program.to([[ConditionOpcode.AGG_SIG_UNSAFE, pk1_u, msg1], [ConditionOpcode.AGG_SIG_ME, pk2_u, msg2]]) + Program.to([[ConditionOpcode.AGG_SIG_UNSAFE, pk1_u, msg1], [ConditionOpcode.AGG_SIG_ME, pk2_u_synth, msg2]]) ) spend_h: CoinSpend = CoinSpend( coin, @@ -64,88 +65,6 @@ ) -@pytest.mark.anyio -async def test_sign_coin_spends() -> None: - def derive_ph(pk: G1Element) -> bytes32: - return bytes32([0] * 32) - - def pk_to_sk(pk: G1Element) -> Optional[PrivateKey]: - if pk == pk1_h: - return sk1_h - return None - - def ph_to_sk(ph: bytes32) -> Optional[PrivateKey]: - if ph == derive_ph(G1Element()): - return sk2_h - return None - - with pytest.raises(ValueError, match="no secret key"): - await sign_coin_spends( - [spend_h], - pk_to_sk, - lambda _: None, - additional_data, - 1000000000, - [derive_ph], - ) - - with pytest.raises(ValueError, match="no secret key"): - await sign_coin_spends( - [spend_h], - lambda _: None, - ph_to_sk, - additional_data, - 1000000000, - [derive_ph], - ) - - with pytest.raises(ValueError, match="no secret key"): - await sign_coin_spends( - [spend_h], - pk_to_sk, - ph_to_sk, - additional_data, - 1000000000, - [], - ) - - signature: G2Element = ( - await sign_coin_spends( - [spend_h], - pk_to_sk, - ph_to_sk, - additional_data, - 1000000000, - [lambda _: bytes32([1] * 32), derive_ph], - ) - ).aggregated_signature - - assert signature == AugSchemeMPL.aggregate( - [ - AugSchemeMPL.sign(sk1_h, msg1), - AugSchemeMPL.sign(sk2_h, msg2 + coin.name() + additional_data), - ] - ) - - async def pk_to_sk_async(pk: G1Element) -> Optional[PrivateKey]: - return pk_to_sk(pk) - - async def ph_to_sk_async(ph: bytes32) -> Optional[PrivateKey]: - return ph_to_sk(ph) - - signature2: G2Element = ( - await sign_coin_spends( - [spend_h], - pk_to_sk_async, - ph_to_sk_async, - additional_data, - 1000000000, - [derive_ph], - ) - ).aggregated_signature - assert signature2 == signature - - @pytest.mark.anyio async def test_wsm_sign_transaction() -> None: async with manage_connection("file:temp.db?mode=memory&cache=shared", uri=True, name="writer") as writer_conn: @@ -156,16 +75,15 @@ async def test_wsm_sign_transaction() -> None: wsm.puzzle_store = await WalletPuzzleStore.create(db) wsm.constants = DEFAULT_CONSTANTS wsm.private_key = top_sk + wsm.user_store = await WalletUserStore.create(db) + wallet_info = await wsm.user_store.get_wallet_by_id(1) + assert wallet_info is not None + wsm.main_wallet = await Wallet.create(wsm, wallet_info) - with pytest.raises(ValueError, match="no secret key"): - await sign_coin_spends( - [spend_h], - wsm.get_private_key_for_pubkey, - wsm.get_synthetic_private_key_for_puzzle_hash, - wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, - wsm.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) + with pytest.raises( + ValueError, match=re.escape(f"Pubkey {pk1_h.get_fingerprint()} not found (or path/sum hinted to)") + ): + await wsm.sign_bundle([spend_h]) await wsm.puzzle_store.add_derivation_paths( [ @@ -184,8 +102,8 @@ async def test_wsm_sign_transaction() -> None: [ DerivationRecord( uint32(2), - puzzle_hash_for_synthetic_public_key(pk2_h), - G1Element(), + puzzle_hash_for_synthetic_public_key(pk2_h_synth), + pk2_h, WalletType.STANDARD_WALLET, uint32(1), True, @@ -193,32 +111,18 @@ async def test_wsm_sign_transaction() -> None: ] ) - signature: G2Element = ( - await sign_coin_spends( - [spend_h], - wsm.get_private_key_for_pubkey, - wsm.get_synthetic_private_key_for_puzzle_hash, - wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, - wsm.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) - ).aggregated_signature + signature: G2Element = ((await wsm.sign_bundle([spend_h]))[0]).aggregated_signature assert signature == AugSchemeMPL.aggregate( [ AugSchemeMPL.sign(sk1_h, msg1), - AugSchemeMPL.sign(sk2_h, msg2 + coin.name() + additional_data), + AugSchemeMPL.sign(sk2_h_synth, msg2 + coin.name() + additional_data), ] ) - with pytest.raises(ValueError, match="no secret key"): - await sign_coin_spends( - [spend_u], - wsm.get_private_key_for_pubkey, - wsm.get_synthetic_private_key_for_puzzle_hash, - wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, - wsm.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) + with pytest.raises( + ValueError, match=re.escape(f"Pubkey {pk1_u.get_fingerprint()} not found (or path/sum hinted to)") + ): + await wsm.sign_bundle([spend_u]) await wsm.puzzle_store.add_derivation_paths( [ @@ -237,27 +141,18 @@ async def test_wsm_sign_transaction() -> None: [ DerivationRecord( uint32(2), - puzzle_hash_for_synthetic_public_key(pk2_u), - G1Element(), + puzzle_hash_for_synthetic_public_key(pk2_u_synth), + pk2_u, WalletType.STANDARD_WALLET, uint32(1), False, ) ] ) - signature2: G2Element = ( - await sign_coin_spends( - [spend_u], - wsm.get_private_key_for_pubkey, - wsm.get_synthetic_private_key_for_puzzle_hash, - wsm.constants.AGG_SIG_ME_ADDITIONAL_DATA, - wsm.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) - ).aggregated_signature + signature2: G2Element = ((await wsm.sign_bundle([spend_u]))[0]).aggregated_signature assert signature2 == AugSchemeMPL.aggregate( [ AugSchemeMPL.sign(sk1_u, msg1), - AugSchemeMPL.sign(sk2_u, msg2 + coin.name() + additional_data), + AugSchemeMPL.sign(sk2_u_synth, msg2 + coin.name() + additional_data), ] ) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 8b1091f3259f..af58b59956d5 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -26,8 +26,6 @@ from chia.wallet.conditions import ConditionValidTimes from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.payment import Payment -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key -from chia.wallet.sign_coin_spends import sign_coin_spends from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import TransactionTypeFilter @@ -1410,14 +1408,7 @@ async def test_wallet_prevent_fee_theft( if compute_additions(cs) == []: stolen_cs = cs # get a legit signature - stolen_sb = await sign_coin_spends( - [stolen_cs], - wallet_node.wallet_state_manager.get_private_key_for_pubkey, - wallet_node.wallet_state_manager.get_synthetic_private_key_for_puzzle_hash, - wallet_node.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA, - wallet_node.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - [puzzle_hash_for_synthetic_public_key], - ) + stolen_sb, _ = await wallet_node.wallet_state_manager.sign_bundle([stolen_cs]) now = uint64(int(time.time())) add_list = list(stolen_sb.additions()) rem_list = list(stolen_sb.removals()) From a319fb285f61093b679a506b1c11ebd2fb5b1ca7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 13:04:03 -0800 Subject: [PATCH 032/274] Allow public-key-only entries in keyring --- chia/daemon/keychain_proxy.py | 63 +++++++++++++++++++++++- chia/daemon/keychain_server.py | 62 ++++++++++++++++++++++- chia/daemon/server.py | 16 +++--- chia/util/keychain.py | 49 ++++++++++++++++-- chia/wallet/derive_keys.py | 15 ++++++ tests/core/daemon/test_daemon.py | 20 +++++++- tests/core/daemon/test_keychain_proxy.py | 24 +++++++++ 7 files changed, 235 insertions(+), 14 deletions(-) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 16ac8c00e69f..6b58ba09c0b3 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Tuple from aiohttp import ClientConnectorError, ClientSession -from chia_rs import AugSchemeMPL, PrivateKey +from chia_rs import AugSchemeMPL, G1Element, PrivateKey from chia.cmds.init_funcs import check_keys from chia.daemon.client import DaemonProxy @@ -20,6 +20,7 @@ KEYCHAIN_ERR_NO_KEYS, ) from chia.server.server import ssl_context_for_client +from chia.util.byte_types import hexstr_to_bytes from chia.util.config import load_config from chia.util.errors import ( KeychainIsEmpty, @@ -194,6 +195,30 @@ async def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> P return key + async def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: + """ + Forwards to Keychain.add_public_key() + """ + key: G1Element + if self.use_local_keychain(): + key = self.keychain.add_public_key(pubkey, label) # pragma: no cover + else: + response, success = await self.get_response_for_request( + "add_public_key", {"public_key": pubkey, "label": label} + ) + if success: + key = G1Element.from_bytes(hexstr_to_bytes(pubkey)) + else: + error = response["data"].get("error", None) + if error == KEYCHAIN_ERR_KEYERROR: # pragma: no cover + error_details = response["data"].get("error_details", {}) + word = error_details.get("word", "") + raise KeyError(word) + else: + self.handle_error(response) + + return key + async def check_keys(self, root_path: Path) -> None: """ Forwards to init_funcs.check_keys() @@ -348,6 +373,42 @@ async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ return key + async def get_public_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[G1Element]: + """ + Locates and returns a private key matching the provided fingerprint + """ + key: Optional[G1Element] = None + if self.use_local_keychain(): + public_keys = self.keychain.get_all_public_keys() + if len(public_keys) == 0: + raise KeychainIsEmpty() + else: + if fingerprint is not None: + for pk in public_keys: + if pk.get_fingerprint() == fingerprint: + key = pk + break + if key is None: + raise KeychainKeyNotFound(fingerprint) + else: + key = public_keys[0] + else: + response, success = await self.get_response_for_request( + "get_public_key_for_fingerprint", {"fingerprint": fingerprint} + ) + if success: + pk_str = response["data"].get("pk", None) + if pk_str is None: # pragma: no cover + err = f"Missing pk in {response.get('command')} response" + self.log.error(f"{err}") + raise KeychainMalformedResponse(f"{err}") + else: + key = G1Element.from_bytes(bytes.fromhex(pk_str)) + else: + self.handle_error(response) + + return key + async def get_key(self, fingerprint: int, include_secrets: bool = False) -> Optional[KeyData]: """ Locates and returns KeyData matching the provided fingerprint diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index 94747e4db3d7..b20f8b3c65dd 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Type -from chia_rs import PrivateKey +from chia_rs import G1Element, PrivateKey from chia.cmds.init_funcs import check_keys from chia.util.errors import KeychainException, KeychainFingerprintNotFound @@ -16,12 +16,14 @@ # Commands that are handled by the KeychainServer keychain_commands = [ "add_private_key", + "add_public_key", "check_keys", "delete_all_keys", "delete_key_by_fingerprint", "get_all_private_keys", "get_first_private_key", "get_key_for_fingerprint", + "get_public_key_for_fingerprint", "get_key", "get_keys", "get_public_key", @@ -174,6 +176,8 @@ async def handle_command(self, command: str, data: Dict[str, Any]) -> Dict[str, try: if command == "add_private_key": return await self.add_private_key(data) + elif command == "add_public_key": + return await self.add_public_key(data) elif command == "check_keys": return await self.check_keys(data) elif command == "delete_all_keys": @@ -186,6 +190,8 @@ async def handle_command(self, command: str, data: Dict[str, Any]) -> Dict[str, return await self.get_first_private_key(data) elif command == "get_key_for_fingerprint": return await self.get_key_for_fingerprint(data) + elif command == "get_public_key_for_fingerprint": + return await self.get_public_key_for_fingerprint(data) elif command == "get_key": return await self.run_request(data, GetKeyRequest) elif command == "get_keys": @@ -234,6 +240,37 @@ async def add_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]: return {"success": True, "fingerprint": sk.get_g1().get_fingerprint()} + async def add_public_key(self, request: Dict[str, Any]) -> Dict[str, Any]: + if self.get_keychain_for_request(request).is_keyring_locked(): # pragma: no cover + return {"success": False, "error": KEYCHAIN_ERR_LOCKED} + + public_key = request.get("public_key", None) + label = request.get("label", None) + + if public_key is None: # pragma: no cover + return { + "success": False, + "error": KEYCHAIN_ERR_MALFORMED_REQUEST, + "error_details": {"message": "missing public_key"}, + } + + try: + pk = self.get_keychain_for_request(request).add_public_key(public_key, label) + except KeyError as e: # pragma: no cover + return { + "success": False, + "error": KEYCHAIN_ERR_KEYERROR, + "error_details": {"message": f"The word '{e.args[0]}' is incorrect.'", "word": e.args[0]}, + } + except ValueError as e: # pragma: no cover + log.exception(e) + return { + "success": False, + "error": str(e), + } + + return {"success": True, "fingerprint": pk.get_fingerprint()} + async def check_keys(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.get_keychain_for_request(request).is_keyring_locked(): return {"success": False, "error": KEYCHAIN_ERR_LOCKED} @@ -352,3 +389,26 @@ async def get_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[str, An return {"success": True, "pk": bytes(private_key.get_g1()).hex(), "entropy": entropy.hex()} else: return {"success": False, "error": KEYCHAIN_ERR_KEY_NOT_FOUND} + + async def get_public_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[str, Any]: + if self.get_keychain_for_request(request).is_keyring_locked(): # pragma: no cover + return {"success": False, "error": KEYCHAIN_ERR_LOCKED} + + public_keys = self.get_keychain_for_request(request).get_all_public_keys() + if len(public_keys) == 0: # pragma: no cover + return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} + + fingerprint = request.get("fingerprint", None) + public_key: Optional[G1Element] = None + if fingerprint is not None: + for pk in public_keys: + if pk.get_fingerprint() == fingerprint: + public_key = pk + break + else: + public_key = public_keys[0] + + if public_key is not None: + return {"success": True, "pk": bytes(public_key).hex()} + else: + return {"success": False, "error": KEYCHAIN_ERR_KEY_NOT_FOUND} diff --git a/chia/daemon/server.py b/chia/daemon/server.py index cc09d4e76e34..0c594a6f0f5b 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -46,10 +46,10 @@ from chia.util.setproctitle import setproctitle from chia.util.ws_message import WsRpcMessage, create_payload, format_response from chia.wallet.derive_keys import ( + master_pk_to_wallet_pk_unhardened, master_sk_to_farmer_sk, master_sk_to_pool_sk, master_sk_to_wallet_sk, - master_sk_to_wallet_sk_unhardened, ) io_pool_exc = ThreadPoolExecutor() @@ -633,16 +633,18 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict for key in keys: address_entries = [] - # we require access to the private key to generate wallet addresses - if key.secrets is None: + # we require access to the private key to generate wallet addresses for non observer + if key.secrets is None and non_observer_derivation: return {"success": False, "error": f"missing private key for key with fingerprint {key.fingerprint}"} for i in range(index, index + count): if non_observer_derivation: - sk = master_sk_to_wallet_sk(key.secrets.private_key, uint32(i)) + # semantics above guarantee that key.secrets is not None here + sk = master_sk_to_wallet_sk(key.secrets.private_key, uint32(i)) # type: ignore[union-attr] + pk = sk.get_g1() else: - sk = master_sk_to_wallet_sk_unhardened(key.secrets.private_key, uint32(i)) - wallet_address = encode_puzzle_hash(create_puzzlehash_for_pk(sk.get_g1()), prefix) + pk = master_pk_to_wallet_pk_unhardened(key.public_key, uint32(i)) + wallet_address = encode_puzzle_hash(create_puzzlehash_for_pk(pk), prefix) if non_observer_derivation: hd_path = f"m/12381n/8444n/2n/{i}n" else: @@ -663,6 +665,8 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: Dic keys_for_plot: Dict[uint32, Any] = {} for key in keys: + if key.secrets is None and fingerprints is None: + continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() pool_public_key: G1Element = master_sk_to_pool_sk(sk).get_g1() diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 7e76635c896f..41336b9aa827 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -13,6 +13,7 @@ from typing_extensions import final from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.byte_types import hexstr_to_bytes from chia.util.errors import ( KeychainException, KeychainFingerprintExists, @@ -307,13 +308,16 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE]) fingerprint = public_key.get_fingerprint() - entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + if len(str_bytes) == G1Element.SIZE + 32: + entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + else: + entropy = None return KeyData( fingerprint=uint32(fingerprint), public_key=public_key, label=self.keyring_wrapper.get_label(fingerprint), - secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets else None, + secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, ) def _get_free_private_key_index(self) -> int: @@ -362,6 +366,36 @@ def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> Private return key + def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: + """ + Adds a public key to the keychain. + """ + key = G1Element.from_bytes(hexstr_to_bytes(pubkey)) + index = self._get_free_private_key_index() + fingerprint = key.get_fingerprint() + + if fingerprint in [pk.get_fingerprint() for pk in self.get_all_public_keys()]: + # Prevents duplicate add + raise KeychainFingerprintExists(fingerprint) + + # Try to set the label first, it may fail if the label is invalid or already exists. + # This can probably just be moved into `FileKeyring.set_passphrase` after the legacy keyring stuff was dropped. + if label is not None: + self.keyring_wrapper.set_label(fingerprint, label) + + try: + self.keyring_wrapper.set_passphrase( + self.service, + get_private_key_user(self.user, index), + bytes(key).hex(), + ) + except Exception: # pragma: no cover + if label is not None: + self.keyring_wrapper.delete_label(fingerprint) + raise + + return key + def set_label(self, fingerprint: int, label: str) -> None: """ Assigns the given label to the first key with the given fingerprint. @@ -410,7 +444,7 @@ def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: try: key_data = self._get_key_data(index) all_keys.append((key_data.private_key, key_data.entropy)) - except KeychainUserNotFound: + except (KeychainUserNotFound, KeychainSecretsMissing): pass return all_keys @@ -444,7 +478,14 @@ def get_all_public_keys(self) -> List[G1Element]: """ Returns all public keys. """ - return [key_data[0].get_g1() for key_data in self.get_all_private_keys()] + all_keys: List[G1Element] = [] + for index in range(MAX_KEYS + 1): + try: + key_data = self._get_key_data(index) + all_keys.append(key_data.public_key) + except KeychainUserNotFound: + pass + return all_keys def get_first_public_key(self) -> Optional[G1Element]: """ diff --git a/chia/wallet/derive_keys.py b/chia/wallet/derive_keys.py index f85801df0054..0f20a223f1f0 100644 --- a/chia/wallet/derive_keys.py +++ b/chia/wallet/derive_keys.py @@ -30,6 +30,12 @@ def _derive_path_unhardened(sk: PrivateKey, path: List[int]) -> PrivateKey: return sk +def _derive_pk_unhardened(pk: G1Element, path: List[int]) -> G1Element: + for index in path: + pk = AugSchemeMPL.derive_child_pk_unhardened(pk, index) + return pk + + def master_sk_to_farmer_sk(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 0, 0]) @@ -51,11 +57,20 @@ def master_sk_to_wallet_sk_unhardened_intermediate(master: PrivateKey) -> Privat return _derive_path_unhardened(master, [12381, 8444, 2]) +def master_pk_to_wallet_pk_unhardened_intermediate(master: G1Element) -> G1Element: + return _derive_pk_unhardened(master, [12381, 8444, 2]) + + def master_sk_to_wallet_sk_unhardened(master: PrivateKey, index: uint32) -> PrivateKey: intermediate = master_sk_to_wallet_sk_unhardened_intermediate(master) return _derive_path_unhardened(intermediate, [index]) +def master_pk_to_wallet_pk_unhardened(master: G1Element, index: uint32) -> G1Element: + intermediate = master_pk_to_wallet_pk_unhardened_intermediate(master) + return _derive_pk_unhardened(intermediate, [index]) + + def master_sk_to_local_sk(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 3, 0]) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index d9733c356a11..a9161616c845 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -645,14 +645,30 @@ async def test_get_routes(mock_lonely_daemon): }, ), WalletAddressCase( - id="missing private key", - request={"fingerprints": [test_key_data.fingerprint]}, + id="missing private key hardened", + request={"fingerprints": [test_key_data.fingerprint], "non_observer_derivation": True}, response={ "success": False, "error": f"missing private key for key with fingerprint {test_key_data.fingerprint}", }, pubkeys_only=True, ), + WalletAddressCase( + id="missing private key unhardened", + request={"fingerprints": [test_key_data.fingerprint]}, + response={ + "success": True, + "wallet_addresses": { + test_key_data.fingerprint: [ + { + "address": "xch1zze67l3jgxuvyaxhjhu7326sezxxve7lgzvq0497ddggzhff7c9s2pdcwh", + "hd_path": "m/12381/8444/2/0", + }, + ], + }, + }, + pubkeys_only=True, + ), ) @pytest.mark.anyio async def test_get_wallet_addresses( diff --git a/tests/core/daemon/test_keychain_proxy.py b/tests/core/daemon/test_keychain_proxy.py index 1f3391197f9c..4355a48f199c 100644 --- a/tests/core/daemon/test_keychain_proxy.py +++ b/tests/core/daemon/test_keychain_proxy.py @@ -9,6 +9,7 @@ from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate from chia.simulator.block_tools import BlockTools from chia.simulator.setup_services import setup_daemon +from chia.util.errors import KeychainKeyNotFound from chia.util.keychain import KeyData TEST_KEY_1 = KeyData.generate(label="🚽🍯") @@ -41,6 +42,29 @@ async def test_add_private_key(keychain_proxy: KeychainProxy) -> None: assert key == TEST_KEY_3 +@pytest.mark.anyio +async def test_add_public_key(keychain_proxy: KeychainProxy) -> None: + keychain = keychain_proxy + await keychain.add_public_key(bytes(TEST_KEY_3.public_key).hex(), TEST_KEY_3.label) + with pytest.raises(Exception, match="already exists"): + await keychain.add_public_key(bytes(TEST_KEY_3.public_key).hex(), "") + key = await keychain.get_key(TEST_KEY_3.fingerprint, include_secrets=False) + assert key is not None + assert key.public_key == TEST_KEY_3.public_key + assert key.secrets is None + + pk = await keychain.get_public_key_for_fingerprint(TEST_KEY_3.fingerprint) + assert pk is not None + assert pk == TEST_KEY_3.public_key + + pk = await keychain.get_public_key_for_fingerprint(None) + assert pk is not None + assert pk == TEST_KEY_3.public_key + + with pytest.raises(KeychainKeyNotFound): + pk = await keychain.get_public_key_for_fingerprint(1234567890) + + @pytest.mark.parametrize("include_secrets", [True, False]) @pytest.mark.anyio async def test_get_key(keychain_proxy_with_keys: KeychainProxy, include_secrets: bool) -> None: From 9302748471d8fd09879acd4e6279fe7d39e20d2d Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 12:48:03 -0800 Subject: [PATCH 033/274] this is the new quex.private_key_optional --- chia/cmds/keys_funcs.py | 40 +++++--- chia/pools/pool_wallet.py | 3 +- chia/rpc/wallet_rpc_api.py | 2 +- chia/wallet/wallet.py | 31 +++--- chia/wallet/wallet_node.py | 41 +++++--- chia/wallet/wallet_state_manager.py | 104 ++++++++++----------- tests/pools/test_pool_rpc.py | 4 +- tests/wallet/cat_wallet/test_cat_wallet.py | 7 +- tests/wallet/rpc/test_wallet_rpc.py | 14 ++- tests/wallet/test_sign_coin_spends.py | 1 + tests/wallet/test_signer_protocol.py | 2 +- tests/wallet/test_wallet.py | 4 +- tests/wallet/test_wallet_node.py | 104 +++++++++++++++++++-- tests/wallet/test_wallet_state_manager.py | 2 +- 14 files changed, 244 insertions(+), 115 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 84d2fed8053c..b019b1148df1 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -20,6 +20,7 @@ from chia.util.keychain import Keychain, KeyData, bytes_to_mnemonic, generate_mnemonic, mnemonic_to_seed from chia.util.keyring_wrapper import KeyringWrapper from chia.wallet.derive_keys import ( + master_pk_to_wallet_pk_unhardened, master_sk_to_farmer_sk, master_sk_to_pool_sk, master_sk_to_wallet_sk, @@ -155,27 +156,44 @@ def show_keys( def process_key_data(key_data: KeyData) -> Dict[str, Any]: key: Dict[str, Any] = {} - sk = key_data.private_key + sk = key_data.private_key if key_data.secrets is not None else None if key_data.label is not None: key["label"] = key_data.label key["fingerprint"] = key_data.fingerprint key["master_pk"] = bytes(key_data.public_key).hex() - key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() - key["pool_pk"] = bytes(master_sk_to_pool_sk(sk).get_g1()).hex() - first_wallet_sk: PrivateKey = ( - master_sk_to_wallet_sk(sk, uint32(0)) - if non_observer_derivation - else master_sk_to_wallet_sk_unhardened(sk, uint32(0)) - ) - wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_sk.get_g1()), prefix) - key["wallet_address"] = wallet_address + if sk is not None: + key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() + key["pool_pk"] = bytes(master_sk_to_pool_sk(sk).get_g1()).hex() + else: + key["farmer_pk"] = "N/A" + key["pool_pk"] = "N/A" + + if non_observer_derivation: + if sk is None: + first_wallet_pk: Optional[G1Element] = None + else: + first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).get_g1() + else: + first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.public_key, uint32(0)) + + if first_wallet_pk is not None: + wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_pk), prefix) + key["wallet_address"] = wallet_address + else: + key["wallet_address"] = "N/A" + key["non_observer"] = non_observer_derivation - if show_mnemonic: + if show_mnemonic and sk is not None: key["master_sk"] = bytes(sk).hex() key["wallet_sk"] = bytes(master_sk_to_wallet_sk(sk, uint32(0))).hex() key["mnemonic"] = bytes_to_mnemonic(key_data.entropy) + else: + key["master_sk"] = "N/A" + key["wallet_sk"] = "N/A" + key["mnemonic"] = "N/A" + return key keys = [process_key_data(key) for key in all_keys] diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 200654a1edd0..212f12f1f9ea 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -469,7 +469,8 @@ async def create_new_pool_wallet_transaction( async def _get_owner_key_cache(self) -> Tuple[PrivateKey, uint32]: if self._owner_sk_and_index is None: self._owner_sk_and_index = find_owner_sk( - [self.wallet_state_manager.private_key], (await self.get_current_state()).current.owner_pubkey + [self.wallet_state_manager.get_master_private_key()], + (await self.get_current_state()).current.owner_pubkey, ) assert self._owner_sk_and_index is not None return self._owner_sk_and_index diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 56796379ef89..45e618abffda 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -900,7 +900,7 @@ async def create_new_wallet( 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) + self.service.wallet_state_manager.get_master_private_key(), uint32(max_pwi + 1) ) owner_pk: G1Element = owner_sk.get_g1() diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index e4d072ff7cd1..adb9916454c0 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -529,18 +529,19 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash( puzzle_hash_for_synthetic_public_key(pk_parsed) ) - root_pubkey: bytes = self.wallet_state_manager.private_key.get_g1().get_fingerprint().to_bytes(4, "big") + root_pubkey: bytes = self.wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big") if index is None: # Pool wallet may have a secret key here - for pool_wallet_index in range(MAX_POOL_WALLETS): - try_owner_sk = master_sk_to_singleton_owner_sk( - self.wallet_state_manager.private_key, uint32(pool_wallet_index) - ) - if try_owner_sk.get_g1() == pk_parsed: - return PathHint( - root_pubkey, - [uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)], + if self.wallet_state_manager.private_key is not None: + for pool_wallet_index in range(MAX_POOL_WALLETS): + try_owner_sk = master_sk_to_singleton_owner_sk( + self.wallet_state_manager.private_key, uint32(pool_wallet_index) ) + if try_owner_sk.get_g1() == pk_parsed: + return PathHint( + root_pubkey, + [uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)], + ) return None return PathHint( root_pubkey, @@ -550,9 +551,11 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: - root_pubkey: G1Element = self.wallet_state_manager.private_key.get_g1() + root_pubkey: G1Element = self.wallet_state_manager.root_pubkey pk_lookup: Dict[int, G1Element] = {root_pubkey.get_fingerprint(): root_pubkey} - sk_lookup: Dict[int, PrivateKey] = {root_pubkey.get_fingerprint(): self.wallet_state_manager.private_key} + sk_lookup: Dict[int, PrivateKey] = { + root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key() + } for path_hint in signing_instructions.key_hints.path_hints: if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint(): @@ -562,8 +565,10 @@ async def execute_signing_instructions( continue else: path = [int(step) for step in path_hint.path] - derive_child_sk = _derive_path(self.wallet_state_manager.private_key, path) - derive_child_sk_unhardened = _derive_path_unhardened(self.wallet_state_manager.private_key, path) + derive_child_sk = _derive_path(self.wallet_state_manager.get_master_private_key(), path) + derive_child_sk_unhardened = _derive_path_unhardened( + self.wallet_state_manager.get_master_private_key(), path + ) derive_child_pk = derive_child_sk.get_g1() derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1() pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 349c48101664..ab9d3f8fb17f 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -220,11 +220,16 @@ def rollback_request_caches(self, reorg_height: int) -> None: for cache in self.peer_caches.values(): cache.clear_after_height(reorg_height) - async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: + async def get_key_for_fingerprint( + self, fingerprint: Optional[int], private: bool = False + ) -> Optional[Union[PrivateKey, G1Element]]: try: keychain_proxy = await self.ensure_keychain_proxy() - # Returns first private key if fingerprint is None - key = await keychain_proxy.get_key_for_fingerprint(fingerprint) + # Returns first key if fingerprint is None + if private: + key: Optional[Union[PrivateKey, G1Element]] = await keychain_proxy.get_key_for_fingerprint(fingerprint) + else: + key = await keychain_proxy.get_public_key_for_fingerprint(fingerprint) except KeychainIsEmpty: self.log.warning("No keys present. Create keys with the UI, or with the 'chia keys' program.") return None @@ -241,18 +246,22 @@ async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ return key - async def get_private_key(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: + async def get_key(self, fingerprint: Optional[int], private: bool = True) -> Optional[Union[PrivateKey, G1Element]]: """ Attempt to get the private key for the given fingerprint. If the fingerprint is None, get_key_for_fingerprint() will return the first private key. Similarly, if a key isn't returned for the provided fingerprint, the first key will be returned. """ - key: Optional[PrivateKey] = await self.get_key_for_fingerprint(fingerprint) + key: Optional[Union[PrivateKey, G1Element]] = await self.get_key_for_fingerprint(fingerprint, private=private) if key is None and fingerprint is not None: - key = await self.get_key_for_fingerprint(None) + key = await self.get_key_for_fingerprint(None, private=private) if key is not None: - self.log.info(f"Using first key found (fingerprint: {key.get_g1().get_fingerprint()})") + if isinstance(key, PrivateKey): + fp = key.get_g1().get_fingerprint() + else: + fp = key.get_fingerprint() + self.log.info(f"Using first key found (fingerprint: {fp})") return key @@ -378,13 +387,19 @@ async def _start_with_fingerprint( multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self._weight_proof_handler = WalletWeightProofHandler(self.constants, multiprocessing_context) self.synced_peers = set() - private_key = await self.get_private_key(fingerprint) + private_key = await self.get_key(fingerprint, private=True) if private_key is None: + public_key = await self.get_key(fingerprint, private=False) + else: + assert isinstance(private_key, PrivateKey) + public_key = private_key.get_g1() + if public_key is None: self.log_out() return False + assert isinstance(public_key, G1Element) # override with private key fetched in case it's different from what was passed if fingerprint is None: - fingerprint = private_key.get_g1().get_fingerprint() + fingerprint = public_key.get_fingerprint() if self.config.get("enable_profiler", False): if sys.getprofile() is not None: self.log.warning("not enabling profiler, getprofile() is already set") @@ -399,6 +414,7 @@ async def _start_with_fingerprint( if self.config.get("reset_sync_for_fingerprint") == fingerprint: await self.reset_sync_db(path, fingerprint) + assert private_key is None or isinstance(private_key, PrivateKey) self._wallet_state_manager = await WalletStateManager.create( private_key, self.config, @@ -407,6 +423,7 @@ async def _start_with_fingerprint( self.server, self.root_path, self, + public_key, ) if self.state_changed_callback is not None: @@ -420,7 +437,7 @@ async def _start_with_fingerprint( self._retry_failed_states_task = asyncio.create_task(self._retry_failed_states()) self.sync_event = asyncio.Event() - self.log_in(private_key) + self.log_in(fingerprint) self.wallet_state_manager.state_changed("sync_changed") # Populate the balance caches for all wallets @@ -620,8 +637,8 @@ async def _process_new_subscriptions(self) -> None: if peer is not None: await peer.close(9999) - def log_in(self, sk: PrivateKey) -> None: - self.logged_in_fingerprint = sk.get_g1().get_fingerprint() + def log_in(self, fingerprint: int) -> None: + self.logged_in_fingerprint = fingerprint self.logged_in = True self.log.info(f"Wallet is logged in using key with fingerprint: {self.logged_in_fingerprint}") try: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index d5f06c543456..8ec1651ae158 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -86,11 +86,12 @@ from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import ( _derive_path, - _derive_path_unhardened, + _derive_pk_unhardened, + master_pk_to_wallet_pk_unhardened, + master_pk_to_wallet_pk_unhardened_intermediate, 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 @@ -105,10 +106,6 @@ 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, -) 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.offer import Offer @@ -195,7 +192,8 @@ class WalletStateManager: main_wallet: Wallet wallets: Dict[uint32, WalletProtocol[Any]] - private_key: PrivateKey + private_key: Optional[PrivateKey] + root_pubkey: G1Element trade_manager: TradeManager notification_manager: NotificationManager @@ -216,13 +214,14 @@ class WalletStateManager: @staticmethod async def create( - private_key: PrivateKey, + private_key: Optional[PrivateKey], config: Dict[str, Any], db_path: Path, constants: ConsensusConstants, server: ChiaServer, root_path: Path, wallet_node: WalletNode, + root_pubkey: Optional[G1Element] = None, ) -> WalletStateManager: self = WalletStateManager() @@ -233,7 +232,6 @@ async def create( 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") @@ -272,13 +270,26 @@ async def create( 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 + if private_key is None: # pragma: no cover + if root_pubkey is None: + raise ValueError("WalletStateManager requires either a root private key or root public key") + else: + self.root_pubkey = root_pubkey + else: + calculated_root_public_key: G1Element = private_key.get_g1() + if root_pubkey is not None: + assert root_pubkey == calculated_root_public_key + self.root_pubkey = calculated_root_public_key + + fingerprint = self.root_pubkey.get_fingerprint() + puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, []) + self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators) + self.main_wallet = await Wallet.create(self, main_wallet_info) self.wallets = {main_wallet_info.id: self.main_wallet} @@ -353,34 +364,21 @@ async def create( return self def get_public_key_unhardened(self, index: uint32) -> G1Element: - return master_sk_to_wallet_sk_unhardened(self.private_key, index).get_g1() + return master_pk_to_wallet_pk_unhardened(self.root_pubkey, index) 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 master_sk_to_wallet_sk(self.get_master_private_key(), record.index) + return master_sk_to_wallet_sk_unhardened(self.get_master_private_key(), record.index) - return calculate_synthetic_secret_key(base_key, DEFAULT_HIDDEN_PUZZLE_HASH) + def get_master_private_key(self) -> PrivateKey: + if self.private_key is None: # pragma: no cover + raise ValueError("Wallet is currently in observer mode and access to private key is denied") - 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) + return self.private_key def get_wallet(self, id: uint32, required_type: Type[TWalletType]) -> TWalletType: wallet = self.wallets[id] @@ -446,36 +444,32 @@ async def create_more_puzzle_hashes( 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) + if self.private_key is not None: + intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) + intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.root_pubkey) 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, + if self.private_key is not None: + # Hardened + pubkey: G1Element = _derive_path(intermediate_sk, [index]).get_g1() + puzzlehash: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) + 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 + pubkey_unhardened: G1Element = _derive_pk_unhardened(intermediate_pk_un, [index]) + puzzlehash_unhardened: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey_unhardened) self.log.debug( f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" ) diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index d3c8b6d54e17..a51eace8608f 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -393,11 +393,11 @@ def mempool_empty() -> bool: status: PoolWalletInfo = (await client.pw_status(some_wallet.id()))[0] assert (await some_wallet.get_pool_wallet_index()) < 5 auth_sk = find_authentication_sk( - [some_wallet.wallet_state_manager.private_key], status.current.owner_pubkey + [some_wallet.wallet_state_manager.get_master_private_key()], status.current.owner_pubkey ) assert auth_sk is not None owner_sk = find_owner_sk( - [some_wallet.wallet_state_manager.private_key], status.current.owner_pubkey + [some_wallet.wallet_state_manager.get_master_private_key()], status.current.owner_pubkey ) assert owner_sk is not None assert owner_sk[0] != auth_sk diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index b428018891f9..cf9db61f1b36 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -27,7 +27,7 @@ from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.derivation_record import DerivationRecord -from chia.wallet.derive_keys import _derive_path_unhardened, master_sk_to_wallet_sk_unhardened_intermediate +from chia.wallet.derive_keys import master_pk_to_wallet_pk_unhardened from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_pk from chia.wallet.transaction_record import TransactionRecord @@ -928,10 +928,9 @@ async def test_cat_change_detection( # Mint CAT to ourselves, immediately spend it to an unhinted puzzle hash that we have manually added to the DB # We should pick up this coin as balance even though it is unhinted because it is "change" - intermediate_sk_un = master_sk_to_wallet_sk_unhardened_intermediate( - wallet_node_0.wallet_state_manager.private_key + pubkey_unhardened = master_pk_to_wallet_pk_unhardened( + wallet_node_0.wallet_state_manager.root_pubkey, uint32(100000000) ) - pubkey_unhardened = _derive_path_unhardened(intermediate_sk_un, [100000000]).get_g1() inner_puzhash = puzzle_hash_for_pk(pubkey_unhardened) puzzlehash_unhardened = construct_cat_puzzle( CAT_MOD, diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 67d0731c8967..605aa32f51bd 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -10,7 +10,7 @@ import aiosqlite import pytest -from chia_rs import G2Element +from chia_rs import G2Element, PrivateKey from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.consensus.coinbase import create_puzzlehash_for_pk @@ -1673,14 +1673,16 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn # Add in reward addresses into farmer and pool for testing delete key checks # set farmer to first private key - sk = await wallet_node.get_key_for_fingerprint(pks[0]) + sk = await wallet_node.get_key_for_fingerprint(pks[0], private=True) assert sk is not None + assert isinstance(sk, PrivateKey) test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()) with lock_and_load_config(wallet_node.root_path, "config.yaml") as test_config: test_config["farmer"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") # set pool to second private key - sk = await wallet_node.get_key_for_fingerprint(pks[1]) + sk = await wallet_node.get_key_for_fingerprint(pks[1], private=True) assert sk is not None + assert isinstance(sk, PrivateKey) test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()) test_config["pool"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") save_config(wallet_node.root_path, "config.yaml", test_config) @@ -1705,14 +1707,16 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn # Add in observer reward addresses into farmer and pool for testing delete key checks # set farmer to first private key - sk = await wallet_node.get_key_for_fingerprint(pks[0]) + sk = await wallet_node.get_key_for_fingerprint(pks[0], private=True) assert sk is not None + assert isinstance(sk, PrivateKey) test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk_unhardened(sk, uint32(0)).get_g1()) with lock_and_load_config(wallet_node.root_path, "config.yaml") as test_config: test_config["farmer"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") # set pool to second private key - sk = await wallet_node.get_key_for_fingerprint(pks[1]) + sk = await wallet_node.get_key_for_fingerprint(pks[1], private=True) assert sk is not None + assert isinstance(sk, PrivateKey) test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk_unhardened(sk, uint32(0)).get_g1()) test_config["pool"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") save_config(wallet_node.root_path, "config.yaml", test_config) diff --git a/tests/wallet/test_sign_coin_spends.py b/tests/wallet/test_sign_coin_spends.py index d20527a22d7a..007427eb19dc 100644 --- a/tests/wallet/test_sign_coin_spends.py +++ b/tests/wallet/test_sign_coin_spends.py @@ -75,6 +75,7 @@ async def test_wsm_sign_transaction() -> None: wsm.puzzle_store = await WalletPuzzleStore.create(db) wsm.constants = DEFAULT_CONSTANTS wsm.private_key = top_sk + wsm.root_pubkey = top_sk.get_g1() wsm.user_store = await WalletUserStore.create(db) wallet_info = await wsm.user_store.get_wallet_by_id(1) assert wallet_info is not None diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 40375ed8308c..585228aebf2e 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -192,7 +192,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ] assert utx.signing_instructions.key_hints.path_hints == [ PathHint( - wallet_state_manager.private_key.get_g1().get_fingerprint().to_bytes(4, "big"), + wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big"), [uint64(12381), uint64(8444), uint64(2), uint64(derivation_record.index)], ) ] diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index af58b59956d5..c469cccb8ce2 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -1573,7 +1573,9 @@ async def test_address_sliding_window( puzzle_hashes = [] for i in range(211): - pubkey = master_sk_to_wallet_sk(wallet_node.wallet_state_manager.private_key, uint32(i)).get_g1() + pubkey = master_sk_to_wallet_sk( + wallet_node.wallet_state_manager.get_master_private_key(), uint32(i) + ).get_g1() puzzle: Program = wallet.puzzle_for_pk(pubkey) puzzle_hash: bytes32 = puzzle.get_tree_hash() puzzle_hashes.append(puzzle_hash) diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index 35b8aafd851a..149e5b77719c 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional import pytest -from chia_rs import PrivateKey +from chia_rs import G1Element, PrivateKey from chia.protocols import wallet_protocol from chia.protocols.protocol_message_types import ProtocolMessageTypes @@ -42,9 +42,10 @@ async def test_get_private_key(root_path_populated_with_config: Path, get_temp_k sk: PrivateKey = keychain.add_private_key(generate_mnemonic()) fingerprint: int = sk.get_g1().get_fingerprint() - key = await node.get_private_key(fingerprint) + key = await node.get_key(fingerprint) assert key is not None + assert isinstance(key, PrivateKey) assert key.get_g1().get_fingerprint() == fingerprint @@ -62,9 +63,10 @@ async def test_get_private_key_default_key(root_path_populated_with_config: Path keychain.add_private_key(generate_mnemonic()) # When no fingerprint is provided, we should get the default (first) key - key = await node.get_private_key(None) + key = await node.get_key(None) assert key is not None + assert isinstance(key, PrivateKey) assert key.get_g1().get_fingerprint() == fingerprint @@ -79,7 +81,7 @@ async def test_get_private_key_missing_key( node: WalletNode = WalletNode(config, root_path, test_constants, keychain) # Keyring is empty, so requesting a key by fingerprint or None should return None - key = await node.get_private_key(fingerprint) + key = await node.get_key(fingerprint) assert key is None @@ -99,12 +101,98 @@ async def test_get_private_key_missing_key_use_default( assert fingerprint != 1234567890 # When fingerprint is provided and the key is missing, we should get the default (first) key - key = await node.get_private_key(1234567890) + key = await node.get_key(1234567890) assert key is not None + assert isinstance(key, PrivateKey) assert key.get_g1().get_fingerprint() == fingerprint +@pytest.mark.anyio +async def test_get_public_key(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: + root_path: Path = root_path_populated_with_config + keychain: Keychain = get_temp_keyring + config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") + node: WalletNode = WalletNode(config, root_path, test_constants, keychain) + pk: G1Element = keychain.add_public_key( + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + fingerprint: int = pk.get_fingerprint() + + key = await node.get_key(fingerprint, private=False) + + assert key is not None + assert isinstance(key, G1Element) + assert key.get_fingerprint() == fingerprint + + +@pytest.mark.anyio +async def test_get_public_key_default_key(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: + root_path: Path = root_path_populated_with_config + keychain: Keychain = get_temp_keyring + config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") + node: WalletNode = WalletNode(config, root_path, test_constants, keychain) + pk: G1Element = keychain.add_public_key( + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + fingerprint: int = pk.get_fingerprint() + + # Add a couple more keys + keychain.add_public_key( + "83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454e9ce1033f4cba8824259dc17dc3b643ab1003e6b961" + ) + keychain.add_public_key( + "a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e3d4d1acfa5d63d6542336c7b24d2fceab991919e989" + ) + + # When no fingerprint is provided, we should get the default (first) key + key = await node.get_key(None, private=False) + + assert key is not None + assert isinstance(key, G1Element) + assert key.get_fingerprint() == fingerprint + + +@pytest.mark.anyio +@pytest.mark.parametrize("fingerprint", [None, 1234567890]) +async def test_get_public_key_missing_key( + root_path_populated_with_config: Path, get_temp_keyring: Keychain, fingerprint: Optional[int] +) -> None: + root_path: Path = root_path_populated_with_config + keychain: Keychain = get_temp_keyring # empty keyring + config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") + node: WalletNode = WalletNode(config, root_path, test_constants, keychain) + + # Keyring is empty, so requesting a key by fingerprint or None should return None + key = await node.get_key(fingerprint, private=False) + + assert key is None + + +@pytest.mark.anyio +async def test_get_public_key_missing_key_use_default( + root_path_populated_with_config: Path, get_temp_keyring: Keychain +) -> None: + root_path: Path = root_path_populated_with_config + keychain: Keychain = get_temp_keyring + config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") + node: WalletNode = WalletNode(config, root_path, test_constants, keychain) + pk: G1Element = keychain.add_public_key( + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + fingerprint: int = pk.get_fingerprint() + + # Stupid sanity check that the fingerprint we're going to use isn't actually in the keychain + assert fingerprint != 1234567890 + + # When fingerprint is provided and the key is missing, we should get the default (first) key + key = await node.get_key(1234567890, private=False) + + assert key is not None + assert isinstance(key, G1Element) + assert key.get_fingerprint() == fingerprint + + def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: root_path: Path = root_path_populated_with_config keychain: Keychain = get_temp_keyring @@ -113,7 +201,7 @@ def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychai sk: PrivateKey = keychain.add_private_key(generate_mnemonic()) fingerprint: int = sk.get_g1().get_fingerprint() - node.log_in(sk) + node.log_in(fingerprint) assert node.logged_in is True assert node.logged_in_fingerprint == fingerprint @@ -140,7 +228,7 @@ def patched_update_last_used_fingerprint(self: Any) -> None: fingerprint: int = sk.get_g1().get_fingerprint() # Expect log_in to succeed, even though we can't write the last used fingerprint - node.log_in(sk) + node.log_in(fingerprint) assert node.logged_in is True assert node.logged_in_fingerprint == fingerprint @@ -156,7 +244,7 @@ def test_log_out(root_path_populated_with_config: Path, get_temp_keyring: Keycha sk: PrivateKey = keychain.add_private_key(generate_mnemonic()) fingerprint: int = sk.get_g1().get_fingerprint() - node.log_in(sk) + node.log_in(fingerprint) assert node.logged_in is True assert node.logged_in_fingerprint == fingerprint diff --git a/tests/wallet/test_wallet_state_manager.py b/tests/wallet/test_wallet_state_manager.py index 7bd7ffa5258b..4cdc67e94a6b 100644 --- a/tests/wallet/test_wallet_state_manager.py +++ b/tests/wallet/test_wallet_state_manager.py @@ -61,7 +61,7 @@ async def test_get_private_key(simulator_and_wallet: SimulatorsAndWallets, harde wallet_state_manager: WalletStateManager = wallet_node.wallet_state_manager derivation_index = uint32(10000) conversion_method = master_sk_to_wallet_sk if hardened else master_sk_to_wallet_sk_unhardened - expected_private_key = conversion_method(wallet_state_manager.private_key, derivation_index) + expected_private_key = conversion_method(wallet_state_manager.get_master_private_key(), derivation_index) record = DerivationRecord( derivation_index, bytes32(b"0" * 32), From 3c1b050f37da9d922325315de764153128aa38af Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 09:41:12 -0800 Subject: [PATCH 034/274] Update key functions to handle observer only functionality --- chia/cmds/keys.py | 38 ++- chia/cmds/keys_funcs.py | 233 ++++++++----- chia/daemon/server.py | 5 +- tests/core/cmds/test_keys.py | 615 +++++++++++++++++++++++++++++++++-- 4 files changed, 757 insertions(+), 134 deletions(-) diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index 976c7e2cfafe..62f25c5131c0 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -73,12 +73,12 @@ def show_cmd( show_keys(ctx.obj["root_path"], show_mnemonic_seed, non_observer_derivation, json, fingerprint) -@keys_cmd.command("add", help="Add a private key by mnemonic") +@keys_cmd.command("add", help="Add a private key by mnemonic or public key as hex") @click.option( "--filename", "-f", default=None, - help="The filename containing the secret key mnemonic to add", + help="The filename containing the secret key mnemonic or public key hex to add", type=str, required=False, ) @@ -93,15 +93,15 @@ def show_cmd( @click.pass_context def add_cmd(ctx: click.Context, filename: str, label: Optional[str]) -> None: from .init_funcs import check_keys - from .keys_funcs import query_and_add_private_key_seed + from .keys_funcs import query_and_add_key_info - mnemonic = None + mnemonic_or_pk = None if filename: from pathlib import Path - mnemonic = Path(filename).read_text().rstrip() + mnemonic_or_pk = Path(filename).read_text().rstrip() - query_and_add_private_key_seed(mnemonic, label) + query_and_add_key_info(mnemonic_or_pk, label) check_keys(ctx.obj["root_path"]) @@ -318,21 +318,19 @@ def search_cmd( ) -> None: import sys - from chia_rs import PrivateKey - from .keys_funcs import resolve_derivation_master_key, search_derive - private_key: Optional[PrivateKey] = None fingerprint: Optional[int] = ctx.obj.get("fingerprint", None) filename: Optional[str] = ctx.obj.get("filename", None) # Specifying the master key is optional for the search command. If not specified, we'll search all keys. - if fingerprint is not None or filename is not None: - private_key = resolve_derivation_master_key(filename if filename is not None else fingerprint) + sk = None + if fingerprint is None and filename is not None: + sk = resolve_derivation_master_key(filename) found: bool = search_derive( ctx.obj["root_path"], - private_key, + fingerprint, search_terms, limit, non_observer_derivation, @@ -340,6 +338,7 @@ def search_cmd( ("all",) if "all" in search_type else search_type, derive_from_hd_path, prefix, + sk, ) sys.exit(0 if found else 1) @@ -375,10 +374,13 @@ def wallet_address_cmd( fingerprint: Optional[int] = ctx.obj.get("fingerprint", None) filename: Optional[str] = ctx.obj.get("filename", None) - private_key = resolve_derivation_master_key(filename if filename is not None else fingerprint) + + sk = None + if fingerprint is None and filename is not None: + sk = resolve_derivation_master_key(filename) derive_wallet_address( - ctx.obj["root_path"], private_key, index, count, prefix, non_observer_derivation, show_hd_path + ctx.obj["root_path"], fingerprint, index, count, prefix, non_observer_derivation, show_hd_path, sk ) @@ -443,10 +445,13 @@ def child_key_cmd( fingerprint: Optional[int] = ctx.obj.get("fingerprint", None) filename: Optional[str] = ctx.obj.get("filename", None) - private_key = resolve_derivation_master_key(filename if filename is not None else fingerprint) + + sk = None + if fingerprint is None and filename is not None: + sk = resolve_derivation_master_key(filename) derive_child_key( - private_key, + fingerprint, key_type, derive_from_hd_path.lower() if derive_from_hd_path is not None else None, index, @@ -454,4 +459,5 @@ def child_key_cmd( non_observer_derivation, show_private_keys, show_hd_path, + sk, ) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index b019b1148df1..69a6ed2b8f97 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -24,7 +24,6 @@ master_sk_to_farmer_sk, master_sk_to_pool_sk, master_sk_to_wallet_sk, - master_sk_to_wallet_sk_unhardened, ) @@ -59,29 +58,34 @@ def generate_and_add(label: Optional[str]) -> None: """ unlock_keyring() print("Generating private key") - query_and_add_private_key_seed(mnemonic=generate_mnemonic(), label=label) + query_and_add_key_info(mnemonic_or_pk=generate_mnemonic(), label=label) -def query_and_add_private_key_seed(mnemonic: Optional[str], label: Optional[str] = None) -> None: +def query_and_add_key_info(mnemonic_or_pk: Optional[str], label: Optional[str] = None) -> None: unlock_keyring() - if mnemonic is None: - mnemonic = input("Enter the mnemonic you want to use: ") + if mnemonic_or_pk is None: + mnemonic_or_pk = input("Enter the mnemonic/observer key you want to use: ") if label is None: label = input("Enter the label you want to assign to this key (Press Enter to skip): ") if len(label) == 0: label = None - add_private_key_seed(mnemonic, label) + add_key_info(mnemonic_or_pk, label) -def add_private_key_seed(mnemonic: str, label: Optional[str]) -> None: +def add_key_info(mnemonic_or_pk: str, label: Optional[str]) -> None: """ - Add a private key seed to the keyring, with the given mnemonic and an optional label. + Add a private key seed or public key to the keyring, with the given mnemonic and an optional label. """ unlock_keyring() try: - sk = Keychain().add_private_key(mnemonic, label) - fingerprint = sk.get_g1().get_fingerprint() - print(f"Added private key with public key fingerprint {fingerprint}") + if mnemonic_or_pk.count(" ") == 23: + sk = Keychain().add_private_key(mnemonic_or_pk, label) + fingerprint = sk.get_g1().get_fingerprint() + print(f"Added private key with public key fingerprint {fingerprint}") + else: + pk = Keychain().add_public_key(mnemonic_or_pk, label) + fingerprint = pk.get_fingerprint() + print(f"Added public key with fingerprint {fingerprint}") except (ValueError, KeychainException) as e: print(e) @@ -149,7 +153,7 @@ def show_keys( return None if not json_output: - msg = "Showing all public keys derived from your master seed and private key:" + msg = "Showing all public keys derived from your master key:" if show_mnemonic: msg = "Showing all public and private keys" print(msg) @@ -166,8 +170,8 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() key["pool_pk"] = bytes(master_sk_to_pool_sk(sk).get_g1()).hex() else: - key["farmer_pk"] = "N/A" - key["pool_pk"] = "N/A" + key["farmer_pk"] = None + key["pool_pk"] = None if non_observer_derivation: if sk is None: @@ -181,7 +185,7 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_pk), prefix) key["wallet_address"] = wallet_address else: - key["wallet_address"] = "N/A" + key["wallet_address"] = None key["non_observer"] = non_observer_derivation @@ -190,9 +194,9 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["wallet_sk"] = bytes(master_sk_to_wallet_sk(sk, uint32(0))).hex() key["mnemonic"] = bytes_to_mnemonic(key_data.entropy) else: - key["master_sk"] = "N/A" - key["wallet_sk"] = "N/A" - key["mnemonic"] = "N/A" + key["master_sk"] = None + key["wallet_sk"] = None + key["mnemonic"] = None return key @@ -201,7 +205,8 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: if json_output: print(json.dumps({"keys": list(keys)})) else: - for key in keys: + for _key in keys: + key = {k: "N/A" if v is None else v for k, v in _key.items()} print("") if "label" in key: print("Label:", key["label"]) @@ -226,13 +231,15 @@ def delete(fingerprint: int) -> None: Keychain().delete_key_by_fingerprint(fingerprint) -def derive_sk_from_hd_path(master_sk: PrivateKey, hd_path_root: str) -> Tuple[PrivateKey, str]: +def derive_pk_and_sk_from_hd_path( + master_pk: G1Element, hd_path_root: str, master_sk: Optional[PrivateKey] = None +) -> Tuple[G1Element, Optional[PrivateKey], str]: """ Derive a private key from the provided HD path. Takes a master key and HD path as input, and returns the derived key and the HD path that was used to derive it. """ - from chia.wallet.derive_keys import _derive_path, _derive_path_unhardened + from chia.wallet.derive_keys import _derive_path, _derive_path_unhardened, _derive_pk_unhardened class DerivationType(Enum): NONOBSERVER = 0 @@ -255,28 +262,38 @@ class DerivationType(Enum): raise ValueError("Invalid HD path. Empty index") non_observer: bool = current_index_str[-1] == "n" + if non_observer and master_sk is None: + raise ValueError("Hardened path specified for observer key") current_index: int = int(current_index_str[:-1]) if non_observer else int(current_index_str) index_and_derivation_types.append( (current_index, DerivationType.NONOBSERVER if non_observer else DerivationType.OBSERVER) ) - current_sk: PrivateKey = master_sk - # Derive keys along the path - for current_index, derivation_type in index_and_derivation_types: - if derivation_type == DerivationType.NONOBSERVER: - current_sk = _derive_path(current_sk, [current_index]) - elif derivation_type == DerivationType.OBSERVER: - current_sk = _derive_path_unhardened(current_sk, [current_index]) - else: - raise ValueError(f"Unhandled derivation type: {derivation_type}") + if master_sk is not None: + current_sk: Optional[PrivateKey] = master_sk + assert current_sk is not None + for current_index, derivation_type in index_and_derivation_types: + if derivation_type == DerivationType.NONOBSERVER: + current_sk = _derive_path(current_sk, [current_index]) + elif derivation_type == DerivationType.OBSERVER: + current_sk = _derive_path_unhardened(current_sk, [current_index]) + else: + raise ValueError(f"Unhandled derivation type: {derivation_type}") # pragma: no cover + current_pk: G1Element = current_sk.get_g1() + else: + current_sk = None + current_pk = master_pk + for current_index, _ in index_and_derivation_types: + current_pk = _derive_pk_unhardened(current_pk, [current_index]) - return (current_sk, "m/" + "/".join(path) + "/") + return (current_pk, current_sk, "m/" + "/".join(path) + "/") def sign(message: str, private_key: PrivateKey, hd_path: str, as_bytes: bool, json_output: bool) -> None: - sk: PrivateKey = derive_sk_from_hd_path(private_key, hd_path)[0] + sk = derive_pk_and_sk_from_hd_path(private_key.get_g1(), hd_path, master_sk=private_key)[1] + assert sk is not None data = bytes.fromhex(message) if as_bytes else bytes(message, "utf-8") signing_mode: SigningMode = ( SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT if as_bytes else SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT @@ -323,7 +340,8 @@ def _clear_line_part(n: int) -> None: def _search_derived( - current_sk: PrivateKey, + current_pk: G1Element, + current_sk: Optional[PrivateKey], search_terms: Tuple[str, ...], path: str, path_indices: Optional[List[int]], @@ -336,11 +354,11 @@ def _search_derived( prefix: str, ) -> List[str]: # Return a subset of search_terms that were found """ - Performs a shallow search of keys derived from the current sk for items matching + Performs a shallow search of keys derived from the current pk/sk for items matching the provided search terms. """ - from chia.wallet.derive_keys import _derive_path, _derive_path_unhardened + from chia.wallet.derive_keys import _derive_path, _derive_path_unhardened, _derive_pk_unhardened class DerivedSearchResultType(Enum): PUBLIC_KEY = "public key" @@ -352,6 +370,8 @@ class DerivedSearchResultType(Enum): current_path_indices: List[int] = path_indices if path_indices is not None else [] found_search_terms: List[str] = [] + assert not (non_observer_derivation and current_sk is None) + for index in range(limit): found_items: List[Tuple[str, str, DerivedSearchResultType]] = [] printed_match: bool = False @@ -365,15 +385,19 @@ class DerivedSearchResultType(Enum): # Derive the private key if non_observer_derivation: + assert current_sk is not None # semantics above guarantee this child_sk = _derive_path(current_sk, current_path_indices) + if search_public_key or search_address: + child_pk = child_sk.get_g1() else: - child_sk = _derive_path_unhardened(current_sk, current_path_indices) - - child_pk: Optional[G1Element] = None - - # Public key is needed for searching against wallet addresses or public keys - if search_public_key or search_address: - child_pk = child_sk.get_g1() + if search_public_key or search_address: + child_pk = _derive_pk_unhardened(current_pk, current_path_indices) + else: + child_pk = None + if search_private_key and current_sk is not None: + child_sk = _derive_path_unhardened(current_sk, current_path_indices) + else: + child_sk = None address: Optional[str] = None @@ -388,6 +412,7 @@ class DerivedSearchResultType(Enum): found_item_type: Optional[DerivedSearchResultType] = None if search_private_key and term in str(child_sk): + assert child_sk is not None # semantics above guarantee this found_item = private_key_string_repr(child_sk) found_item_type = DerivedSearchResultType.PRIVATE_KEY elif search_public_key and child_pk is not None and term in str(child_pk): @@ -435,7 +460,7 @@ class DerivedSearchResultType(Enum): def search_derive( root_path: Path, - private_key: Optional[PrivateKey], + fingerprint: Optional[int], search_terms: Tuple[str, ...], limit: int, non_observer_derivation: bool, @@ -443,6 +468,7 @@ def search_derive( search_types: Tuple[str, ...], derive_from_hd_path: Optional[str], prefix: Optional[str], + private_key: Optional[PrivateKey], ) -> bool: """ Searches for items derived from the provided private key, or if not specified, @@ -452,7 +478,6 @@ def search_derive( from time import perf_counter start_time = perf_counter() - private_keys: List[PrivateKey] remaining_search_terms: Dict[str, None] = dict.fromkeys(search_terms) # poor man's ordered set search_address = "address" in search_types search_public_key = "public_key" in search_types @@ -468,27 +493,41 @@ def search_derive( search_public_key = True search_private_key = True - if private_key is None: - private_keys = [sk for sk, _ in Keychain().get_all_private_keys()] - else: + if fingerprint is None and private_key is None: + public_keys: List[G1Element] = Keychain().get_all_public_keys() + private_keys: List[Optional[PrivateKey]] = [ + data.private_key if data.secrets is not None else None for data in Keychain().get_keys(include_secrets=True) + ] + elif fingerprint is None: + assert private_key is not None + public_keys = [private_key.get_g1()] private_keys = [private_key] + else: + master_key_data = Keychain().get_key(fingerprint, include_secrets=True) + public_keys = [master_key_data.public_key] + private_keys = [master_key_data.private_key if master_key_data.secrets is not None else None] - for sk in private_keys: + for pk, sk in zip(public_keys, private_keys): + if sk is None and non_observer_derivation: + continue current_path: str = "" found_terms: List[str] = [] if show_progress: - print(f"Searching keys derived from: {sk.get_g1().get_fingerprint()}") + print(f"Searching keys derived from: {pk.get_fingerprint()}") # Derive from the provided HD path if derive_from_hd_path is not None: - derivation_root_sk, hd_path_root = derive_sk_from_hd_path(sk, derive_from_hd_path) + derivation_root_pk, derivation_root_sk, hd_path_root = derive_pk_and_sk_from_hd_path( + pk, derive_from_hd_path, master_sk=sk + ) if show_progress: sys.stdout.write(hd_path_root) # Shallow search under hd_path_root found_terms = _search_derived( + derivation_root_pk, derivation_root_sk, tuple(remaining_search_terms.keys()), hd_path_root, @@ -535,6 +574,7 @@ def search_derive( sys.stdout.write(f"{account_str}/") # lgtm [py/clear-text-logging-sensitive-data] found_terms = _search_derived( + pk, sk, tuple(remaining_search_terms.keys()), current_path, @@ -585,16 +625,31 @@ def search_derive( def derive_wallet_address( root_path: Path, - private_key: PrivateKey, + fingerprint: Optional[int], index: int, count: int, prefix: Optional[str], non_observer_derivation: bool, show_hd_path: bool, + private_key: Optional[PrivateKey], ) -> None: """ Generate wallet addresses using keys derived from the provided private key. """ + if fingerprint is not None: + key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=non_observer_derivation) + if non_observer_derivation and key_data.secrets is None: + print("Need a private key for non observer derivation of wallet addresses") + return + elif non_observer_derivation: + sk = key_data.private_key + else: + sk = None + pk = key_data.public_key + else: + assert private_key is not None + sk = private_key + pk = sk.get_g1() if prefix is None: config: Dict[str, Any] = load_config(root_path, "config.yaml") @@ -606,12 +661,13 @@ def derive_wallet_address( wallet_hd_path_root += f"{i}{'n' if non_observer_derivation else ''}/" for i in range(index, index + count): if non_observer_derivation: - sk = master_sk_to_wallet_sk(private_key, uint32(i)) + assert sk is not None + pubkey = master_sk_to_wallet_sk(sk, uint32(i)).get_g1() else: - sk = master_sk_to_wallet_sk_unhardened(private_key, uint32(i)) + pubkey = master_pk_to_wallet_pk_unhardened(pk, uint32(i)) # Generate a wallet address using the standard p2_delegated_puzzle_or_hidden_puzzle puzzle # TODO: consider generating addresses using other puzzles - address = encode_puzzle_hash(create_puzzlehash_for_pk(sk.get_g1()), prefix) + address = encode_puzzle_hash(create_puzzlehash_for_pk(pubkey), prefix) if show_hd_path: print( f"Wallet address {i} " @@ -629,7 +685,7 @@ def private_key_string_repr(private_key: PrivateKey) -> str: def derive_child_key( - master_sk: PrivateKey, + fingerprint: Optional[int], key_type: Optional[str], derive_from_hd_path: Optional[str], index: int, @@ -637,16 +693,24 @@ def derive_child_key( non_observer_derivation: bool, show_private_keys: bool, show_hd_path: bool, + private_key: Optional[PrivateKey], ) -> None: """ Derive child keys from the provided master key. """ + from chia.wallet.derive_keys import _derive_path, _derive_path_unhardened, _derive_pk_unhardened - from chia.wallet.derive_keys import _derive_path, _derive_path_unhardened + if fingerprint is not None: + key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=True) + current_pk: G1Element = key_data.public_key + current_sk: Optional[PrivateKey] = key_data.private_key if key_data.secrets is not None else None + else: + assert private_key is not None + current_pk = private_key.get_g1() + current_sk = private_key - derivation_root_sk: Optional[PrivateKey] = None - hd_path_root: Optional[str] = None - current_sk: Optional[PrivateKey] = None + if non_observer_derivation and current_sk is None: + raise ValueError("Cannot perform non-observer derivation on an observer-only key") # Key type was specified if key_type is not None: @@ -664,38 +728,51 @@ def derive_child_key( ) if non_observer_derivation: - current_sk = _derive_path(master_sk, path_indices) + assert current_sk is not None # semantics above guarantee this + current_sk = _derive_path(current_sk, path_indices) else: - current_sk = _derive_path_unhardened(master_sk, path_indices) + if current_sk is not None: + current_sk = _derive_path_unhardened(current_sk, path_indices) + else: + current_pk = _derive_pk_unhardened(current_pk, path_indices) derivation_root_sk = current_sk + derivation_root_pk = current_pk hd_path_root = "m/" for i in path_indices: hd_path_root += f"{i}{'n' if non_observer_derivation else ''}/" # Arbitrary HD path was specified elif derive_from_hd_path is not None: - derivation_root_sk, hd_path_root = derive_sk_from_hd_path(master_sk, derive_from_hd_path) + derivation_root_pk, derivation_root_sk, hd_path_root = derive_pk_and_sk_from_hd_path( + current_pk, derive_from_hd_path, master_sk=current_sk + ) # Derive child keys from derivation_root_sk - if derivation_root_sk is not None and hd_path_root is not None: - for i in range(index, index + count): - if non_observer_derivation: - sk = _derive_path(derivation_root_sk, [i]) - else: + for i in range(index, index + count): + if non_observer_derivation: + assert derivation_root_sk is not None # semantics above guarantee this + sk = _derive_path(derivation_root_sk, [i]) + pk = sk.get_g1() + else: + if derivation_root_sk is not None: sk = _derive_path_unhardened(derivation_root_sk, [i]) - hd_path: str = ( - " (" + hd_path_root + str(i) + ("n" if non_observer_derivation else "") + ")" if show_hd_path else "" - ) - key_type_str: Optional[str] - - if key_type is not None: - key_type_str = key_type.capitalize() + pk = sk.get_g1() else: - key_type_str = "Non-Observer" if non_observer_derivation else "Observer" + sk = None + pk = _derive_pk_unhardened(derivation_root_pk, [i]) + hd_path: str = ( + " (" + hd_path_root + str(i) + ("n" if non_observer_derivation else "") + ")" if show_hd_path else "" + ) + key_type_str: Optional[str] + + if key_type is not None: + key_type_str = key_type.capitalize() + else: + key_type_str = "Non-Observer" if non_observer_derivation else "Observer" - print(f"{key_type_str} public key {i}{hd_path}: {sk.get_g1()}") - if show_private_keys: - print(f"{key_type_str} private key {i}{hd_path}: {private_key_string_repr(sk)}") + print(f"{key_type_str} public key {i}{hd_path}: {pk}") + if show_private_keys and sk is not None: + print(f"{key_type_str} private key {i}{hd_path}: {private_key_string_repr(sk)}") def private_key_for_fingerprint(fingerprint: int) -> Optional[PrivateKey]: diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 0c594a6f0f5b..0132c514a00c 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -639,8 +639,7 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict for i in range(index, index + count): if non_observer_derivation: - # semantics above guarantee that key.secrets is not None here - sk = master_sk_to_wallet_sk(key.secrets.private_key, uint32(i)) # type: ignore[union-attr] + sk = master_sk_to_wallet_sk(key.private_key, uint32(i)) pk = sk.get_g1() else: pk = master_pk_to_wallet_pk_unhardened(key.public_key, uint32(i)) @@ -665,8 +664,6 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: Dic keys_for_plot: Dict[uint32, Any] = {} for key in keys: - if key.secrets is None and fingerprints is None: - continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() pool_public_key: G1Element = master_sk_to_pool_sk(sk).get_g1() diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index a5911f3986a9..008aa84b3e39 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional import pytest -from click.testing import CliRunner +from click.testing import CliRunner, Result from chia.cmds.chia import cli from chia.cmds.keys import delete_all_cmd, generate_and_print_cmd, sign_cmd, verify_cmd @@ -21,13 +21,16 @@ "another venue evidence spread season bright private " "tomato remind jaguar original blur embody project can" ) +TEST_PUBLIC_KEY = "8a37cad4c5edf0a7544cbdb9f9383f7c5e82567d0236dc4f5b0547137afff9a5ce33aece3358d0202cafb9a12607ab53" +TEST_PK_FINGERPRINT = 2167729070 TEST_FINGERPRINT = 2877570395 @pytest.fixture(scope="function") -def keyring_with_one_key(empty_keyring): +def keyring_with_one_public_one_private_key(empty_keyring): keychain = empty_keyring keychain.add_private_key(TEST_MNEMONIC_SEED) + keychain.add_public_key(TEST_PUBLIC_KEY) return keychain @@ -191,6 +194,7 @@ def test_generate_with_existing_config(self, tmp_path, empty_keyring): (["add", "--label", "key_0"], "key_0", f"{TEST_MNEMONIC_SEED}\n"), (["add", "-l", ""], None, f"{TEST_MNEMONIC_SEED}\n"), (["add", "--label", ""], None, f"{TEST_MNEMONIC_SEED}\n"), + (["add", "-l", "key_0"], "key_0", f"{TEST_PUBLIC_KEY}\n"), ], ) def test_generate_and_add_label_parameter( @@ -218,8 +222,8 @@ def test_generate_and_add_label_parameter( # And make sure the label was set to the expected label assert_label(keychain, label, 0) - def test_set_label(self, keyring_with_one_key, tmp_path): - keychain = keyring_with_one_key + def test_set_label(self, keyring_with_one_public_one_private_key, tmp_path): + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ "--root-path", @@ -245,8 +249,8 @@ def set_and_validate(label: str): # Change the label set_and_validate("changed") - def test_delete_label(self, keyring_with_one_key, tmp_path): - keychain = keyring_with_one_key + def test_delete_label(self, keyring_with_one_public_one_private_key, tmp_path): + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ "--root-path", @@ -302,14 +306,15 @@ def test_show_labels(self, empty_keyring, tmp_path): else: assert label == key.label - def test_show(self, keyring_with_one_key, tmp_path): + def test_show(self, keyring_with_one_public_one_private_key, tmp_path): """ Test that the `chia keys show` command shows the correct key. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key assert len(keychain.get_all_private_keys()) == 1 + assert len(keychain.get_all_public_keys()) == 2 keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ @@ -327,17 +332,25 @@ def test_show(self, keyring_with_one_key, tmp_path): # assert result.exit_code == 0 assert result.output.find(f"Fingerprint: {TEST_FINGERPRINT}") != -1 + assert result.output.find(f"Fingerprint: {TEST_PK_FINGERPRINT}") != -1 - def test_show_fingerprint(self, keyring_with_one_key, tmp_path): + # Try with non_observer_derivation + result = runner.invoke(cli, [*base_params, *cmd_params, "--non-observer-derivation"]) + assert result.output.find(f"Fingerprint: {TEST_FINGERPRINT}") != -1 + assert result.output.find(f"Fingerprint: {TEST_PK_FINGERPRINT}") != -1 + assert result.output.find("First wallet address (non-observer): N/A") != -1 + + def test_show_fingerprint(self, keyring_with_one_public_one_private_key, tmp_path): """ Test that the `chia keys show --fingerprint` command shows the correct key. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key # add a key keychain.add_private_key(generate_mnemonic()) assert len(keychain.get_all_private_keys()) == 2 + assert len(keychain.get_all_public_keys()) == 3 keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ @@ -358,14 +371,15 @@ def test_show_fingerprint(self, keyring_with_one_key, tmp_path): assert len(fingerprints) == 1 assert str(TEST_FINGERPRINT) in fingerprints[0] - def test_show_json(self, keyring_with_one_key, tmp_path): + def test_show_json(self, keyring_with_one_public_one_private_key, tmp_path): """ Test that the `chia keys show --json` command shows the correct key. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key assert len(keychain.get_all_private_keys()) == 1 + assert len(keychain.get_all_public_keys()) == 2 keys_root_path = keychain.keyring_wrapper.keys_root_path base_params = [ @@ -385,13 +399,14 @@ def test_show_json(self, keyring_with_one_key, tmp_path): # assert result.exit_code == 0 assert json_result["keys"][0]["fingerprint"] == TEST_FINGERPRINT + assert json_result["keys"][1]["fingerprint"] == TEST_PK_FINGERPRINT - def test_show_mnemonic(self, keyring_with_one_key, tmp_path): + def test_show_mnemonic(self, keyring_with_one_public_one_private_key, tmp_path): """ Test that the `chia keys show --show-mnemonic-seed` command shows the key's mnemonic seed. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key assert len(keychain.get_all_private_keys()) == 1 @@ -413,13 +428,15 @@ def test_show_mnemonic(self, keyring_with_one_key, tmp_path): assert result.output.find(f"Fingerprint: {TEST_FINGERPRINT}") != -1 assert result.output.find("Mnemonic seed (24 secret words):") != -1 assert result.output.find(TEST_MNEMONIC_SEED) != -1 + assert result.output.find(f"Fingerprint: {TEST_PK_FINGERPRINT}") != -1 + assert result.output.find("Mnemonic seed (24 secret words):\nN/A") != -1 - def test_show_mnemonic_json(self, keyring_with_one_key, tmp_path): + def test_show_mnemonic_json(self, keyring_with_one_public_one_private_key, tmp_path): """ Test that the `chia keys show --show-mnemonic-seed --json` command shows the key's mnemonic seed. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key assert len(keychain.get_all_private_keys()) == 1 @@ -441,6 +458,8 @@ def test_show_mnemonic_json(self, keyring_with_one_key, tmp_path): # assert result.exit_code == 0 assert json_result["keys"][0]["fingerprint"] == TEST_FINGERPRINT assert json_result["keys"][0]["mnemonic"] == TEST_MNEMONIC_SEED + assert json_result["keys"][1]["fingerprint"] == TEST_PK_FINGERPRINT + assert json_result["keys"][1]["mnemonic"] is None def test_add_interactive(self, tmp_path, empty_keyring): """ @@ -605,7 +624,7 @@ def test_generate_and_print(self): assert result.exit_code == 0 assert result.output.find("Mnemonic (24 secret words):") != -1 - def test_sign(self, keyring_with_one_key): + def test_sign(self, keyring_with_one_public_one_private_key): """ Test the `chia keys sign` command. """ @@ -636,7 +655,7 @@ def test_sign(self, keyring_with_one_key): != -1 ) - def test_sign_non_observer(self, keyring_with_one_key): + def test_sign_non_observer(self, keyring_with_one_public_one_private_key): """ Test the `chia keys sign` command with a non-observer key. """ @@ -726,12 +745,12 @@ def test_verify(self): assert result.exit_code == 0 assert result.output.find("True") == 0 - def test_derive_search(self, tmp_path, keyring_with_one_key): + def test_derive_search(self, tmp_path, keyring_with_one_public_one_private_key): """ Test the `chia keys derive search` command, searching a public and private key """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -763,11 +782,13 @@ def test_derive_search(self, tmp_path, keyring_with_one_key): "all", "a4601f992f24047097a30854ef656382911575694439108723698972941e402d737c13df76fdf43597f7b3c2fa9ed27a", "028e33fa3f8caa3102c028f3bff6b6680e528d9a0c543c479ef0b0339060ef36", + "--show-progress", ], catch_exceptions=False, ) assert result.exit_code == 0 + assert result.output.find(f"Searching keys derived from: {TEST_FINGERPRINT}") != -1 assert ( result.output.find( "Found public key: a4601f992f24047097a30854ef656382911575694439108723698" @@ -783,12 +804,191 @@ def test_derive_search(self, tmp_path, keyring_with_one_key): != -1 ) - def test_derive_search_wallet_address(self, tmp_path, keyring_with_one_key): + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_FINGERPRINT), + "search", + "--limit", + "10", + "--search-type", + "all", + "83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454e9ce1033f4cba8824259dc17dc3b643ab1003e6b961", + "--show-progress", + "--non-observer-derivation", + ], + ) + + assert result.exit_code == 0 + assert result.output.find(f"Searching keys derived from: {TEST_FINGERPRINT}") != -1 + assert ( + result.output.find( + "Found public key: 83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454" + "e9ce1033f4cba8824259dc17dc3b643ab1003e6b961 (HD path: m/12381n/8444n/2n/9n)" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_FINGERPRINT), + "search", + "--limit", + "10", + "--search-type", + "private_key", + "028e33fa3f8caa3102c028f3bff6b6680e528d9a0c543c479ef0b0339060ef36", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Found private key: 028e33fa3f8caa3102c028f3bff6b6680e528d9a0c543c479ef0b0339060ef36" + " (HD path: m/12381/8444/2/9)" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "search", + "--limit", + "10", + "--search-type", + "all", + "a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e3d4d1acfa5d63d6542336c7b24d2fceab991919e989", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Found public key: a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e" + "3d4d1acfa5d63d6542336c7b24d2fceab991919e989 (HD path: m/12381/8444/2/9)" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "search", + "--limit", + "10", + "--search-type", + "all", + "a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e3d4d1acfa5d63d6542336c7b24d2fceab991919e989", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Found public key: a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e" + "3d4d1acfa5d63d6542336c7b24d2fceab991919e989 (HD path: m/12381/8444/2/9)" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "search", + "--limit", + "10", + "--search-type", + "all", + "83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454e9ce1033f4cba8824259dc17dc3b643ab1003e6b961", + "a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e3d4d1acfa5d63d6542336c7b24d2fceab991919e989", + "--non-observer-derivation", + ], + ) + + assert result.exit_code == 1 + assert ( + result.output.find( + "Found public key: 83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454" + "e9ce1033f4cba8824259dc17dc3b643ab1003e6b961 (HD path: m/12381n/8444n/2n/9n)" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "search", + "--limit", + "10", + "--search-type", + "all", + "a4601f992f24047097a30854ef656382911575694439108723698972941e402d737c13df76fdf43597f7b3c2fa9ed27a", + "--show-progress", + ], + ) + + assert result.exit_code == 1 + assert result.output.find(f"Searching keys derived from: {TEST_PK_FINGERPRINT}") != -1 + assert ( + result.output.find( + "Could not find 'a4601f992f24047097a30854ef656382911575694439108723698972941e402d73" + "7c13df76fdf43597f7b3c2fa9ed27a'" + ) + != -1 + ) + + # 83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454e9ce1033f4cba8824259dc17dc3b643ab1003e6b961 + + def test_derive_search_wallet_address(self, tmp_path, keyring_with_one_public_one_private_key): """ Test the `chia keys derive search` command, searching for a wallet address """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -832,12 +1032,41 @@ def test_derive_search_wallet_address(self, tmp_path, keyring_with_one_key): != -1 ) - def test_derive_search_wallet_testnet_address(self, tmp_path, keyring_with_one_key): + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "search", + "--limit", + "40", + "--search-type", + "address", + "xch1p33y7kv48u7l68m490mr8levl6nkyxm3x8tfcnnec555egxzd3gs829wkl", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Found wallet address: " + "xch1p33y7kv48u7l68m490mr8levl6nkyxm3x8tfcnnec555egxzd3gs829wkl (HD path: m/12381/8444/2/9)" + ) + != -1 + ) + + def test_derive_search_wallet_testnet_address(self, tmp_path, keyring_with_one_public_one_private_key): """ Test the `chia keys derive search` command, searching for a testnet wallet address """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -883,12 +1112,43 @@ def test_derive_search_wallet_testnet_address(self, tmp_path, keyring_with_one_k != -1 ) - def test_derive_search_failure(self, tmp_path, keyring_with_one_key): + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "search", + "--limit", + "40", + "--search-type", + "address", + "txch1p33y7kv48u7l68m490mr8levl6nkyxm3x8tfcnnec555egxzd3gs2dzchv", + "--prefix", + "txch", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Found wallet address: " + "txch1p33y7kv48u7l68m490mr8levl6nkyxm3x8tfcnnec555egxzd3gs2dzchv (HD path: m/12381/8444/2/9)" + ) + != -1 + ) + + def test_derive_search_failure(self, tmp_path, keyring_with_one_public_one_private_key): """ Test the `chia keys derive search` command with a failing search. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -976,12 +1236,12 @@ def test_derive_search_hd_path(self, tmp_path, empty_keyring, mnemonic_seed_file != -1 ) - def test_derive_wallet_address(self, tmp_path, keyring_with_one_key): + def test_derive_wallet_address(self, tmp_path, keyring_with_one_public_one_private_key, mnemonic_seed_file): """ Test the `chia keys derive wallet-address` command, generating a couple of wallet addresses. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -1004,8 +1264,8 @@ def test_derive_wallet_address(self, tmp_path, keyring_with_one_key): os.fspath(keys_root_path), "keys", "derive", - "--fingerprint", - str(TEST_FINGERPRINT), + "--mnemonic-seed-filename", + str(mnemonic_seed_file), "wallet-address", "--index", "50", @@ -1033,12 +1293,63 @@ def test_derive_wallet_address(self, tmp_path, keyring_with_one_key): != -1 ) - def test_derive_wallet_testnet_address(self, tmp_path, keyring_with_one_key): + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "wallet-address", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Wallet address 9 (m/12381/8444/2/9): " "xch1p33y7kv48u7l68m490mr8levl6nkyxm3x8tfcnnec555egxzd3gs829wkl" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "wallet-address", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + "--non-observer-derivation", + ], + ) + assert result.exit_code == 0 + assert result.output.find("Need a private key for non observer derivation of wallet addresses") != -1 + + def test_derive_wallet_testnet_address(self, tmp_path, keyring_with_one_public_one_private_key): """ Test the `chia keys derive wallet-address` command, generating a couple of testnet wallet addresses. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -1092,12 +1403,43 @@ def test_derive_wallet_testnet_address(self, tmp_path, keyring_with_one_key): != -1 ) - def test_derive_child_keys(self, tmp_path, keyring_with_one_key): + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "wallet-address", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + "--prefix", + "txch", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Wallet address 9 (m/12381/8444/2/9): " + "txch1p33y7kv48u7l68m490mr8levl6nkyxm3x8tfcnnec555egxzd3gs2dzchv" + ) + != -1 + ) + + def test_derive_child_keys(self, tmp_path, keyring_with_one_public_one_private_key, mnemonic_seed_file): """ Test the `chia keys derive child-keys` command, generating a couple of derived keys. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_public_one_private_key keys_root_path = keychain.keyring_wrapper.keys_root_path runner = CliRunner() @@ -1120,8 +1462,8 @@ def test_derive_child_keys(self, tmp_path, keyring_with_one_key): os.fspath(keys_root_path), "keys", "derive", - "--fingerprint", - str(TEST_FINGERPRINT), + "--mnemonic-seed-filename", + str(mnemonic_seed_file), "child-key", "--derive-from-hd-path", "m/12381n/8444n/2/3/4/", @@ -1164,3 +1506,204 @@ def test_derive_child_keys(self, tmp_path, keyring_with_one_key): ) != -1 ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_FINGERPRINT), + "child-key", + "--type", + "wallet", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + "--show-private-keys", + "--non-observer-derivation", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Wallet public key 9 (m/12381n/8444n/2n/9n): " + "83062a1b26d27820600eac4e31c1a890a6ba026b28bb96bb66454e9ce1033f4cba8824259dc17dc3b643ab1003e6b961" + ) + != -1 + ) + assert ( + result.output.find( + "Wallet private key 9 (m/12381n/8444n/2n/9n): " + "522f45786db6446d2f617a0c7df894385a21d05c7fbbfb34ee5aaaa417d8f41f" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_FINGERPRINT), + "child-key", + "--type", + "wallet", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + "--show-private-keys", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Wallet public key 9 (m/12381/8444/2/9): " + "a4601f992f24047097a30854ef656382911575694439108723698972941e402d737c13df76fdf43597f7b3c2fa9ed27a" + ) + != -1 + ) + assert ( + result.output.find( + "Wallet private key 9 (m/12381/8444/2/9): " + "028e33fa3f8caa3102c028f3bff6b6680e528d9a0c543c479ef0b0339060ef36" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "child-key", + "--derive-from-hd-path", + "m/12381/8444/2/3/4/", + "--index", + "30", + "--count", + "2", + "--show-hd-path", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Observer public key 30 (m/12381/8444/2/3/4/30): " + "b64b1c85bacc8fe3509a559d0178ce8373d8ea1dfcdb0c4b76fa03540da31d01276f7f000306d576c1c29468cf13b45a" + ) + != -1 + ) + assert ( + result.output.find( + "Observer public key 31 (m/12381/8444/2/3/4/31): " + "92ba067175f2e87dd47336302ef8331a05ee0a737a92deec1b23dcd49eeb783d7feafdd055e3cba24fd6c94b39eb7bf3" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "child-key", + "--type", + "wallet", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + ], + ) + + assert result.exit_code == 0 + assert ( + result.output.find( + "Wallet public key 9 (m/12381/8444/2/9): " + "a272d5aaa6046e64bd7fd69bae288b9f9e5622c13058ec7d1b85e3d4d1acfa5d63d6542336c7b24d2fceab991919e989" + ) + != -1 + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "child-key", + "--type", + "wallet", + "--index", + "9", + "--count", + "1", + "--show-hd-path", + "--non-observer-derivation", + ], + ) + + assert isinstance(result.exception, ValueError) and result.exception.args == ( + "Cannot perform non-observer derivation on an observer-only key", + ) + + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "derive", + "--fingerprint", + str(TEST_PK_FINGERPRINT), + "child-key", + "--derive-from-hd-path", + "m/12381n/8444/2/3/4/", + "--index", + "30", + "--count", + "2", + "--show-hd-path", + ], + ) + + assert isinstance(result.exception, ValueError) and result.exception.args == ( + "Hardened path specified for observer key", + ) From 2807650c4cc003dfd8ce4c241b6debdd824e8091 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 12:15:46 -0800 Subject: [PATCH 035/274] Broaden the definition of keys to include more than BLS --- chia/cmds/keys_funcs.py | 46 +++++++++++++++--------- chia/daemon/server.py | 4 ++- chia/util/keychain.py | 36 ++++++++++++++----- chia/util/observation_root.py | 15 ++++++++ tests/core/daemon/test_keychain_proxy.py | 12 ++++--- tests/core/util/test_keychain.py | 22 ++++++++---- tools/legacy_keyring.py | 12 ++++--- 7 files changed, 105 insertions(+), 42 deletions(-) create mode 100644 chia/util/observation_root.py diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 69a6ed2b8f97..1bac4f941850 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -165,7 +165,10 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["label"] = key_data.label key["fingerprint"] = key_data.fingerprint - key["master_pk"] = bytes(key_data.public_key).hex() + if isinstance(key_data.observation_root, G1Element): + key["master_pk"] = key_data.public_key.hex() + else: + key["observation_root"] = key_data.public_key.hex() if sk is not None: key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() key["pool_pk"] = bytes(master_sk_to_pool_sk(sk).get_g1()).hex() @@ -173,19 +176,20 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["farmer_pk"] = None key["pool_pk"] = None - if non_observer_derivation: - if sk is None: - first_wallet_pk: Optional[G1Element] = None + if isinstance(key_data.observation_root, G1Element): + if non_observer_derivation: + if sk is None: + first_wallet_pk: Optional[G1Element] = None + else: + first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).get_g1() else: - first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).get_g1() - else: - first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.public_key, uint32(0)) + first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.observation_root, uint32(0)) - if first_wallet_pk is not None: - wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_pk), prefix) - key["wallet_address"] = wallet_address - else: - key["wallet_address"] = None + if first_wallet_pk is not None: + wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_pk), prefix) + key["wallet_address"] = wallet_address + else: + key["wallet_address"] = None key["non_observer"] = non_observer_derivation @@ -504,8 +508,12 @@ def search_derive( private_keys = [private_key] else: master_key_data = Keychain().get_key(fingerprint, include_secrets=True) - public_keys = [master_key_data.public_key] - private_keys = [master_key_data.private_key if master_key_data.secrets is not None else None] + if isinstance(master_key_data.observation_root, G1Element): + public_keys = [master_key_data.observation_root] + private_keys = [master_key_data.private_key if master_key_data.secrets is not None else None] + else: + print("Cannot currently derive paths from non-BLS keys") + return True for pk, sk in zip(public_keys, private_keys): if sk is None and non_observer_derivation: @@ -638,6 +646,9 @@ def derive_wallet_address( """ if fingerprint is not None: key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=non_observer_derivation) + if not isinstance(key_data.observation_root, G1Element): + print("Cannot currently derive from non-BLS keys") + return if non_observer_derivation and key_data.secrets is None: print("Need a private key for non observer derivation of wallet addresses") return @@ -645,7 +656,7 @@ def derive_wallet_address( sk = key_data.private_key else: sk = None - pk = key_data.public_key + pk: G1Element = key_data.observation_root else: assert private_key is not None sk = private_key @@ -702,7 +713,10 @@ def derive_child_key( if fingerprint is not None: key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=True) - current_pk: G1Element = key_data.public_key + if not isinstance(key_data.observation_root, G1Element): + print("Cannot currently derive from non-BLS keys") + return + current_pk: G1Element = key_data.observation_root current_sk: Optional[PrivateKey] = key_data.private_key if key_data.secrets is not None else None else: assert private_key is not None diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 0132c514a00c..dfae72ccdc44 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -631,6 +631,8 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict wallet_addresses_by_fingerprint = {} for key in keys: + if not isinstance(key.observation_root, G1Element): + continue address_entries = [] # we require access to the private key to generate wallet addresses for non observer @@ -642,7 +644,7 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict sk = master_sk_to_wallet_sk(key.private_key, uint32(i)) pk = sk.get_g1() else: - pk = master_pk_to_wallet_pk_unhardened(key.public_key, uint32(i)) + pk = master_pk_to_wallet_pk_unhardened(key.observation_root, uint32(i)) wallet_address = encode_puzzle_hash(create_puzzlehash_for_pk(pk), prefix) if non_observer_derivation: hd_path = f"m/12381n/8444n/2n/{i}n" diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 41336b9aa827..19cb711543a1 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -3,6 +3,8 @@ import sys import unicodedata from dataclasses import dataclass +from enum import Enum +from functools import cached_property from hashlib import pbkdf2_hmac from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -26,6 +28,7 @@ from chia.util.hash import std_hash from chia.util.ints import uint32 from chia.util.keyring_wrapper import KeyringWrapper +from chia.util.observation_root import ObservationRoot from chia.util.streamable import Streamable, streamable CURRENT_KEY_VERSION = "1.8" @@ -214,21 +217,32 @@ def mnemonic_str(self) -> str: return " ".join(self.mnemonic) +class KeyTypes(str, Enum): + G1_ELEMENT = "G1 Element" + + @final @streamable @dataclass(frozen=True) class KeyData(Streamable): fingerprint: uint32 - public_key: G1Element + public_key: bytes label: Optional[str] secrets: Optional[KeyDataSecrets] + key_type: str + + @cached_property + def observation_root(self) -> ObservationRoot: + if self.key_type == KeyTypes.G1_ELEMENT: + return G1Element.from_bytes(self.public_key) + raise TypeError(f"Invalid key_type {self.key_type}") def __post_init__(self) -> None: # This is redundant if `from_*` methods are used but its to make sure there can't be an `KeyData` instance with # an attribute mismatch for calculated cached values. Should be ok since we don't handle a lot of keys here. - if self.secrets is not None and self.public_key != self.private_key.get_g1(): + if self.secrets is not None and self.observation_root != self.private_key.get_g1(): raise KeychainKeyDataMismatch("public_key") - if uint32(self.public_key.get_fingerprint()) != self.fingerprint: + if uint32(self.observation_root.get_fingerprint()) != self.fingerprint: raise KeychainKeyDataMismatch("fingerprint") @classmethod @@ -236,9 +250,10 @@ def from_mnemonic(cls, mnemonic: str, label: Optional[str] = None) -> KeyData: private_key = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic)) return cls( fingerprint=uint32(private_key.get_g1().get_fingerprint()), - public_key=private_key.get_g1(), + public_key=bytes(private_key.get_g1()), label=label, secrets=KeyDataSecrets.from_mnemonic(mnemonic), + key_type=KeyTypes.G1_ELEMENT.value, ) @classmethod @@ -306,8 +321,9 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: raise KeychainUserNotFound(self.service, user) str_bytes = bytes.fromhex(read_str) - public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE]) - fingerprint = public_key.get_fingerprint() + pk_bytes: bytes = str_bytes[: G1Element.SIZE] + observation_root: ObservationRoot = G1Element.from_bytes(pk_bytes) + fingerprint = observation_root.get_fingerprint() if len(str_bytes) == G1Element.SIZE + 32: entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] else: @@ -315,9 +331,10 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: return KeyData( fingerprint=uint32(fingerprint), - public_key=public_key, + public_key=pk_bytes, label=self.keyring_wrapper.get_label(fingerprint), secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, + key_type=KeyTypes.G1_ELEMENT.value, ) def _get_free_private_key_index(self) -> int: @@ -455,7 +472,7 @@ def get_key(self, fingerprint: int, include_secrets: bool = False) -> KeyData: for index in range(MAX_KEYS + 1): try: key_data = self._get_key_data(index, include_secrets) - if key_data.public_key.get_fingerprint() == fingerprint: + if key_data.observation_root.get_fingerprint() == fingerprint: return key_data except KeychainUserNotFound: pass @@ -482,7 +499,8 @@ def get_all_public_keys(self) -> List[G1Element]: for index in range(MAX_KEYS + 1): try: key_data = self._get_key_data(index) - all_keys.append(key_data.public_key) + if isinstance(key_data.observation_root, G1Element): + all_keys.append(key_data.observation_root) except KeychainUserNotFound: pass return all_keys diff --git a/chia/util/observation_root.py b/chia/util/observation_root.py new file mode 100644 index 000000000000..8e6db7100284 --- /dev/null +++ b/chia/util/observation_root.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import Protocol + + +class ObservationRoot(Protocol): + def get_fingerprint(self) -> int: + ... + + def __bytes__(self) -> bytes: + ... + + @classmethod + def from_bytes(cls, blob: bytes) -> ObservationRoot: + ... diff --git a/tests/core/daemon/test_keychain_proxy.py b/tests/core/daemon/test_keychain_proxy.py index 4355a48f199c..da0f05a3f087 100644 --- a/tests/core/daemon/test_keychain_proxy.py +++ b/tests/core/daemon/test_keychain_proxy.py @@ -5,6 +5,7 @@ from typing import AsyncGenerator import pytest +from chia_rs import G1Element from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate from chia.simulator.block_tools import BlockTools @@ -45,21 +46,22 @@ async def test_add_private_key(keychain_proxy: KeychainProxy) -> None: @pytest.mark.anyio async def test_add_public_key(keychain_proxy: KeychainProxy) -> None: keychain = keychain_proxy - await keychain.add_public_key(bytes(TEST_KEY_3.public_key).hex(), TEST_KEY_3.label) + assert isinstance(TEST_KEY_3.observation_root, G1Element) + await keychain.add_public_key(TEST_KEY_3.public_key.hex(), TEST_KEY_3.label) with pytest.raises(Exception, match="already exists"): - await keychain.add_public_key(bytes(TEST_KEY_3.public_key).hex(), "") + await keychain.add_public_key(TEST_KEY_3.public_key.hex(), "") key = await keychain.get_key(TEST_KEY_3.fingerprint, include_secrets=False) assert key is not None - assert key.public_key == TEST_KEY_3.public_key + assert key.observation_root == TEST_KEY_3.observation_root assert key.secrets is None pk = await keychain.get_public_key_for_fingerprint(TEST_KEY_3.fingerprint) assert pk is not None - assert pk == TEST_KEY_3.public_key + assert pk == TEST_KEY_3.observation_root pk = await keychain.get_public_key_for_fingerprint(None) assert pk is not None - assert pk == TEST_KEY_3.public_key + assert pk == TEST_KEY_3.observation_root with pytest.raises(KeychainKeyNotFound): pk = await keychain.get_public_key_for_fingerprint(1234567890) diff --git a/tests/core/util/test_keychain.py b/tests/core/util/test_keychain.py index 226cf05c69a3..92bc733ab4ea 100644 --- a/tests/core/util/test_keychain.py +++ b/tests/core/util/test_keychain.py @@ -25,6 +25,7 @@ Keychain, KeyData, KeyDataSecrets, + KeyTypes, bytes_from_mnemonic, bytes_to_mnemonic, generate_mnemonic, @@ -228,7 +229,7 @@ def test_key_data_generate(label: Optional[str]) -> None: key_data = KeyData.generate(label) assert key_data.private_key == AugSchemeMPL.key_gen(mnemonic_to_seed(key_data.mnemonic_str())) assert key_data.entropy == bytes_from_mnemonic(key_data.mnemonic_str()) - assert key_data.public_key == key_data.private_key.get_g1() + assert key_data.observation_root == key_data.private_key.get_g1() assert key_data.fingerprint == key_data.private_key.get_g1().get_fingerprint() assert key_data.label == label @@ -240,7 +241,7 @@ def test_key_data_generate(label: Optional[str]) -> None: def test_key_data_creation(input_data: object, from_method: Callable[..., KeyData], label: Optional[str]) -> None: key_data = from_method(input_data, label) assert key_data.fingerprint == fingerprint - assert key_data.public_key == public_key + assert key_data.observation_root == public_key assert key_data.mnemonic == mnemonic.split() assert key_data.mnemonic_str() == mnemonic assert key_data.entropy == entropy @@ -249,7 +250,7 @@ def test_key_data_creation(input_data: object, from_method: Callable[..., KeyDat def test_key_data_without_secrets() -> None: - key_data = KeyData(fingerprint, public_key, None, None) + key_data = KeyData(fingerprint, bytes(public_key), None, None, KeyTypes.G1_ELEMENT.value) assert key_data.secrets is None with pytest.raises(KeychainSecretsMissing): @@ -281,12 +282,21 @@ def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, PrivateK @pytest.mark.parametrize( "input_data, data_type", [ - ((fingerprint, G1Element(), None, KeyDataSecrets(mnemonic.split(), entropy, private_key)), "public_key"), - ((fingerprint, G1Element(), None, None), "fingerprint"), + ( + ( + fingerprint, + bytes(G1Element()), + None, + KeyDataSecrets(mnemonic.split(), entropy, private_key), + KeyTypes.G1_ELEMENT.value, + ), + "public_key", + ), + ((fingerprint, bytes(G1Element()), None, None, KeyTypes.G1_ELEMENT.value), "fingerprint"), ], ) def test_key_data_post_init( - input_data: Tuple[uint32, G1Element, Optional[str], Optional[KeyDataSecrets]], data_type: str + input_data: Tuple[uint32, bytes, Optional[str], Optional[KeyDataSecrets], str], data_type: str ) -> None: with pytest.raises(KeychainKeyDataMismatch, match=data_type): KeyData(*input_data) diff --git a/tools/legacy_keyring.py b/tools/legacy_keyring.py index 73967554f398..fdcc9df9ae21 100644 --- a/tools/legacy_keyring.py +++ b/tools/legacy_keyring.py @@ -24,7 +24,7 @@ from chia.util.errors import KeychainUserNotFound -from chia.util.keychain import KeyData, KeyDataSecrets, get_private_key_user +from chia.util.keychain import KeyData, KeyDataSecrets, KeyTypes, get_private_key_user from chia.util.misc import prompt_yes_no LegacyKeyring = Union[MacKeyring, WinKeyring, CryptFileKeyring] @@ -62,7 +62,7 @@ def generate_and_add(keyring: LegacyKeyring) -> KeyData: keyring.set_password( DEFAULT_SERVICE, get_private_key_user(DEFAULT_USER, index), - bytes(key.public_key).hex() + key.entropy.hex(), + bytes(key.observation_root).hex() + key.entropy.hex(), ) return key @@ -74,15 +74,17 @@ def get_key_data(keyring: LegacyKeyring, index: int) -> KeyData: raise KeychainUserNotFound(DEFAULT_SERVICE, user) str_bytes = bytes.fromhex(read_str) - public_key = G1Element.from_bytes(str_bytes[: G1Element.SIZE]) - fingerprint = public_key.get_fingerprint() + pk_bytes = str_bytes[: G1Element.SIZE] + observation_root = G1Element.from_bytes(pk_bytes) + fingerprint = observation_root.get_fingerprint() entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] return KeyData( fingerprint=uint32(fingerprint), - public_key=public_key, + public_key=pk_bytes, label=None, secrets=KeyDataSecrets.from_entropy(entropy), + key_type=KeyTypes.G1_ELEMENT, ) From 554e7727eba925455529344956a971acba44b7b7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 29 Nov 2023 13:38:15 -0800 Subject: [PATCH 036/274] Add test to pin KeyTypes support --- tests/core/util/test_keychain.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/core/util/test_keychain.py b/tests/core/util/test_keychain.py index 92bc733ab4ea..96545ea4df4e 100644 --- a/tests/core/util/test_keychain.py +++ b/tests/core/util/test_keychain.py @@ -4,7 +4,7 @@ import pathlib import random from dataclasses import replace -from typing import Callable, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple import pytest from chia_rs import AugSchemeMPL, G1Element, PrivateKey @@ -32,6 +32,7 @@ mnemonic_from_short_words, mnemonic_to_seed, ) +from chia.util.observation_root import ObservationRoot mnemonic = ( "rapid this oven common drive ribbon bulb urban uncover napkin kitten usage enforce uncle unveil scene " @@ -469,3 +470,16 @@ async def test_delete_drops_labels(get_temp_keyring: Keychain, delete_all: bool) for key_data in keys: keychain.delete_key_by_fingerprint(key_data.fingerprint) assert keychain.keyring_wrapper.get_label(key_data.fingerprint) is None + + +@pytest.mark.parametrize("key_type", [e.value for e in KeyTypes]) +def test_key_type_support(key_type: str) -> None: + """ + The purpose of this test is to make sure that whenever KeyTypes is updated, all relevant functionality is + also updated with it. + """ + generate_test_key_for_key_type: Dict[str, Tuple[int, bytes, ObservationRoot]] = { + KeyTypes.G1_ELEMENT.value: (G1Element().get_fingerprint(), bytes(G1Element()), G1Element()) + } + obr_fingerprint, obr_bytes, obr = generate_test_key_for_key_type[key_type] + assert KeyData(uint32(obr_fingerprint), obr_bytes, None, None, key_type).observation_root == obr From 6c479ddd124833d95a711d287f3e70e9dd0b38b1 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 12:33:25 -0800 Subject: [PATCH 037/274] Add support in wallet for non-G1Element observation roots --- chia/cmds/keys_funcs.py | 2 +- chia/daemon/keychain_proxy.py | 9 ++++---- chia/daemon/keychain_server.py | 11 +++++---- chia/util/keychain.py | 26 +++++++++++++++++---- chia/wallet/wallet.py | 9 ++++---- chia/wallet/wallet_node.py | 27 ++++++++++++++-------- chia/wallet/wallet_state_manager.py | 27 ++++++++++++++-------- tests/core/util/test_keychain.py | 3 +++ tests/wallet/cat_wallet/test_cat_wallet.py | 4 +++- tests/wallet/test_sign_coin_spends.py | 2 +- tests/wallet/test_signer_protocol.py | 2 +- 11 files changed, 82 insertions(+), 40 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 1bac4f941850..ebdf137361cd 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -498,7 +498,7 @@ def search_derive( search_private_key = True if fingerprint is None and private_key is None: - public_keys: List[G1Element] = Keychain().get_all_public_keys() + public_keys: List[G1Element] = Keychain().get_all_public_keys_of_type(G1Element) private_keys: List[Optional[PrivateKey]] = [ data.private_key if data.secrets is not None else None for data in Keychain().get_keys(include_secrets=True) ] diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 6b58ba09c0b3..c79ed7ff6db9 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -30,7 +30,8 @@ KeychainMalformedResponse, KeychainProxyConnectionTimeout, ) -from chia.util.keychain import Keychain, KeyData, bytes_to_mnemonic, mnemonic_to_seed +from chia.util.keychain import KEY_TYPES_TO_TYPES, Keychain, KeyData, bytes_to_mnemonic, mnemonic_to_seed +from chia.util.observation_root import ObservationRoot from chia.util.ws_message import WsRpcMessage @@ -373,11 +374,11 @@ async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ return key - async def get_public_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[G1Element]: + async def get_public_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ObservationRoot]: """ Locates and returns a private key matching the provided fingerprint """ - key: Optional[G1Element] = None + key: Optional[ObservationRoot] = None if self.use_local_keychain(): public_keys = self.keychain.get_all_public_keys() if len(public_keys) == 0: @@ -403,7 +404,7 @@ async def get_public_key_for_fingerprint(self, fingerprint: Optional[int]) -> Op self.log.error(f"{err}") raise KeychainMalformedResponse(f"{err}") else: - key = G1Element.from_bytes(bytes.fromhex(pk_str)) + key = KEY_TYPES_TO_TYPES[response["data"]["type"]].from_bytes(bytes.fromhex(pk_str)) else: self.handle_error(response) diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index b20f8b3c65dd..773c7bd3f6a1 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -5,12 +5,13 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Type -from chia_rs import G1Element, PrivateKey +from chia_rs import PrivateKey from chia.cmds.init_funcs import check_keys from chia.util.errors import KeychainException, KeychainFingerprintNotFound from chia.util.ints import uint32 -from chia.util.keychain import Keychain, KeyData +from chia.util.keychain import KEY_TYPES_TO_TYPES, Keychain, KeyData +from chia.util.observation_root import ObservationRoot from chia.util.streamable import Streamable, streamable # Commands that are handled by the KeychainServer @@ -394,12 +395,14 @@ async def get_public_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[ if self.get_keychain_for_request(request).is_keyring_locked(): # pragma: no cover return {"success": False, "error": KEYCHAIN_ERR_LOCKED} - public_keys = self.get_keychain_for_request(request).get_all_public_keys() + public_keys = self.get_keychain_for_request(request).get_all_public_keys_of_type( + KEY_TYPES_TO_TYPES[request["type"]] + ) if len(public_keys) == 0: # pragma: no cover return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} fingerprint = request.get("fingerprint", None) - public_key: Optional[G1Element] = None + public_key: Optional[ObservationRoot] = None if fingerprint is not None: for pk in public_keys: if pk.get_fingerprint() == fingerprint: diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 19cb711543a1..8f066bf8262c 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -7,7 +7,7 @@ from functools import cached_property from hashlib import pbkdf2_hmac from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar import pkg_resources from bitstring import BitArray # pyright: reportMissingImports=false @@ -38,6 +38,9 @@ MIN_PASSPHRASE_LEN = 8 +_T_ObservationRoot = TypeVar("_T_ObservationRoot", bound=ObservationRoot) + + def supports_os_passphrase_storage() -> bool: return sys.platform in ["darwin", "win32", "cygwin"] @@ -221,6 +224,10 @@ class KeyTypes(str, Enum): G1_ELEMENT = "G1 Element" +TYPES_TO_KEY_TYPES: Dict[Type[ObservationRoot], KeyTypes] = {G1Element: KeyTypes.G1_ELEMENT} +KEY_TYPES_TO_TYPES: Dict[KeyTypes, Type[ObservationRoot]] = {v: k for k, v in TYPES_TO_KEY_TYPES.items()} + + @final @streamable @dataclass(frozen=True) @@ -491,15 +498,26 @@ def get_keys(self, include_secrets: bool = False) -> List[KeyData]: pass return all_keys - def get_all_public_keys(self) -> List[G1Element]: + def get_all_public_keys(self) -> List[ObservationRoot]: """ Returns all public keys. """ - all_keys: List[G1Element] = [] + all_keys: List[ObservationRoot] = [] + for index in range(MAX_KEYS + 1): + try: + key_data = self._get_key_data(index) + all_keys.append(key_data.observation_root) + except KeychainUserNotFound: + pass + return all_keys + + def get_all_public_keys_of_type(self, key_type: Type[_T_ObservationRoot]) -> List[_T_ObservationRoot]: + all_keys: List[_T_ObservationRoot] = [] for index in range(MAX_KEYS + 1): try: key_data = self._get_key_data(index) - if isinstance(key_data.observation_root, G1Element): + if key_data.key_type == TYPES_TO_KEY_TYPES[key_type]: + assert isinstance(key_data.observation_root, key_type) all_keys.append(key_data.observation_root) except KeychainUserNotFound: pass diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index adb9916454c0..0fec1b0442b5 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -529,7 +529,7 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash( puzzle_hash_for_synthetic_public_key(pk_parsed) ) - root_pubkey: bytes = self.wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big") + root_fingerprint: bytes = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") if index is None: # Pool wallet may have a secret key here if self.wallet_state_manager.private_key is not None: @@ -539,19 +539,20 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: ) if try_owner_sk.get_g1() == pk_parsed: return PathHint( - root_pubkey, + root_fingerprint, [uint64(12381), uint64(8444), uint64(5), uint64(pool_wallet_index)], ) return None return PathHint( - root_pubkey, + root_fingerprint, [uint64(12381), uint64(8444), uint64(2), uint64(index)], ) async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: - root_pubkey: G1Element = self.wallet_state_manager.root_pubkey + assert isinstance(self.wallet_state_manager.observation_root, G1Element) + root_pubkey: G1Element = self.wallet_state_manager.observation_root pk_lookup: Dict[int, G1Element] = {root_pubkey.get_fingerprint(): root_pubkey} sk_lookup: Dict[int, PrivateKey] = { root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index ab9d3f8fb17f..a237cef66847 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -58,6 +58,7 @@ from chia.util.ints import uint16, uint32, uint64, uint128 from chia.util.keychain import Keychain from chia.util.misc import to_batches +from chia.util.observation_root import ObservationRoot from chia.util.path import path_from_root from chia.util.profiler import mem_profile_task, profile_task from chia.util.streamable import Streamable, streamable @@ -222,12 +223,14 @@ def rollback_request_caches(self, reorg_height: int) -> None: async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool = False - ) -> Optional[Union[PrivateKey, G1Element]]: + ) -> Optional[Union[PrivateKey, ObservationRoot]]: try: keychain_proxy = await self.ensure_keychain_proxy() # Returns first key if fingerprint is None if private: - key: Optional[Union[PrivateKey, G1Element]] = await keychain_proxy.get_key_for_fingerprint(fingerprint) + key: Optional[Union[PrivateKey, ObservationRoot]] = await keychain_proxy.get_key_for_fingerprint( + fingerprint + ) else: key = await keychain_proxy.get_public_key_for_fingerprint(fingerprint) except KeychainIsEmpty: @@ -246,13 +249,17 @@ async def get_key_for_fingerprint( return key - async def get_key(self, fingerprint: Optional[int], private: bool = True) -> Optional[Union[PrivateKey, G1Element]]: + async def get_key( + self, fingerprint: Optional[int], private: bool = True + ) -> Optional[Union[PrivateKey, ObservationRoot]]: """ Attempt to get the private key for the given fingerprint. If the fingerprint is None, get_key_for_fingerprint() will return the first private key. Similarly, if a key isn't returned for the provided fingerprint, the first key will be returned. """ - key: Optional[Union[PrivateKey, G1Element]] = await self.get_key_for_fingerprint(fingerprint, private=private) + key: Optional[Union[PrivateKey, ObservationRoot]] = await self.get_key_for_fingerprint( + fingerprint, private=private + ) if key is None and fingerprint is not None: key = await self.get_key_for_fingerprint(None, private=private) @@ -389,17 +396,17 @@ async def _start_with_fingerprint( self.synced_peers = set() private_key = await self.get_key(fingerprint, private=True) if private_key is None: - public_key = await self.get_key(fingerprint, private=False) + observation_root = await self.get_key(fingerprint, private=False) else: assert isinstance(private_key, PrivateKey) - public_key = private_key.get_g1() - if public_key is None: + observation_root = private_key.get_g1() + if observation_root is None: self.log_out() return False - assert isinstance(public_key, G1Element) + assert not isinstance(observation_root, PrivateKey) # override with private key fetched in case it's different from what was passed if fingerprint is None: - fingerprint = public_key.get_fingerprint() + fingerprint = observation_root.get_fingerprint() if self.config.get("enable_profiler", False): if sys.getprofile() is not None: self.log.warning("not enabling profiler, getprofile() is already set") @@ -423,7 +430,7 @@ async def _start_with_fingerprint( self.server, self.root_path, self, - public_key, + observation_root, ) if self.state_changed_callback is not None: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 8ec1651ae158..94617b9ddbc5 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -59,6 +59,7 @@ 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.observation_root import ObservationRoot from chia.util.path import path_from_root from chia.util.streamable import Streamable from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS @@ -193,7 +194,7 @@ class WalletStateManager: main_wallet: Wallet wallets: Dict[uint32, WalletProtocol[Any]] private_key: Optional[PrivateKey] - root_pubkey: G1Element + observation_root: ObservationRoot trade_manager: TradeManager notification_manager: NotificationManager @@ -221,7 +222,7 @@ async def create( server: ChiaServer, root_path: Path, wallet_node: WalletNode, - root_pubkey: Optional[G1Element] = None, + observation_root: Optional[ObservationRoot] = None, ) -> WalletStateManager: self = WalletStateManager() @@ -276,17 +277,17 @@ async def create( self.private_key = private_key if private_key is None: # pragma: no cover - if root_pubkey is None: + if observation_root is None: raise ValueError("WalletStateManager requires either a root private key or root public key") else: - self.root_pubkey = root_pubkey + self.observation_root = observation_root else: calculated_root_public_key: G1Element = private_key.get_g1() - if root_pubkey is not None: - assert root_pubkey == calculated_root_public_key - self.root_pubkey = calculated_root_public_key + if observation_root is not None: + assert observation_root == calculated_root_public_key + self.observation_root = calculated_root_public_key - fingerprint = self.root_pubkey.get_fingerprint() + fingerprint = self.observation_root.get_fingerprint() puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, []) self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators) @@ -364,7 +365,9 @@ async def create( return self def get_public_key_unhardened(self, index: uint32) -> G1Element: - return master_pk_to_wallet_pk_unhardened(self.root_pubkey, index) + if not isinstance(self.observation_root, G1Element): + raise ValueError("Public key derivation is not supported for non-G1Element keys") + return master_pk_to_wallet_pk_unhardened(self.observation_root, index) async def get_private_key(self, puzzle_hash: bytes32) -> PrivateKey: record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash) @@ -446,7 +449,11 @@ async def create_more_puzzle_hashes( self.log.info(f"Start: {creating_msg}") if self.private_key is not None: intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) - intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.root_pubkey) + # This function shoul work for other types of observation roots too + # However to generalize this function beyond pubkeys is beyond the scope of current work + # So we're just going to sanitize and move on + assert isinstance(self.observation_root, G1Element) + intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.observation_root) for index in range(start_index, last_index): if target_wallet.type() == WalletType.POOLING_WALLET: continue diff --git a/tests/core/util/test_keychain.py b/tests/core/util/test_keychain.py index 96545ea4df4e..f738528c2ad2 100644 --- a/tests/core/util/test_keychain.py +++ b/tests/core/util/test_keychain.py @@ -82,6 +82,9 @@ def test_basic_add_delete(self, empty_temp_file_keyring: TempKeyring, seeded_ran assert kc._get_free_private_key_index() == 2 assert len(kc.get_all_private_keys()) == 2 assert len(kc.get_all_public_keys()) == 2 + all_pks: List[G1Element] = kc.get_all_public_keys_of_type(G1Element) + assert len(all_pks) == 2 + assert kc.get_all_private_keys()[0] == kc.get_first_private_key() assert kc.get_all_public_keys()[0] == kc.get_first_public_key() diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index cf9db61f1b36..8d43470a3dcf 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -6,6 +6,7 @@ from typing import List import pytest +from chia_rs import G1Element from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.protocols.wallet_protocol import CoinState @@ -928,8 +929,9 @@ async def test_cat_change_detection( # Mint CAT to ourselves, immediately spend it to an unhinted puzzle hash that we have manually added to the DB # We should pick up this coin as balance even though it is unhinted because it is "change" + assert isinstance(wallet_node_0.wallet_state_manager.observation_root, G1Element) pubkey_unhardened = master_pk_to_wallet_pk_unhardened( - wallet_node_0.wallet_state_manager.root_pubkey, uint32(100000000) + wallet_node_0.wallet_state_manager.observation_root, uint32(100000000) ) inner_puzhash = puzzle_hash_for_pk(pubkey_unhardened) puzzlehash_unhardened = construct_cat_puzzle( diff --git a/tests/wallet/test_sign_coin_spends.py b/tests/wallet/test_sign_coin_spends.py index 007427eb19dc..2756722bba57 100644 --- a/tests/wallet/test_sign_coin_spends.py +++ b/tests/wallet/test_sign_coin_spends.py @@ -75,7 +75,7 @@ async def test_wsm_sign_transaction() -> None: wsm.puzzle_store = await WalletPuzzleStore.create(db) wsm.constants = DEFAULT_CONSTANTS wsm.private_key = top_sk - wsm.root_pubkey = top_sk.get_g1() + wsm.observation_root = top_sk.get_g1() wsm.user_store = await WalletUserStore.create(db) wallet_info = await wsm.user_store.get_wallet_by_id(1) assert wallet_info is not None diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 585228aebf2e..34339cb17326 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -192,7 +192,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ] assert utx.signing_instructions.key_hints.path_hints == [ PathHint( - wallet_state_manager.root_pubkey.get_fingerprint().to_bytes(4, "big"), + wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big"), [uint64(12381), uint64(8444), uint64(2), uint64(derivation_record.index)], ) ] From 246fadbe25846c789509b98ca615e4cd8a8476e7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 13:01:53 -0800 Subject: [PATCH 038/274] Fix get_pk_for_fp --- chia/daemon/keychain_server.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index 773c7bd3f6a1..994c2305cf94 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -10,8 +10,7 @@ from chia.cmds.init_funcs import check_keys from chia.util.errors import KeychainException, KeychainFingerprintNotFound from chia.util.ints import uint32 -from chia.util.keychain import KEY_TYPES_TO_TYPES, Keychain, KeyData -from chia.util.observation_root import ObservationRoot +from chia.util.keychain import Keychain, KeyData from chia.util.streamable import Streamable, streamable # Commands that are handled by the KeychainServer @@ -395,23 +394,21 @@ async def get_public_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[ if self.get_keychain_for_request(request).is_keyring_locked(): # pragma: no cover return {"success": False, "error": KEYCHAIN_ERR_LOCKED} - public_keys = self.get_keychain_for_request(request).get_all_public_keys_of_type( - KEY_TYPES_TO_TYPES[request["type"]] - ) - if len(public_keys) == 0: # pragma: no cover + keys = self.get_keychain_for_request(request).get_keys(include_secrets=False) + if len(keys) == 0: # pragma: no cover return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} fingerprint = request.get("fingerprint", None) - public_key: Optional[ObservationRoot] = None + key_data: Optional[KeyData] = None if fingerprint is not None: - for pk in public_keys: - if pk.get_fingerprint() == fingerprint: - public_key = pk + for kd in keys: + if kd.observation_root.get_fingerprint() == fingerprint: + key_data = kd break else: - public_key = public_keys[0] + key_data = keys[0] - if public_key is not None: - return {"success": True, "pk": bytes(public_key).hex()} + if key_data is not None: + return {"success": True, "pk": bytes(key_data.observation_root).hex(), "type": key_data.key_type} else: return {"success": False, "error": KEYCHAIN_ERR_KEY_NOT_FOUND} From 59d085c6f40c2d7d85445d1951dd79e1bcd41672 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 30 Nov 2023 15:42:17 -0800 Subject: [PATCH 039/274] Wallet -> MainWalletProtocol --- chia/data_layer/data_layer_wallet.py | 5 +- chia/pools/pool_wallet.py | 14 +-- chia/simulator/full_node_simulator.py | 12 +- chia/wallet/cat_wallet/cat_wallet.py | 15 ++- chia/wallet/cat_wallet/dao_cat_wallet.py | 8 +- chia/wallet/dao_wallet/dao_wallet.py | 16 +-- chia/wallet/did_wallet/did_wallet.py | 17 ++- chia/wallet/nft_wallet/nft_wallet.py | 13 +-- chia/wallet/trade_manager.py | 4 +- chia/wallet/vc_wallet/cr_cat_wallet.py | 13 +-- chia/wallet/vc_wallet/vc_wallet.py | 9 +- chia/wallet/wallet_node.py | 2 + chia/wallet/wallet_protocol.py | 105 +++++++++++++++++- chia/wallet/wallet_state_manager.py | 7 +- tests/core/data_layer/test_data_rpc.py | 4 +- tests/core/mempool/test_mempool_manager.py | 4 +- tests/wallet/conftest.py | 4 +- tests/wallet/rpc/test_wallet_rpc.py | 43 +++---- .../simple_sync/test_simple_sync_protocol.py | 8 +- tests/wallet/test_signer_protocol.py | 4 +- tests/wallet/vc_wallet/test_vc_wallet.py | 4 +- 21 files changed, 203 insertions(+), 108 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index 1d7955fdf87b..c6442519d51b 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -61,10 +61,9 @@ from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig, TXConfigLoader from chia.wallet.util.wallet_sync_utils import fetch_coin_spend, fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager @@ -122,7 +121,7 @@ class DataLayerWallet: log: logging.Logger wallet_info: WalletInfo wallet_id: uint8 - standard_wallet: Wallet + standard_wallet: MainWalletProtocol """ interface used by datalayer for interacting with the chain """ diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 212f12f1f9ea..d6adae90ff97 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -50,9 +50,9 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import MainWalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager @@ -74,7 +74,7 @@ class PoolWallet: wallet_state_manager: WalletStateManager log: logging.Logger wallet_info: WalletInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_id: int next_transaction_fee: uint64 = uint64(0) next_tx_config: TXConfig = DEFAULT_TX_CONFIG @@ -331,7 +331,7 @@ async def rewind(self, block_height: int) -> bool: async def create( cls, wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, launcher_coin_id: bytes32, block_spends: List[CoinSpend], block_height: uint32, @@ -372,7 +372,7 @@ async def create( async def create_from_db( cls, wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> PoolWallet: @@ -392,7 +392,7 @@ async def create_from_db( @staticmethod async def create_new_pool_wallet_transaction( wallet_state_manager: Any, - main_wallet: Wallet, + main_wallet: MainWalletProtocol, initial_target_state: PoolState, tx_config: TXConfig, fee: uint64 = uint64(0), @@ -417,7 +417,7 @@ async def create_new_pool_wallet_transaction( if p2_singleton_delay_time is None: p2_singleton_delay_time = uint64(604800) - unspent_records = await wallet_state_manager.coin_store.get_unspent_coins_for_wallet(standard_wallet.wallet_id) + unspent_records = await wallet_state_manager.coin_store.get_unspent_coins_for_wallet(standard_wallet.id()) balance = await standard_wallet.get_confirmed_balance(unspent_records) if balance < PoolWallet.MINIMUM_INITIAL_BALANCE: raise ValueError("Not enough balance in main wallet to create a managed plotting pool.") @@ -605,7 +605,7 @@ async def generate_travel_transactions( @staticmethod async def generate_launcher_spend( - standard_wallet: Wallet, + standard_wallet: MainWalletProtocol, amount: uint64, fee: uint64, initial_target_state: PoolState, diff --git a/chia/simulator/full_node_simulator.py b/chia/simulator/full_node_simulator.py index dae75bd1a94f..7e41d3dc4903 100644 --- a/chia/simulator/full_node_simulator.py +++ b/chia/simulator/full_node_simulator.py @@ -28,8 +28,8 @@ from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager @@ -42,7 +42,7 @@ class _Default: timeout_per_block = 5 -async def wait_for_coins_in_wallet(coins: Set[Coin], wallet: Wallet, timeout: Optional[float] = 5): +async def wait_for_coins_in_wallet(coins: Set[Coin], wallet: MainWalletProtocol, timeout: Optional[float] = 5): """Wait until all of the specified coins are simultaneously reported as spendable by the wallet. @@ -324,7 +324,7 @@ async def farm_blocks_to_puzzlehash( async def farm_blocks_to_wallet( self, count: int, - wallet: Wallet, + wallet: MainWalletProtocol, timeout: Union[None, _Default, float] = default, ) -> int: """Farm the requested number of blocks to the passed wallet. This will @@ -390,7 +390,7 @@ async def farm_blocks_to_wallet( async def farm_rewards_to_wallet( self, amount: int, - wallet: Wallet, + wallet: MainWalletProtocol, timeout: Union[None, _Default, float] = default, ) -> int: """Farm at least the requested amount of mojos to the passed wallet. Extra @@ -584,7 +584,7 @@ async def process_coin_spends( if len(coin_set) == 0: return - async def process_all_wallet_transactions(self, wallet: Wallet, timeout: Optional[float] = 5) -> None: + async def process_all_wallet_transactions(self, wallet: MainWalletProtocol, timeout: Optional[float] = 5) -> None: # TODO: Maybe something could be done around waiting for the tx to enter the # mempool. Maybe not, might be too many races or such. wallet_state_manager: Optional[WalletStateManager] = wallet.wallet_state_manager @@ -627,7 +627,7 @@ async def check_transactions_confirmed( async def create_coins_with_amounts( self, amounts: List[uint64], - wallet: Wallet, + wallet: MainWalletProtocol, per_transaction_record_group: int = 50, timeout: Union[None, float] = 15, ) -> Set[Coin]: diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 8fd9c1cf8c60..ece019da434b 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -54,10 +54,9 @@ from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager @@ -95,7 +94,7 @@ class CATWallet: log: logging.Logger wallet_info: WalletInfo cat_info: CATInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol lineage_store: CATLineageStore @staticmethod @@ -105,7 +104,7 @@ def default_wallet_name_for_unknown_cat(limitations_program_hash_hex: str) -> st @staticmethod async def create_new_cat_wallet( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, cat_tail_info: Dict[str, Any], amount: uint64, tx_config: TXConfig, @@ -115,7 +114,7 @@ async def create_new_cat_wallet( self = CATWallet() self.standard_wallet = wallet self.log = logging.getLogger(__name__) - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") @@ -201,7 +200,7 @@ async def create_new_cat_wallet( @staticmethod async def get_or_create_wallet_for_cat( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, limitations_program_hash_hex: str, name: Optional[str] = None, ) -> CATWallet: @@ -254,7 +253,7 @@ async def get_or_create_wallet_for_cat( async def create_from_puzzle_info( cls, wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, puzzle_driver: PuzzleInfo, name: Optional[str] = None, # We're hinting this as Any for mypy by should explore adding this to the wallet protocol and hinting properly @@ -280,7 +279,7 @@ async def create_from_puzzle_info( @staticmethod async def create( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, ) -> CATWallet: self = CATWallet() diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index 790fe81a0da4..bfb8211a62d9 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -39,9 +39,9 @@ from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import MainWalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager @@ -61,7 +61,7 @@ class DAOCATWallet: log: logging.Logger wallet_info: WalletInfo dao_cat_info: DAOCATInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol cost_of_single_tx: Optional[int] lineage_store: CATLineageStore @@ -72,7 +72,7 @@ def type(cls) -> WalletType: @staticmethod async def create( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, ) -> DAOCATWallet: self = DAOCATWallet() @@ -93,7 +93,7 @@ async def create( @staticmethod async def get_or_create_wallet_for_cat( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, limitations_program_hash_hex: str, name: Optional[str] = None, ) -> DAOCATWallet: diff --git a/chia/wallet/dao_wallet/dao_wallet.py b/chia/wallet/dao_wallet/dao_wallet.py index e73288175ad8..4b57fd5c7d75 100644 --- a/chia/wallet/dao_wallet/dao_wallet.py +++ b/chia/wallet/dao_wallet/dao_wallet.py @@ -74,9 +74,9 @@ from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import MainWalletProtocol class DAOWallet: @@ -112,13 +112,13 @@ class DAOWallet: wallet_info: WalletInfo dao_info: DAOInfo dao_rules: DAORules - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_id: uint32 @staticmethod async def create_new_dao_and_wallet( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, amount_of_cats: uint64, dao_rules: DAORules, tx_config: TXConfig, @@ -148,7 +148,7 @@ async def create_new_dao_and_wallet( self.standard_wallet = wallet self.log = logging.getLogger(name if name else __name__) - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount_of_cats > bal: raise ValueError(f"Your balance of {bal} mojos is not enough to create {amount_of_cats} CATs") @@ -172,7 +172,7 @@ async def create_new_dao_and_wallet( name, WalletType.DAO.value, info_as_string ) self.wallet_id = self.wallet_info.id - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() try: txs = await self.generate_new_dao( @@ -205,7 +205,7 @@ async def create_new_dao_and_wallet( @staticmethod async def create_new_dao_wallet_for_existing_dao( wallet_state_manager: Any, - main_wallet: Wallet, + main_wallet: MainWalletProtocol, treasury_id: bytes32, filter_amount: uint64 = uint64(1), name: Optional[str] = None, @@ -271,7 +271,7 @@ async def create_new_dao_wallet_for_existing_dao( @staticmethod async def create( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> DAOWallet: @@ -1529,7 +1529,7 @@ async def _create_treasury_fund_transaction( ) -> List[TransactionRecord]: if funding_wallet.type() == WalletType.STANDARD_WALLET.value: p2_singleton_puzhash = get_p2_singleton_puzhash(self.dao_info.treasury_id, asset_id=None) - wallet: Wallet = funding_wallet # type: ignore[assignment] + wallet: MainWalletProtocol = funding_wallet # type: ignore[assignment] return await wallet.generate_signed_transaction( amount, p2_singleton_puzhash, diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 87e67031628c..9a59ca3cc19e 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -50,10 +50,9 @@ from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend, fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import WalletProtocol +from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol class DIDWallet: @@ -65,7 +64,7 @@ class DIDWallet: log: logging.Logger wallet_info: WalletInfo did_info: DIDInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol base_puzzle_program: Optional[bytes] base_inner_puzzle_hash: Optional[bytes32] wallet_id: int @@ -73,7 +72,7 @@ class DIDWallet: @staticmethod async def create_new_did_wallet( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, amount: uint64, backups_ids: List = [], num_of_backup_ids_needed: uint64 = None, @@ -103,7 +102,7 @@ async def create_new_did_wallet( self.base_inner_puzzle_hash = None self.standard_wallet = wallet self.log = logging.getLogger(name if name else __name__) - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") @@ -122,7 +121,7 @@ async def create_new_did_wallet( name, WalletType.DECENTRALIZED_ID.value, info_as_string ) self.wallet_id = self.wallet_info.id - std_wallet_id = self.standard_wallet.wallet_id + std_wallet_id = self.standard_wallet.id() bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") @@ -142,7 +141,7 @@ async def create_new_did_wallet( @staticmethod async def create_new_did_wallet_from_recovery( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, backup_data: str, name: Optional[str] = None, ): @@ -182,7 +181,7 @@ async def create_new_did_wallet_from_recovery( @staticmethod async def create_new_did_wallet_from_coin_spend( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, launch_coin: Coin, inner_puzzle: Program, coin_spend: CoinSpend, @@ -257,7 +256,7 @@ async def create_new_did_wallet_from_coin_spend( @staticmethod async def create( wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: str = None, ): diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 1a3ed43dd455..fe2fcd68adff 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -54,11 +54,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_nft_store import WalletNftStore -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol _T_NFTWallet = TypeVar("_T_NFTWallet", bound="NFTWallet") @@ -71,7 +70,7 @@ class NFTWallet: log: logging.Logger wallet_info: WalletInfo nft_wallet_info: NFTWalletInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_id: int nft_store: WalletNftStore @@ -83,7 +82,7 @@ def did_id(self) -> Optional[bytes32]: async def create_new_nft_wallet( cls: Type[_T_NFTWallet], wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, did_id: Optional[bytes32] = None, name: Optional[str] = None, ) -> _T_NFTWallet: @@ -103,7 +102,7 @@ async def create_new_nft_wallet( ) self.wallet_id = self.wallet_info.id self.nft_store = wallet_state_manager.nft_store - self.log.debug("NFT wallet id: %r and standard wallet id: %r", self.wallet_id, self.standard_wallet.wallet_id) + self.log.debug("NFT wallet id: %r and standard wallet id: %r", self.wallet_id, self.standard_wallet.id()) await self.wallet_state_manager.add_new_wallet(self) self.log.debug("Generated a new NFT wallet: %s", self.__dict__) @@ -113,7 +112,7 @@ async def create_new_nft_wallet( async def create( cls: Type[_T_NFTWallet], wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> _T_NFTWallet: @@ -553,7 +552,7 @@ async def match_puzzle_info(self, puzzle_driver: PuzzleInfo) -> bool: async def create_from_puzzle_info( cls: Any, wallet_state_manager: Any, - wallet: Wallet, + wallet: MainWalletProtocol, puzzle_driver: PuzzleInfo, name: Optional[str] = None, ) -> Any: diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index abb72f01afab..977cdc90318a 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -37,8 +37,8 @@ from chia.wallet.util.wallet_types import WalletType from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker, construct_pending_approval_state from chia.wallet.vc_wallet.vc_wallet import VCWallet -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord +from chia.wallet.wallet_protocol import WalletProtocol OFFER_MOD = load_clvm_maybe_recompile("settlement_payments.clsp") @@ -604,7 +604,7 @@ async def maybe_create_wallets_for_offer(self, offer: Offer) -> None: if key is None: continue # ATTENTION: new_wallets - exists: Optional[Wallet] = await wsm.get_wallet_for_puzzle_info(offer.driver_dict[key]) + exists: Optional[WalletProtocol[Any]] = await wsm.get_wallet_for_puzzle_info(offer.driver_dict[key]) if exists is None: await wsm.create_wallet_for_puzzle_info(offer.driver_dict[key]) diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index 38696b21dc3c..7d5f23aac25e 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -55,10 +55,9 @@ ) from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential from chia.wallet.vc_wallet.vc_wallet import VCWallet -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import MetadataTypes, WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager @@ -69,7 +68,7 @@ class CRCATWallet(CATWallet): log: logging.Logger wallet_info: WalletInfo info: CRCATInfo - standard_wallet: Wallet + standard_wallet: MainWalletProtocol @staticmethod def default_wallet_name_for_unknown_cat(limitations_program_hash_hex: str) -> str: @@ -82,7 +81,7 @@ def cost_of_single_tx(self) -> int: @staticmethod async def create_new_cat_wallet( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, cat_tail_info: Dict[str, Any], amount: uint64, tx_config: TXConfig, @@ -94,7 +93,7 @@ async def create_new_cat_wallet( @staticmethod async def get_or_create_wallet_for_cat( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, limitations_program_hash_hex: str, name: Optional[str] = None, authorized_providers: Optional[List[bytes32]] = None, @@ -130,7 +129,7 @@ async def get_or_create_wallet_for_cat( async def create_from_puzzle_info( cls, wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, puzzle_driver: PuzzleInfo, name: Optional[str] = None, # We're hinting this as Any for mypy by should explore adding this to the wallet protocol and hinting properly @@ -151,7 +150,7 @@ async def create_from_puzzle_info( @staticmethod async def create( wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, ) -> CRCATWallet: self = CRCATWallet() diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index 0a7644443b79..aa0db93892ac 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -43,10 +43,9 @@ from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, CRCATSpend, ProofsChecker, construct_pending_approval_state from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord, VCStore -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo -from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol, WalletProtocol if TYPE_CHECKING: from chia.wallet.wallet_state_manager import WalletStateManager # pragma: no cover @@ -57,7 +56,7 @@ class VCWallet: wallet_state_manager: WalletStateManager log: logging.Logger - standard_wallet: Wallet + standard_wallet: MainWalletProtocol wallet_info: WalletInfo store: VCStore @@ -65,7 +64,7 @@ class VCWallet: async def create_new_vc_wallet( cls: Type[_T_VCWallet], wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, name: Optional[str] = None, ) -> _T_VCWallet: name = "VCWallet" if name is None else name @@ -82,7 +81,7 @@ async def create_new_vc_wallet( async def create( cls: Type[_T_VCWallet], wallet_state_manager: WalletStateManager, - wallet: Wallet, + wallet: MainWalletProtocol, wallet_info: WalletInfo, name: Optional[str] = None, ) -> _T_VCWallet: diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index a237cef66847..c867023e86c0 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -77,6 +77,7 @@ subscribe_to_phs, ) from chia.wallet.util.wallet_types import CoinType, WalletType +from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler, get_wp_fork_point @@ -430,6 +431,7 @@ async def _start_with_fingerprint( self.server, self.root_path, self, + Wallet, observation_root, ) diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 179206053f9c..30f907e4ca8b 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -1,18 +1,31 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, Set, Tuple, TypeVar +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, TypeVar -from chia_rs import G1Element -from typing_extensions import NotRequired, Protocol, TypedDict +from chia_rs import G1Element, G2Element +from typing_extensions import NotRequired, Protocol, TypedDict, Unpack from chia.server.ws_connection import WSChiaConnection 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.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64, uint128 +from chia.wallet.conditions import Condition from chia.wallet.nft_wallet.nft_info import NFTCoinInfo -from chia.wallet.util.tx_config import CoinSelectionConfig +from chia.wallet.payment import Payment +from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.signer_protocol import ( + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + SumHint, +) +from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo @@ -76,6 +89,90 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: wallet_state_manager: WalletStateManager # pylint: disable=used-before-assignment +class MainWalletProtocol(WalletProtocol[ClawbackMetadata], Protocol): + @property + def max_send_quantity(self) -> int: + ... + + @staticmethod + async def create( + wallet_state_manager: Any, + info: WalletInfo, + name: str = ..., + ) -> MainWalletProtocol: + ... + + async def get_new_puzzle(self) -> Program: + ... + + async def get_new_puzzlehash(self) -> bytes32: + ... + + # This isn't part of the WalletProtocol but it should be + # Also this doesn't likely conform to the eventual one that ends up in WalletProtocol + 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, + memos: Optional[List[bytes]] = None, + puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None, + extra_conditions: Tuple[Condition, ...] = tuple(), + **kwargs: Unpack[GSTOptionalArgs], + ) -> List[TransactionRecord]: + ... + + def puzzle_for_pk(self, pubkey: G1Element) -> Program: + ... + + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: + ... + + async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> Tuple[G1Element, G2Element]: + ... + + async def get_puzzle_hash(self, new: bool) -> bytes32: + ... + + async def apply_signatures( + self, spends: List[Spend], signing_responses: List[SigningResponse] + ) -> SignedTransaction: + ... + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> List[SigningResponse]: + ... + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + ... + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + ... + + async def create_tandem_xch_tx( + self, + fee: uint64, + tx_config: TXConfig, + extra_conditions: Tuple[Condition, ...] = tuple(), + ) -> TransactionRecord: + ... + + def make_solution( + self, + primaries: List[Payment], + conditions: Tuple[Condition, ...] = tuple(), + fee: uint64 = uint64(0), + ) -> Program: + ... + + async def get_puzzle(self, new: bool) -> Program: + ... + + class GSTOptionalArgs(TypedDict): # DataLayerWallet launcher_id: NotRequired[Optional[bytes32]] diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 94617b9ddbc5..5a63c865b1f7 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -151,7 +151,7 @@ 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_protocol import MainWalletProtocol, 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 @@ -191,7 +191,7 @@ class WalletStateManager: db_path: Path db_wrapper: DBWrapper2 - main_wallet: Wallet + main_wallet: MainWalletProtocol wallets: Dict[uint32, WalletProtocol[Any]] private_key: Optional[PrivateKey] observation_root: ObservationRoot @@ -222,6 +222,7 @@ async def create( server: ChiaServer, root_path: Path, wallet_node: WalletNode, + main_wallet_driver: Type[MainWalletProtocol], observation_root: Optional[ObservationRoot] = None, ) -> WalletStateManager: self = WalletStateManager() @@ -291,7 +292,7 @@ async def create( puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, []) self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators) - self.main_wallet = await Wallet.create(self, main_wallet_info) + self.main_wallet = await main_wallet_driver.create(self, main_wallet_info) self.wallets = {main_wallet_info.id: self.main_wallet} diff --git a/tests/core/data_layer/test_data_rpc.py b/tests/core/data_layer/test_data_rpc.py index 8588f1ef4ad9..ca19a0240ae0 100644 --- a/tests/core/data_layer/test_data_rpc.py +++ b/tests/core/data_layer/test_data_rpc.py @@ -42,9 +42,9 @@ from chia.util.timing import adjusted_timeout, backoff_times from chia.wallet.trading.offer import Offer as TradingOffer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode from chia.wallet.wallet_node_api import WalletNodeAPI +from chia.wallet.wallet_protocol import MainWalletProtocol from tests.util.time_out_assert import time_out_assert pytestmark = pytest.mark.data_layer @@ -740,7 +740,7 @@ async def offer_setup_fixture( ) -> AsyncIterator[OfferSetup]: [full_node_service], wallet_services, bt = two_wallet_nodes_services full_node_api = full_node_service._api - wallets: List[Wallet] = [] + wallets: List[MainWalletProtocol] = [] for wallet_service in wallet_services: wallet_node = wallet_service._node assert wallet_node.server is not None diff --git a/tests/core/mempool/test_mempool_manager.py b/tests/core/mempool/test_mempool_manager.py index 194db4ce986b..4806622ebbfe 100644 --- a/tests/core/mempool/test_mempool_manager.py +++ b/tests/core/mempool/test_mempool_manager.py @@ -44,9 +44,9 @@ from chia.wallet.conditions import AssertCoinAnnouncement from chia.wallet.payment import Payment from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol IDENTITY_PUZZLE = SerializedProgram.from_program(Program.to(1)) IDENTITY_PUZZLE_HASH = IDENTITY_PUZZLE.get_tree_hash() @@ -1429,7 +1429,7 @@ async def farm_a_block(full_node_api: FullNodeSimulator, wallet_node: WalletNode async def make_setup_and_coins( full_node_api: FullNodeSimulator, wallet_node: WalletNode - ) -> Tuple[Wallet, list[WalletCoinRecord], bytes32]: + ) -> Tuple[MainWalletProtocol, list[WalletCoinRecord], bytes32]: wallet = wallet_node.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() phs = [await wallet.get_new_puzzlehash() for _ in range(3)] diff --git a/tests/wallet/conftest.py b/tests/wallet/conftest.py index 00272e933c88..6318c60ec9d0 100644 --- a/tests/wallet/conftest.py +++ b/tests/wallet/conftest.py @@ -17,8 +17,8 @@ from chia.wallet.derivation_record import DerivationRecord from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import Balance, WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager OPP_DICT = {"<": operator.lt, ">": operator.gt, "<=": operator.le, ">=": operator.ge} @@ -54,7 +54,7 @@ class WalletStateTransition: class WalletEnvironment: wallet_node: WalletNode wallet_state_manager: WalletStateManager - xch_wallet: Wallet + xch_wallet: MainWalletProtocol rpc_client: WalletRpcClient wallet_states: Dict[uint32, WalletState] wallet_aliases: Dict[str, int] = field(default_factory=dict) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 605aa32f51bd..2edec6b7cf45 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -60,11 +60,10 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import CoinType, WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_coin_store import GetCoinRecords from chia.wallet.wallet_node import WalletNode -from chia.wallet.wallet_protocol import WalletProtocol +from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol from tests.conftest import ConsensusMode from tests.util.time_out_assert import time_out_assert, time_out_assert_not_none from tests.wallet.test_wallet_coin_store import ( @@ -102,7 +101,7 @@ class WalletBundle: service: Service node: WalletNode rpc_client: WalletRpcClient - wallet: Wallet + wallet: MainWalletProtocol @dataclasses.dataclass @@ -204,7 +203,9 @@ async def wallet_rpc_environment(two_wallet_nodes_services, request, self_hostna yield WalletRpcTestEnvironment(wallet_bundle_1, wallet_bundle_2, node_bundle) -async def create_tx_outputs(wallet: Wallet, output_args: List[Tuple[int, Optional[List[str]]]]) -> List[Dict[str, Any]]: +async def create_tx_outputs( + wallet: MainWalletProtocol, output_args: List[Tuple[int, Optional[List[str]]]] +) -> List[Dict[str, Any]]: outputs = [] for args in output_args: output = {"amount": uint64(args[0]), "puzzle_hash": await wallet.get_new_puzzlehash()} @@ -295,7 +296,7 @@ def update_verify_signature_request(request: Dict[str, Any], prefix_hex_values: async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -368,7 +369,7 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -401,7 +402,7 @@ async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironmen @pytest.mark.anyio async def test_get_balance(wallet_rpc_environment: WalletRpcTestEnvironment): env = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api wallet_rpc_client = env.wallet_1.rpc_client @@ -418,7 +419,7 @@ async def test_get_balance(wallet_rpc_environment: WalletRpcTestEnvironment): @pytest.mark.anyio async def test_get_farmed_amount(wallet_rpc_environment: WalletRpcTestEnvironment): env = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet full_node_api: FullNodeSimulator = env.full_node.api wallet_rpc_client = env.wallet_1.rpc_client await full_node_api.farm_blocks_to_wallet(2, wallet) @@ -443,7 +444,7 @@ async def test_get_farmed_amount(wallet_rpc_environment: WalletRpcTestEnvironmen @pytest.mark.anyio async def test_get_farmed_amount_with_fee(wallet_rpc_environment: WalletRpcTestEnvironment): env = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet full_node_api: FullNodeSimulator = env.full_node.api wallet_rpc_client = env.wallet_1.rpc_client wallet_node: WalletNode = env.wallet_1.node @@ -509,7 +510,7 @@ async def test_create_signed_transaction( ): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_1_node: WalletNode = env.wallet_1.node wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client full_node_api: FullNodeSimulator = env.full_node.api @@ -590,7 +591,7 @@ async def test_create_signed_transaction( async def test_create_signed_transaction_with_coin_announcement(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client client_node: FullNodeRpcClient = env.full_node.rpc_client @@ -621,7 +622,7 @@ async def test_create_signed_transaction_with_coin_announcement(wallet_rpc_envir async def test_create_signed_transaction_with_puzzle_announcement(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client client_node: FullNodeRpcClient = env.full_node.rpc_client @@ -651,7 +652,7 @@ async def test_create_signed_transaction_with_puzzle_announcement(wallet_rpc_env @pytest.mark.anyio async def test_create_signed_transaction_with_excluded_coins(wallet_rpc_environment: WalletRpcTestEnvironment) -> None: env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_1: Wallet = env.wallet_1.wallet + wallet_1: MainWalletProtocol = env.wallet_1.wallet wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client full_node_api: FullNodeSimulator = env.full_node.api full_node_rpc: FullNodeRpcClient = env.full_node.rpc_client @@ -807,7 +808,7 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -857,7 +858,7 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir async def test_get_transactions(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -1403,8 +1404,8 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_1: Wallet = env.wallet_1.wallet - wallet_2: Wallet = env.wallet_2.wallet + wallet_1: MainWalletProtocol = env.wallet_1.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_1_node: WalletNode = env.wallet_1.node wallet_2_node: WalletNode = env.wallet_2.node wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client @@ -1536,7 +1537,7 @@ async def test_nft_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment wallet_1_node: WalletNode = env.wallet_1.node wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_2_node: WalletNode = env.wallet_2.node wallet_2_rpc: WalletRpcClient = env.wallet_2.rpc_client full_node_api: FullNodeSimulator = env.full_node.api @@ -1628,7 +1629,7 @@ async def have_nfts(): async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet: Wallet = env.wallet_1.wallet + wallet: MainWalletProtocol = env.wallet_1.wallet wallet_node: WalletNode = env.wallet_1.node client: WalletRpcClient = env.wallet_1.rpc_client @@ -1762,7 +1763,7 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client @@ -2055,7 +2056,7 @@ async def test_get_coin_records_rpc_failures( async def test_notification_rpcs(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment - wallet_2: Wallet = env.wallet_2.wallet + wallet_2: MainWalletProtocol = env.wallet_2.wallet wallet_node: WalletNode = env.wallet_1.node full_node_api: FullNodeSimulator = env.full_node.api client: WalletRpcClient = env.wallet_1.rpc_client diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index 18c08d3b29ab..7e987675f5f5 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -154,7 +154,7 @@ async def test_subscribe_for_ph(self, simulator_and_wallet, self_hostname): assert all_coins == notified_all_coins wsm: WalletStateManager = wallet_node.wallet_state_manager - wallet: Wallet = wsm.wallets[1] + wallet: MainWalletProtocol = wsm.wallets[1] puzzle_hash = await wallet.get_new_puzzlehash() for i in range(0, num_blocks): @@ -238,7 +238,7 @@ async def test_subscribe_for_coin_id(self, simulator_and_wallet, self_hostname): wallet_node, server_2 = wallets[0] fn_server = full_node_api.full_node.server wsm: WalletStateManager = wallet_node.wallet_state_manager - standard_wallet: Wallet = wsm.wallets[1] + standard_wallet: MainWalletProtocol = wsm.wallets[1] puzzle_hash = await standard_wallet.get_new_puzzlehash() await server_2.start_client(PeerInfo(self_hostname, fn_server.get_port()), None) @@ -340,7 +340,7 @@ async def test_subscribe_for_ph_reorg(self, simulator_and_wallet, self_hostname) wallet_node, server_2 = wallets[0] fn_server = full_node_api.full_node.server wsm: WalletStateManager = wallet_node.wallet_state_manager - standard_wallet: Wallet = wsm.wallets[1] + standard_wallet: MainWalletProtocol = wsm.wallets[1] puzzle_hash = await standard_wallet.get_new_puzzlehash() await server_2.start_client(PeerInfo(self_hostname, fn_server.get_port()), None) @@ -415,7 +415,7 @@ async def test_subscribe_for_coin_id_reorg(self, simulator_and_wallet, self_host wallet_node, server_2 = wallets[0] fn_server = full_node_api.full_node.server wsm: WalletStateManager = wallet_node.wallet_state_manager - standard_wallet: Wallet = wsm.wallets[1] + standard_wallet: MainWalletProtocol = wsm.wallets[1] puzzle_hash = await standard_wallet.get_new_puzzlehash() await server_2.start_client(PeerInfo(self_hostname, fn_server.get_port()), None) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 34339cb17326..3a5d14050102 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -34,7 +34,7 @@ clvm_serialization_mode, ) from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG -from chia.wallet.wallet import Wallet +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager from tests.wallet.conftest import WalletStateTransition, WalletTestFramework @@ -155,7 +155,7 @@ class TempStreamable(Streamable): ) @pytest.mark.anyio async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFramework) -> None: - wallet: Wallet = wallet_environments.environments[0].xch_wallet + wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client diff --git a/tests/wallet/vc_wallet/test_vc_wallet.py b/tests/wallet/vc_wallet/test_vc_wallet.py index 1854df0c46a8..2a3fe826f418 100644 --- a/tests/wallet/vc_wallet/test_vc_wallet.py +++ b/tests/wallet/vc_wallet/test_vc_wallet.py @@ -27,15 +27,15 @@ from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker, construct_cr_layer from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord -from chia.wallet.wallet import Wallet from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_protocol import MainWalletProtocol from tests.util.time_out_assert import time_out_assert_not_none from tests.wallet.conftest import WalletEnvironment, WalletStateTransition, WalletTestFramework async def mint_cr_cat( num_blocks: int, - wallet_0: Wallet, + wallet_0: MainWalletProtocol, wallet_node_0: WalletNode, client_0: WalletRpcClient, full_node_api: FullNodeSimulator, From 5dcc0d2bc1d452195ae4c0db1b91575466ea45c2 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 Dec 2023 10:31:03 -0800 Subject: [PATCH 040/274] pylint --- tests/wallet/simple_sync/test_simple_sync_protocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index 7e987675f5f5..c2383441db3c 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -26,6 +26,7 @@ from chia.util.ints import uint32, uint64 from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.wallet import Wallet +from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import add_dummy_connection from tests.util.time_out_assert import time_out_assert From cbf9c4b53dbac57c694ca7e721531629efe801c6 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 07:38:15 -0800 Subject: [PATCH 041/274] Add RPCEndpoint comment --- chia/rpc/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index ca48ade42812..d36958dec52c 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -17,6 +17,9 @@ log = logging.getLogger(__name__) +# TODO: consolidate this with chia.rpc.rpc_server.Endpoint +# Not all endpoints only take a dictionary so that definition is imperfect +# This definition is weaker than that one however because the arguments can be anything RpcEndpoint = Callable[..., Coroutine[Any, Any, Dict[str, Any]]] MarshallableRpcEndpoint = Callable[..., Coroutine[Any, Any, Streamable]] From 6f0736127ef281aafff7d8dab58f1341b31ab61a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 7 Dec 2023 08:31:51 -0800 Subject: [PATCH 042/274] Address comments by @altendky --- chia/rpc/util.py | 13 ++++--------- tests/core/test_rpc_util.py | 7 +++++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index d36958dec52c..f04eeaf15bd6 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -3,7 +3,7 @@ import logging import traceback from dataclasses import dataclass -from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, TypeVar, get_type_hints +from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, get_type_hints import aiohttp from typing_extensions import TypedDict, dataclass_transform @@ -28,11 +28,8 @@ class RequestType(TypedDict): pass -_T_Streamable = TypeVar("_T_Streamable", bound="Streamable") - - @dataclass_transform() -def get_streamable_from_request_type(cls: Type[RequestType]) -> Type[_T_Streamable]: +def get_streamable_from_request_type(cls: Type[RequestType]) -> Type[Streamable]: return streamable( dataclass(frozen=True)(type("_" + cls.__name__, (Streamable,), {"__annotations__": cls.__annotations__})) ) @@ -42,12 +39,10 @@ def marshall(func: MarshallableRpcEndpoint) -> RpcEndpoint: hints = get_type_hints(func) request_class: Type[RequestType] = hints["request"] - async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]: + async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: object) -> Dict[str, Any]: response_obj: Streamable = await func( self, - request_class( - get_streamable_from_request_type(request_class).from_json_dict(request).__dict__ # type: ignore - ), + get_streamable_from_request_type(request_class).from_json_dict(request).__dict__, *args, **kwargs, ) diff --git a/tests/core/test_rpc_util.py b/tests/core/test_rpc_util.py index 4ac09fbac7a2..97deb287287b 100644 --- a/tests/core/test_rpc_util.py +++ b/tests/core/test_rpc_util.py @@ -27,13 +27,16 @@ class TestRequestType(RequestType): @dataclass(frozen=True) class TestResponseObject(Streamable): qat: List[str] + sub: SubObject @pytest.mark.anyio async def test_rpc_marshalling() -> None: @marshall async def test_rpc_endpoint(self: None, request: TestRequestType) -> TestResponseObject: - return TestResponseObject([request["foofoo"], str(request["barbar"]), request["bat"].hex(), request["bam"].qux]) + return TestResponseObject( + [request["foofoo"], str(request["barbar"]), request["bat"].hex(), request["bam"].qux], request["bam"] + ) assert await test_rpc_endpoint( None, @@ -45,4 +48,4 @@ async def test_rpc_endpoint(self: None, request: TestRequestType) -> TestRespons "qux": "qux", }, }, - ) == {"qat": ["foofoo", "1", "ff", "qux"]} + ) == {"qat": ["foofoo", "1", "ff", "qux"], "sub": {"qux": "qux"}} From c39660e108f4df2d3d8b4cedfffb1e9483e192ed Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 15 Dec 2023 13:44:15 -0800 Subject: [PATCH 043/274] Just go back to Streamable for requests --- chia/rpc/util.py | 21 ++++----------------- tests/core/test_rpc_util.py | 10 ++++++---- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index f04eeaf15bd6..40f03a54d4dd 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -2,15 +2,13 @@ import logging import traceback -from dataclasses import dataclass from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, get_type_hints import aiohttp -from typing_extensions import TypedDict, dataclass_transform from chia.types.blockchain_format.coin import Coin from chia.util.json_util import obj_to_response -from chia.util.streamable import Streamable, streamable +from chia.util.streamable import Streamable from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_from_json_dicts, parse_timelock_info from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import TXConfig, TXConfigLoader @@ -24,25 +22,14 @@ MarshallableRpcEndpoint = Callable[..., Coroutine[Any, Any, Streamable]] -class RequestType(TypedDict): - pass - - -@dataclass_transform() -def get_streamable_from_request_type(cls: Type[RequestType]) -> Type[Streamable]: - return streamable( - dataclass(frozen=True)(type("_" + cls.__name__, (Streamable,), {"__annotations__": cls.__annotations__})) - ) - - -def marshall(func: MarshallableRpcEndpoint) -> RpcEndpoint: +def marshal(func: MarshallableRpcEndpoint) -> RpcEndpoint: hints = get_type_hints(func) - request_class: Type[RequestType] = hints["request"] + request_class: Type[Streamable] = hints["request"] async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: object) -> Dict[str, Any]: response_obj: Streamable = await func( self, - get_streamable_from_request_type(request_class).from_json_dict(request).__dict__, + request_class.from_json_dict(request), *args, **kwargs, ) diff --git a/tests/core/test_rpc_util.py b/tests/core/test_rpc_util.py index 97deb287287b..a4c0b429ccc0 100644 --- a/tests/core/test_rpc_util.py +++ b/tests/core/test_rpc_util.py @@ -5,7 +5,7 @@ import pytest -from chia.rpc.util import RequestType, marshall +from chia.rpc.util import marshal from chia.util.ints import uint32 from chia.util.streamable import Streamable, streamable @@ -16,7 +16,9 @@ class SubObject(Streamable): qux: str -class TestRequestType(RequestType): +@streamable +@dataclass(frozen=True) +class TestRequestType(Streamable): foofoo: str barbar: uint32 bat: bytes @@ -32,10 +34,10 @@ class TestResponseObject(Streamable): @pytest.mark.anyio async def test_rpc_marshalling() -> None: - @marshall + @marshal async def test_rpc_endpoint(self: None, request: TestRequestType) -> TestResponseObject: return TestResponseObject( - [request["foofoo"], str(request["barbar"]), request["bat"].hex(), request["bam"].qux], request["bam"] + [request.foofoo, str(request.barbar), request.bat.hex(), request.bam.qux], request.bam ) assert await test_rpc_endpoint( From 3c64a81b050f1015f2b05b245697bdf2d8a33b44 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 15 Dec 2023 13:48:10 -0800 Subject: [PATCH 044/274] Just go back to Streamable for requests --- chia/rpc/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 40f03a54d4dd..f7cbb661676f 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -2,7 +2,7 @@ import logging import traceback -from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Type, get_type_hints +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, get_type_hints import aiohttp @@ -18,8 +18,8 @@ # TODO: consolidate this with chia.rpc.rpc_server.Endpoint # Not all endpoints only take a dictionary so that definition is imperfect # This definition is weaker than that one however because the arguments can be anything -RpcEndpoint = Callable[..., Coroutine[Any, Any, Dict[str, Any]]] -MarshallableRpcEndpoint = Callable[..., Coroutine[Any, Any, Streamable]] +RpcEndpoint = Callable[..., Awaitable[Dict[str, Any]]] +MarshallableRpcEndpoint = Callable[..., Awaitable[Streamable]] def marshal(func: MarshallableRpcEndpoint) -> RpcEndpoint: From 1851b01ace443e260c9cc3092279cf8756fb8ea6 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 15 Dec 2023 14:39:22 -0800 Subject: [PATCH 045/274] Extract clvm streamable library to its own file and move signer_protocol.py --- chia/wallet/signer_protocol.py | 96 +++++++++++++ ...{signer_protocol.py => clvm_streamable.py} | 127 ++++-------------- tests/wallet/test_signer_protocol.py | 4 +- 3 files changed, 125 insertions(+), 102 deletions(-) create mode 100644 chia/wallet/signer_protocol.py rename chia/wallet/util/{signer_protocol.py => clvm_streamable.py} (55%) diff --git a/chia/wallet/signer_protocol.py b/chia/wallet/signer_protocol.py new file mode 100644 index 000000000000..3055df70f969 --- /dev/null +++ b/chia/wallet/signer_protocol.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from typing import List + +from chia.types.blockchain_format.coin import Coin as _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.util.ints import uint64 +from chia.wallet.util.clvm_streamable import ClvmStreamable + + +class Coin(ClvmStreamable): + parent_coin_id: bytes32 + puzzle_hash: bytes32 + amount: uint64 + + +class Spend(ClvmStreamable): + coin: Coin + puzzle: Program + solution: Program + + @classmethod + def from_coin_spend(cls, coin_spend: CoinSpend) -> Spend: + return cls( + Coin( + coin_spend.coin.parent_coin_info, + coin_spend.coin.puzzle_hash, + uint64(coin_spend.coin.amount), + ), + coin_spend.puzzle_reveal.to_program(), + coin_spend.solution.to_program(), + ) + + def as_coin_spend(self) -> CoinSpend: + return CoinSpend( + _Coin( + self.coin.parent_coin_id, + self.coin.puzzle_hash, + self.coin.amount, + ), + SerializedProgram.from_program(self.puzzle), + SerializedProgram.from_program(self.solution), + ) + + +class TransactionInfo(ClvmStreamable): + spends: List[Spend] + + +class SigningTarget(ClvmStreamable): + pubkey: bytes + message: bytes + hook: bytes32 + + +class SumHint(ClvmStreamable): + fingerprints: List[bytes] + synthetic_offset: bytes + + +class PathHint(ClvmStreamable): + root_fingerprint: bytes + path: List[uint64] + + +class KeyHints(ClvmStreamable): + sum_hints: List[SumHint] + path_hints: List[PathHint] + + +class SigningInstructions(ClvmStreamable): + key_hints: KeyHints + targets: List[SigningTarget] + + +class UnsignedTransaction(ClvmStreamable): + transaction_info: TransactionInfo + signing_instructions: SigningInstructions + + +class SigningResponse(ClvmStreamable): + signature: bytes + hook: bytes32 + + +class Signature(ClvmStreamable): + type: str + signature: bytes + + +class SignedTransaction(ClvmStreamable): + transaction_info: TransactionInfo + signatures: List[Signature] diff --git a/chia/wallet/util/signer_protocol.py b/chia/wallet/util/clvm_streamable.py similarity index 55% rename from chia/wallet/util/signer_protocol.py rename to chia/wallet/util/clvm_streamable.py index 58e2c048c420..489502aeae7e 100644 --- a/chia/wallet/util/signer_protocol.py +++ b/chia/wallet/util/clvm_streamable.py @@ -1,32 +1,46 @@ from __future__ import annotations +import contextvars +import threading from contextlib import contextmanager from dataclasses import dataclass, fields from io import BytesIO -from typing import Any, BinaryIO, Callable, Dict, Iterator, List, Type, TypeVar +from typing import Any, BinaryIO, Callable, Dict, Iterator, Type, TypeVar from hsms.clvm_serde import from_program_for_type, to_program_for_type from typing_extensions import dataclass_transform -from chia.types.blockchain_format.coin import Coin as _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.util.byte_types import hexstr_to_bytes -from chia.util.ints import uint64 from chia.util.streamable import ConversionError, Streamable, streamable -USE_CLVM_SERIALIZATION = False + +@dataclass +class ClvmSerializationConfig: + use: bool = False + + +class _ClvmSerializationMode: + config = contextvars.ContextVar("config", default=threading.local()) + + @classmethod + def get_config(cls) -> ClvmSerializationConfig: + return cls.config.get().config # type: ignore[no-any-return] + + @classmethod + def set_config(cls, config: ClvmSerializationConfig) -> None: + cls.config.get().config = config + + +_ClvmSerializationMode.set_config(ClvmSerializationConfig()) @contextmanager def clvm_serialization_mode(use: bool) -> Iterator[None]: - global USE_CLVM_SERIALIZATION - old_mode = USE_CLVM_SERIALIZATION - USE_CLVM_SERIALIZATION = use + old_config = _ClvmSerializationMode.get_config() + _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use)) yield - USE_CLVM_SERIALIZATION = old_mode + _ClvmSerializationMode.set_config(old_config) @dataclass_transform() @@ -59,8 +73,7 @@ def from_program(cls: Type[_T_ClvmStreamable], prog: Program) -> _T_ClvmStreamab raise NotImplementedError() # pragma: no cover def stream(self, f: BinaryIO) -> None: - global USE_CLVM_SERIALIZATION - if USE_CLVM_SERIALIZATION: + if _ClvmSerializationMode.get_config().use: f.write(bytes(self.as_program())) else: super().stream(f) @@ -76,8 +89,7 @@ def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: return super().parse(f) def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], Dict[str, Any]]) -> Any: - global USE_CLVM_SERIALIZATION - if USE_CLVM_SERIALIZATION: + if _ClvmSerializationMode.get_config().use: return bytes(self).hex() else: new_dict = {} @@ -99,88 +111,3 @@ def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStrea raise ConversionError(json_dict, cls, e) else: return super().from_json_dict(json_dict) - - -class Coin(ClvmStreamable): - parent_coin_id: bytes32 - puzzle_hash: bytes32 - amount: uint64 - - -class Spend(ClvmStreamable): - coin: Coin - puzzle: Program - solution: Program - - @classmethod - def from_coin_spend(cls, coin_spend: CoinSpend) -> Spend: - return cls( - Coin( - coin_spend.coin.parent_coin_info, - coin_spend.coin.puzzle_hash, - uint64(coin_spend.coin.amount), - ), - coin_spend.puzzle_reveal.to_program(), - coin_spend.solution.to_program(), - ) - - def as_coin_spend(self) -> CoinSpend: - return CoinSpend( - _Coin( - self.coin.parent_coin_id, - self.coin.puzzle_hash, - self.coin.amount, - ), - SerializedProgram.from_program(self.puzzle), - SerializedProgram.from_program(self.solution), - ) - - -class TransactionInfo(ClvmStreamable): - spends: List[Spend] - - -class SigningTarget(ClvmStreamable): - pubkey: bytes - message: bytes - hook: bytes32 - - -class SumHint(ClvmStreamable): - fingerprints: List[bytes] - synthetic_offset: bytes - - -class PathHint(ClvmStreamable): - root_fingerprint: bytes - path: List[uint64] - - -class KeyHints(ClvmStreamable): - sum_hints: List[SumHint] - path_hints: List[PathHint] - - -class SigningInstructions(ClvmStreamable): - key_hints: KeyHints - targets: List[SigningTarget] - - -class UnsignedTransaction(ClvmStreamable): - transaction_info: TransactionInfo - signing_instructions: SigningInstructions - - -class SigningResponse(ClvmStreamable): - signature: bytes - hook: bytes32 - - -class Signature(ClvmStreamable): - type: str - signature: bytes - - -class SignedTransaction(ClvmStreamable): - transaction_info: TransactionInfo - signatures: List[Signature] diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 00a2ecf1fb84..f52fbe905978 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -12,15 +12,15 @@ from chia.util.ints import uint64 from chia.util.streamable import ConversionError, Streamable, streamable from chia.wallet.conditions import AggSigMe -from chia.wallet.util.signer_protocol import ( +from chia.wallet.signer_protocol import ( KeyHints, SigningInstructions, SigningTarget, Spend, TransactionInfo, UnsignedTransaction, - clvm_serialization_mode, ) +from chia.wallet.util.clvm_streamable import clvm_serialization_mode def test_signing_lifecycle() -> None: From a98fd4dfb87c1e03363f7e352bbb5916a914b320 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Dec 2023 09:37:56 -0800 Subject: [PATCH 046/274] Add tests for safety of config --- chia/wallet/util/clvm_streamable.py | 9 ++++-- tests/wallet/test_signer_protocol.py | 44 +++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 489502aeae7e..19899cd1ca1d 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -23,6 +23,10 @@ class ClvmSerializationConfig: class _ClvmSerializationMode: config = contextvars.ContextVar("config", default=threading.local()) + @classmethod + def has_config(cls) -> bool: + return hasattr(cls.config.get(), "config") + @classmethod def get_config(cls) -> ClvmSerializationConfig: return cls.config.get().config # type: ignore[no-any-return] @@ -32,11 +36,10 @@ def set_config(cls, config: ClvmSerializationConfig) -> None: cls.config.get().config = config -_ClvmSerializationMode.set_config(ClvmSerializationConfig()) - - @contextmanager def clvm_serialization_mode(use: bool) -> Iterator[None]: + if not _ClvmSerializationMode.has_config(): + _ClvmSerializationMode.set_config(ClvmSerializationConfig()) old_config = _ClvmSerializationMode.get_config() _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use)) yield diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index f52fbe905978..037bca9a4a36 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -1,6 +1,9 @@ from __future__ import annotations +import asyncio import dataclasses +import threading +import time import pytest from chia_rs import G1Element @@ -20,7 +23,7 @@ TransactionInfo, UnsignedTransaction, ) -from chia.wallet.util.clvm_streamable import clvm_serialization_mode +from chia.wallet.util.clvm_streamable import ClvmSerializationConfig, _ClvmSerializationMode, clvm_serialization_mode def test_signing_lifecycle() -> None: @@ -123,3 +126,42 @@ class TempStreamable(Streamable): Spend.from_json_dict("blah") with pytest.raises(ConversionError): UnsignedTransaction.from_json_dict(streamable_blob.hex()) + + +def test_serialization_config_thread_safe() -> None: + def get_and_check_config(use: bool, wait_before: int, wait_after: int) -> None: + with clvm_serialization_mode(use): + time.sleep(wait_before) + assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig(use) + time.sleep(wait_after) + assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig() + + thread_1 = threading.Thread(target=get_and_check_config, args=(True, 0, 2)) + thread_2 = threading.Thread(target=get_and_check_config, args=(False, 1, 3)) + thread_3 = threading.Thread(target=get_and_check_config, args=(True, 2, 4)) + thread_4 = threading.Thread(target=get_and_check_config, args=(False, 3, 5)) + + thread_1.start() + thread_2.start() + thread_3.start() + thread_4.start() + + thread_1.join() + thread_2.join() + thread_3.join() + thread_4.join() + + +@pytest.mark.anyio +async def test_serialization_config_coroutine_safe() -> None: + async def get_and_check_config(use: bool, wait_before: int, wait_after: int) -> None: + with clvm_serialization_mode(use): + await asyncio.sleep(wait_before) + assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig(use) + await asyncio.sleep(wait_after) + assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig() + + await get_and_check_config(True, 0, 2) + await get_and_check_config(False, 1, 3) + await get_and_check_config(True, 2, 4) + await get_and_check_config(False, 3, 5) From a74c9f0f0558a47161f585b25fdb68cc37ebae0b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Dec 2023 10:04:09 -0800 Subject: [PATCH 047/274] Rework getting clvm streamable config --- chia/wallet/util/clvm_streamable.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 19899cd1ca1d..664a8e291e08 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -23,13 +23,9 @@ class ClvmSerializationConfig: class _ClvmSerializationMode: config = contextvars.ContextVar("config", default=threading.local()) - @classmethod - def has_config(cls) -> bool: - return hasattr(cls.config.get(), "config") - @classmethod def get_config(cls) -> ClvmSerializationConfig: - return cls.config.get().config # type: ignore[no-any-return] + return getattr(cls.config.get(), "config", ClvmSerializationConfig()) @classmethod def set_config(cls, config: ClvmSerializationConfig) -> None: @@ -38,8 +34,6 @@ def set_config(cls, config: ClvmSerializationConfig) -> None: @contextmanager def clvm_serialization_mode(use: bool) -> Iterator[None]: - if not _ClvmSerializationMode.has_config(): - _ClvmSerializationMode.set_config(ClvmSerializationConfig()) old_config = _ClvmSerializationMode.get_config() _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use)) yield From 26c0eda884e63467469c7585edc18588b6a662c9 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Dec 2023 10:35:34 -0800 Subject: [PATCH 048/274] fix imports --- chia/rpc/util.py | 2 +- chia/rpc/wallet_request_types.py | 2 +- chia/rpc/wallet_rpc_api.py | 2 +- chia/wallet/wallet.py | 8 ++++---- chia/wallet/wallet_state_manager.py | 22 +++++++++++----------- tests/wallet/rpc/test_wallet_rpc.py | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index e2b8f61b458e..a7358e93f6d9 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -17,7 +17,7 @@ from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.signer_protocol import clvm_serialization_mode +from chia.wallet.util.clvm_streamable import clvm_serialization_mode from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import TXConfig, TXConfigLoader diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 8c696bef7b07..50881274b098 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -5,7 +5,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.streamable import Streamable, streamable -from chia.wallet.util.signer_protocol import SignedTransaction, SigningInstructions, SigningResponse, Spend +from chia.wallet.signer_protocol import SignedTransaction, SigningInstructions, SigningResponse, Spend @streamable diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index b7a2bfc678d1..37860fe10011 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -95,6 +95,7 @@ from chia.wallet.puzzle_drivers import PuzzleInfo, Solver from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings, ClawbackMetadata from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key +from chia.wallet.signer_protocol import SigningResponse from chia.wallet.singleton import ( SINGLETON_LAUNCHER_PUZZLE_HASH, create_singleton_puzzle, @@ -108,7 +109,6 @@ 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.query_filter import HashFilter, TransactionTypeFilter -from chia.wallet.util.signer_protocol import SigningResponse from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index cb978684af69..33c413020b33 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -39,10 +39,7 @@ solution_for_conditions, ) from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition, 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.signer_protocol import ( +from chia.wallet.signer_protocol import ( PathHint, Signature, SignedTransaction, @@ -52,6 +49,9 @@ SumHint, TransactionInfo, ) +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 diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 27737e1e0c53..d37b3a93e288 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -111,17 +111,7 @@ 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.signer_protocol import ( +from chia.wallet.signer_protocol import ( KeyHints, PathHint, SignedTransaction, @@ -133,6 +123,16 @@ TransactionInfo, UnsignedTransaction, ) +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 ( diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index fa92f8336230..7810a7fb6191 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -49,6 +49,7 @@ from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.nft_wallet.nft_wallet import NFTWallet +from chia.wallet.signer_protocol import UnsignedTransaction from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey @@ -56,7 +57,6 @@ from chia.wallet.util.address_type import AddressType from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import AmountFilter, HashFilter, TransactionTypeFilter -from chia.wallet.util.signer_protocol import UnsignedTransaction from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.util.wallet_types import CoinType, WalletType From f891262608ecff72e1f8fe710328d8538f232ece Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 19 Dec 2023 13:58:16 +1300 Subject: [PATCH 049/274] quex's comments --- .../wallet/puzzles/deployed_puzzle_hashes.json | 3 +-- chia/wallet/puzzles/vault_p2_recovery.clsp | 2 -- chia/wallet/puzzles/vault_p2_recovery.clsp.hex | 2 +- chia/wallet/puzzles/vault_recovery_escape.clsp | 18 ------------------ .../puzzles/vault_recovery_escape.clsp.hex | 1 - tests/wallet/vault/test_vault_clsp.py | 6 ++---- tests/wallet/vault/test_vault_lifecycle.py | 4 ++-- 7 files changed, 6 insertions(+), 30 deletions(-) delete mode 100644 chia/wallet/puzzles/vault_recovery_escape.clsp delete mode 100644 chia/wallet/puzzles/vault_recovery_escape.clsp.hex diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 2ac58d2b57fc..9a9a80e4cad1 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -62,8 +62,7 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", - "vault_p2_recovery": "8f09bc0c13b0ee0f23f169ee4de7167dbec145b24aa4944746186bc8914ef40f", - "vault_recovery_escape": "0b2d3925e9a5097d95734f80f9205b4f6e37d69c317907cbc01a208f37fa9b40", + "vault_p2_recovery": "5bcfac8571e7464ab8852794b37b428ce23c55976d0a6b092227fd0a6b0e07c5", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp index eb92f5ceb8c8..31d5bf699c99 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp @@ -10,7 +10,6 @@ P2_SECP_PUZZLEHASH BLS_PK TIMELOCK - my_puzzlehash my_amount recovery_conditions ) @@ -61,7 +60,6 @@ my_amount ) (list ASSERT_MY_AMOUNT my_amount) - (list ASSERT_MY_PUZZLEHASH my_puzzlehash) ) diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex index 697c8adf571f..895752c239de 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff8202ffff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff8202ffff8080808080808080ffff04ff82017fffff0180808080ffff04ffff04ffff0149ffff04ff82017fffff01808080ffff04ffff04ffff0148ffff04ff8200bfffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 +ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff82017fff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ff8200bfffff0180808080ffff04ffff04ffff0149ffff04ff8200bfffff01808080ffff0180808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 diff --git a/chia/wallet/puzzles/vault_recovery_escape.clsp b/chia/wallet/puzzles/vault_recovery_escape.clsp deleted file mode 100644 index 4ea7d4b6d32e..000000000000 --- a/chia/wallet/puzzles/vault_recovery_escape.clsp +++ /dev/null @@ -1,18 +0,0 @@ -; Escape spend for a vault recovery -; This is to cancel a recovery spend (before the timelocked p2_conditions path is spent) -; BLS_PK is curried into vault_recovery -; P2_PUZZLEHASH is the puzzlehash for the vault root -; AMOUNT is the full amount of the coin - -(mod - (BLS_PK P2_PUZZLEHASH AMOUNT) - - (include *standard-cl-21*) - (include condition_codes.clib) - - (list - (list AGG_SIG_ME BLS_PK (sha256 P2_PUZZLEHASH AMOUNT)) - (list CREATE_COIN P2_PUZZLEHASH AMOUNT) - (list ASSERT_MY_AMOUNT AMOUNT) - ) -) diff --git a/chia/wallet/puzzles/vault_recovery_escape.clsp.hex b/chia/wallet/puzzles/vault_recovery_escape.clsp.hex deleted file mode 100644 index 57ea9a84be15..000000000000 --- a/chia/wallet/puzzles/vault_recovery_escape.clsp.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff01ff04ffff04ffff0132ffff04ff05ffff04ffff0bff0bff1780ffff0180808080ffff04ffff04ffff0133ffff04ff0bffff04ff17ffff0180808080ffff04ffff04ffff0149ffff04ff17ffff01808080ffff0180808080ffff04ff80ff018080 diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 0ae3b95f9a0f..474e5404d569 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -38,7 +38,6 @@ def test_recovery_puzzles() -> None: secp_pk = secp_sk.verifying_key.to_string("compressed") p2_puzzlehash = ACS_PH - vault_puzzlehash = Program.to("vault_puzzlehash").get_tree_hash() amount = 10000 timelock = 5000 coin_id = Program.to("coin_id").get_tree_hash() @@ -53,7 +52,7 @@ def test_recovery_puzzles() -> None: P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, escape_puzzlehash, bls_pk, timelock ) - recovery_solution = Program.to([vault_puzzlehash, amount, recovery_conditions]) + recovery_solution = Program.to([amount, recovery_conditions]) conds = conditions_dict_for_solution(curried_recovery_puzzle, recovery_solution, INFINITE_COST) @@ -145,7 +144,6 @@ def test_vault_root_puzzle() -> None: vault_merkle_tree = MerkleTree([secp_puzzlehash, recovery_puzzlehash]) vault_merkle_root = vault_merkle_tree.calculate_root() vault_puzzle = P2_1_OF_N_MOD.curry(vault_merkle_root) - vault_puzzlehash = vault_puzzle.get_tree_hash() # secp spend path delegated_puzzle = ACS @@ -168,7 +166,7 @@ def test_vault_root_puzzle() -> None: recovery_merkle_root = recovery_merkle_tree.calculate_root() recovery_merkle_puzzle = P2_1_OF_N_MOD.curry(recovery_merkle_root) recovery_merkle_puzzlehash = recovery_merkle_puzzle.get_tree_hash() - recovery_solution = Program.to([vault_puzzlehash, amount, recovery_conditions]) + recovery_solution = Program.to([amount, recovery_conditions]) proof = vault_merkle_tree.generate_proof(recovery_puzzlehash) recovery_proof = Program.to((proof[0], proof[1][0])) diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index c8256fcddc53..3ee806df7b08 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -42,7 +42,7 @@ @pytest.mark.anyio async def test_vault_inner(cost_logger: CostLogger) -> None: async with sim_and_client() as (sim, client): - sim.pass_blocks(DEFAULT_CONSTANTS.SOFT_FORK3_HEIGHT) # Make sure secp_verify is available + sim.pass_blocks(DEFAULT_CONSTANTS.SOFT_FORK2_HEIGHT) # Make sure secp_verify is available # Setup puzzles secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE) @@ -93,7 +93,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: ].coin recovery_conditions = Program.to([[51, ACS_PH, vault_coin.amount]]) - recovery_solution = Program.to([vault_puzzlehash, vault_coin.amount, recovery_conditions]) + recovery_solution = Program.to([vault_coin.amount, recovery_conditions]) recovery_proof = get_vault_proof(vault_merkle_tree, p2_recovery_puzzlehash) vault_solution_recovery = Program.to([recovery_proof, p2_recovery_puzzle, recovery_solution]) vault_spendbundle = SpendBundle( From ed95bdcac1fc170d1153ff45136991eef5af62ee Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 19 Dec 2023 14:11:48 +1300 Subject: [PATCH 050/274] add entropy to secp sig --- chia/wallet/puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/p2_delegated_secp.clsp | 4 ++-- chia/wallet/puzzles/p2_delegated_secp.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 10 ++++++---- tests/wallet/vault/test_vault_clsp.py | 16 +++++++++------- tests/wallet/vault/test_vault_lifecycle.py | 11 ++++++++--- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 9a9a80e4cad1..3dd563612793 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -45,7 +45,7 @@ "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", - "p2_delegated_secp": "282533d811a01d8cc2fcf3e7d93142c376c62b569ac6bdee4fea65d172d716e7", + "p2_delegated_secp": "a62156ff2e5672b0bdab35de52d0db14f8ad80c61d4bee26f110e5a629fe6d1a", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", "p2_parent": "b10ce2d0b18dcf8c21ddfaf55d9b9f0adcbf1e0beb55b1a8b9cad9bbff4e5f22", "p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363", diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp b/chia/wallet/puzzles/p2_delegated_secp.clsp index b0ae2f8512b9..f9ae9a13a0fa 100644 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp @@ -1,12 +1,12 @@ ; p2_delegated with SECP256-R1 signature ; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) -(mod (GENESIS_CHALLENGE SECP_PK delegated_puzzle delegated_solution signature coin_id) +(mod (GENESIS_CHALLENGE SECP_PK ENTROPY delegated_puzzle delegated_solution signature coin_id) (include *standard-cl-21*) (include condition_codes.clib) (include sha256tree.clib) - (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE) signature) + (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE ENTROPY) signature) ; secp256r1_verify returns 0 if successful. (x) ; this doesn't actually run because secp256_verify will raise on failure (c diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex index 03da697cdf99..4b32f0b29093 100644 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex +++ b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ffff841c3a8f00ff0bffff0bffff02ff02ffff04ff02ffff04ff17ff80808080ff8200bfff0580ff5f80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ff8200bfffff01808080ffff02ff17ff2f8080ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 +ff02ffff01ff02ffff03ffff841c3a8f00ff0bffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ff82017fff05ff1780ff8200bf80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ff82017fffff01808080ffff02ff2fff5f8080ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 64885825fe86..106b2ae4c975 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -20,8 +20,8 @@ # PUZZLES -def construct_p2_delegated_secp(secp_pk: bytes, genesis_challenge: bytes32) -> Program: - return P2_DELEGATED_SECP_MOD.curry(genesis_challenge, secp_pk) +def construct_p2_delegated_secp(secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes) -> Program: + return P2_DELEGATED_SECP_MOD.curry(genesis_challenge, secp_pk, entropy) def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> Program: @@ -48,5 +48,7 @@ def get_vault_proof(merkle_tree: MerkleTree, puzzlehash: bytes32) -> Program: # SECP SIGNATURE -def construct_secp_message(delegated_puzzlehash: bytes32, coin_id: bytes32, genesis_challenge: bytes32) -> bytes: - return delegated_puzzlehash + coin_id + genesis_challenge +def construct_secp_message( + delegated_puzzlehash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, entropy: bytes +) -> bytes: + return delegated_puzzlehash + coin_id + genesis_challenge + entropy diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 474e5404d569..2fe7ba798d29 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -6,6 +6,7 @@ import pytest from blspy import PrivateKey from chia_rs import ENABLE_SECP_OPS +from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey from chia.consensus.default_constants import DEFAULT_CONSTANTS @@ -25,6 +26,7 @@ RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() ACS = Program.to(1) ACS_PH = ACS.get_tree_hash() +ENTROPY = int_to_bytes(101) def run_with_secp(puzzle: Program, solution: Program) -> Tuple[int, Program]: @@ -43,7 +45,7 @@ def test_recovery_puzzles() -> None: coin_id = Program.to("coin_id").get_tree_hash() recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) - escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) + escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) escape_puzzlehash = escape_puzzle.get_tree_hash() finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) finish_puzzlehash = finish_puzzle.get_tree_hash() @@ -80,7 +82,7 @@ def test_recovery_puzzles() -> None: delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ENTROPY ) secp_solution = Program.to( [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] @@ -93,13 +95,13 @@ def test_recovery_puzzles() -> None: def test_p2_delegated_secp() -> None: secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") - secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) coin_id = Program.to("coin_id").get_tree_hash() delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, 1000]]) signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ENTROPY ) secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) @@ -124,7 +126,7 @@ def test_vault_root_puzzle() -> None: # secp puzzle secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") - secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) secp_puzzlehash = secp_puzzle.get_tree_hash() # recovery keys @@ -149,7 +151,7 @@ def test_vault_root_puzzle() -> None: delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ENTROPY ) secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) @@ -160,7 +162,7 @@ def test_vault_root_puzzle() -> None: # recovery spend path recovery_conditions = Program.to([[51, ACS_PH, amount]]) - curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk) + curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) recovery_merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) recovery_merkle_root = recovery_merkle_tree.calculate_root() diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index 3ee806df7b08..fc118b13f2dd 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -5,6 +5,7 @@ import pytest from chia_rs import AugSchemeMPL, G2Element, PrivateKey +from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey from chia.clvm.spend_sim import CostLogger, sim_and_client @@ -37,6 +38,7 @@ TIMELOCK = uint64(1000) ACS = Program.to(0) ACS_PH = ACS.get_tree_hash() +ENTROPY = int_to_bytes(101) @pytest.mark.anyio @@ -45,7 +47,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: sim.pass_blocks(DEFAULT_CONSTANTS.SOFT_FORK2_HEIGHT) # Make sure secp_verify is available # Setup puzzles - secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE) + secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, ENTROPY) secp_puzzlehash = secp_puzzle.get_tree_hash() p2_recovery_puzzle = construct_p2_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) p2_recovery_puzzlehash = p2_recovery_puzzle.get_tree_hash() @@ -66,7 +68,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) secp_signature = SECP_SK.sign_deterministic( construct_secp_message( - secp_delegated_puzzle.get_tree_hash(), vault_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE + secp_delegated_puzzle.get_tree_hash(), vault_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE, ENTROPY ) ) @@ -151,7 +153,10 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) secp_signature = SECP_SK.sign_deterministic( construct_secp_message( - secp_delegated_puzzle.get_tree_hash(), recovery_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE + secp_delegated_puzzle.get_tree_hash(), + recovery_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + ENTROPY, ) ) secp_solution = Program.to( From ce68705004bb529b4244ad816cfb1d50dff22861 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 19 Dec 2023 08:06:45 -0800 Subject: [PATCH 051/274] Bring add_public_key into agreement --- chia/cmds/keys_funcs.py | 2 +- chia/daemon/keychain_proxy.py | 14 ++++++++------ chia/daemon/keychain_server.py | 4 ++-- chia/util/keychain.py | 20 ++++++++++++++++---- tests/wallet/test_wallet_node.py | 11 +++++++---- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 541453a4dd91..cbc5b6c5ed2a 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -83,7 +83,7 @@ def add_key_info(mnemonic_or_pk: str, label: Optional[str]) -> None: fingerprint = sk.get_g1().get_fingerprint() print(f"Added private key with public key fingerprint {fingerprint}") else: - pk = Keychain().add_public_key(mnemonic_or_pk, label) + pk, _ = Keychain().add_public_key(mnemonic_or_pk, label) fingerprint = pk.get_fingerprint() print(f"Added public key with fingerprint {fingerprint}") diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 6b58ba09c0b3..b8fbf8a69a4e 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -30,7 +30,8 @@ KeychainMalformedResponse, KeychainProxyConnectionTimeout, ) -from chia.util.keychain import Keychain, KeyData, bytes_to_mnemonic, mnemonic_to_seed +from chia.util.keychain import Keychain, KeyData, KeyTypes, bytes_to_mnemonic, mnemonic_to_seed +from chia.util.observation_root import ObservationRoot from chia.util.ws_message import WsRpcMessage @@ -195,19 +196,20 @@ async def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> P return key - async def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: + async def add_public_key(self, pubkey: str, label: Optional[str] = None) -> Tuple[ObservationRoot, KeyTypes]: """ Forwards to Keychain.add_public_key() """ - key: G1Element + key: ObservationRoot + key_type: KeyTypes if self.use_local_keychain(): - key = self.keychain.add_public_key(pubkey, label) # pragma: no cover + key, key_type = self.keychain.add_public_key(pubkey, label) # pragma: no cover else: response, success = await self.get_response_for_request( "add_public_key", {"public_key": pubkey, "label": label} ) if success: - key = G1Element.from_bytes(hexstr_to_bytes(pubkey)) + key = KeyTypes.parse_observation_root(hexstr_to_bytes(pubkey), KeyTypes(response["data"]["key_type"])) else: error = response["data"].get("error", None) if error == KEYCHAIN_ERR_KEYERROR: # pragma: no cover @@ -217,7 +219,7 @@ async def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1El else: self.handle_error(response) - return key + return key, key_type async def check_keys(self, root_path: Path) -> None: """ diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index b20f8b3c65dd..2544673ee1b1 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -255,7 +255,7 @@ async def add_public_key(self, request: Dict[str, Any]) -> Dict[str, Any]: } try: - pk = self.get_keychain_for_request(request).add_public_key(public_key, label) + pk, key_type = self.get_keychain_for_request(request).add_public_key(public_key, label) except KeyError as e: # pragma: no cover return { "success": False, @@ -269,7 +269,7 @@ async def add_public_key(self, request: Dict[str, Any]) -> Dict[str, Any]: "error": str(e), } - return {"success": True, "fingerprint": pk.get_fingerprint()} + return {"success": True, "fingerprint": pk.get_fingerprint(), "key_type": key_type} async def check_keys(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.get_keychain_for_request(request).is_keyring_locked(): diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 19cb711543a1..9a39beafa9d1 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -7,7 +7,7 @@ from functools import cached_property from hashlib import pbkdf2_hmac from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Type import pkg_resources from bitstring import BitArray # pyright: reportMissingImports=false @@ -220,6 +220,13 @@ def mnemonic_str(self) -> str: class KeyTypes(str, Enum): G1_ELEMENT = "G1 Element" + @classmethod + def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: + if key_type == cls.G1_ELEMENT: + return G1Element.from_bytes(pk_bytes) + else: + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") + @final @streamable @@ -383,11 +390,16 @@ def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> Private return key - def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: + def add_public_key(self, pubkey: str, label: Optional[str] = None) -> Tuple[ObservationRoot, KeyTypes]: """ Adds a public key to the keychain. """ - key = G1Element.from_bytes(hexstr_to_bytes(pubkey)) + pk_bytes = hexstr_to_bytes(pubkey) + if len(pk_bytes) == 48: + key: ObservationRoot = G1Element.from_bytes(pk_bytes) + key_type: KeyTypes = KeyTypes.G1_ELEMENT + else: + raise ValueError(f"Cannot identify type of pubkey {pubkey}") index = self._get_free_private_key_index() fingerprint = key.get_fingerprint() @@ -411,7 +423,7 @@ def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: self.keyring_wrapper.delete_label(fingerprint) raise - return key + return key, key_type def set_label(self, fingerprint: int, label: str) -> None: """ diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index 046999db41a9..0b9587fdd781 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -24,7 +24,7 @@ from chia.util.config import load_config from chia.util.errors import Err from chia.util.ints import uint8, uint32, uint64, uint128 -from chia.util.keychain import Keychain, KeyData, generate_mnemonic +from chia.util.keychain import Keychain, KeyData, KeyTypes, generate_mnemonic from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.wallet_node import Balance, WalletNode from tests.conftest import ConsensusMode @@ -114,7 +114,7 @@ async def test_get_public_key(root_path_populated_with_config: Path, get_temp_ke keychain: Keychain = get_temp_keyring config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk: G1Element = keychain.add_public_key( + pk, key_type = keychain.add_public_key( "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) fingerprint: int = pk.get_fingerprint() @@ -124,6 +124,7 @@ async def test_get_public_key(root_path_populated_with_config: Path, get_temp_ke assert key is not None assert isinstance(key, G1Element) assert key.get_fingerprint() == fingerprint + assert key_type == KeyTypes.G1_ELEMENT @pytest.mark.anyio @@ -132,7 +133,7 @@ async def test_get_public_key_default_key(root_path_populated_with_config: Path, keychain: Keychain = get_temp_keyring config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk: G1Element = keychain.add_public_key( + pk, key_type = keychain.add_public_key( "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) fingerprint: int = pk.get_fingerprint() @@ -151,6 +152,7 @@ async def test_get_public_key_default_key(root_path_populated_with_config: Path, assert key is not None assert isinstance(key, G1Element) assert key.get_fingerprint() == fingerprint + assert key_type == KeyTypes.G1_ELEMENT @pytest.mark.anyio @@ -177,7 +179,7 @@ async def test_get_public_key_missing_key_use_default( keychain: Keychain = get_temp_keyring config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk: G1Element = keychain.add_public_key( + pk, key_type = keychain.add_public_key( "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) fingerprint: int = pk.get_fingerprint() @@ -191,6 +193,7 @@ async def test_get_public_key_missing_key_use_default( assert key is not None assert isinstance(key, G1Element) assert key.get_fingerprint() == fingerprint + assert key_type == KeyTypes.G1_ELEMENT def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: From 1acd0116b5e67cc30d7c790109d58bacb5c18779 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 20 Dec 2023 06:39:17 +1300 Subject: [PATCH 052/274] Endpoint for vault singleton creation --- chia/rpc/wallet_rpc_api.py | 88 +++++++++++++++++++++++++ chia/rpc/wallet_rpc_client.py | 24 +++++++ chia/wallet/vault/vault_drivers.py | 36 +++++++--- tests/wallet/vault/test_vault_wallet.py | 50 ++++++++++++++ 4 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 tests/wallet/vault/test_vault_wallet.py diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index e4e5927db9a3..7200cd3107a7 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -3,6 +3,7 @@ import dataclasses import json import logging +import time import zlib from pathlib import Path from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Union @@ -11,6 +12,7 @@ from clvm_tools.binutils import assemble from chia.consensus.block_rewards import calculate_base_farmer_reward +from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.data_layer.data_layer_errors import LauncherCoinNotFoundError from chia.data_layer.data_layer_wallet import DataLayerWallet from chia.pools.pool_wallet import PoolWallet @@ -49,6 +51,7 @@ AssertCoinAnnouncement, AssertPuzzleAnnouncement, Condition, + ConditionValidTimes, CreateCoinAnnouncement, CreatePuzzleAnnouncement, ) @@ -88,8 +91,10 @@ from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings, ClawbackMetadata from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key from chia.wallet.singleton import ( + SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, create_singleton_puzzle, + create_singleton_puzzle_hash, get_inner_puzzle_from_singleton, ) from chia.wallet.trade_record import TradeRecord @@ -104,6 +109,7 @@ from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import CoinType, WalletType +from chia.wallet.vault.vault_drivers import get_vault_puzzle_hash from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_store import VCProofs @@ -281,6 +287,8 @@ def get_routes(self) -> Dict[str, Endpoint]: "/vc_revoke": self.vc_revoke, # CR-CATs "/crcat_approve_pending": self.crcat_approve_pending, + # VAULT + "/vault_create": self.vault_create, } def get_connections(self, request_node_type: Optional[NodeType]) -> List[Dict[str, Any]]: @@ -4496,3 +4504,83 @@ class CRCATApprovePending(Streamable): return { "transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs], } + + ########################################################################################## + # VAULT + ########################################################################################## + @tx_endpoint(push=True) + async def vault_create( + self, + request: Dict[str, Any], + tx_config: TXConfig = DEFAULT_TX_CONFIG, + extra_conditions: Tuple[Condition, ...] = tuple(), + ) -> EndpointResult: + """ + Create a new vault + """ + assert self.service.wallet_state_manager + wallet = self.service.wallet_state_manager.main_wallet + + secp_pk = bytes.fromhex(str(request.get("secp_pk"))) + entropy = bytes.fromhex(str(request.get("entropy"))) + bls_pk = G1Element.from_bytes(bytes.fromhex(str(request.get("bls_pk")))) + timelock = uint64(request["timelock"]) + fee = request.get("fee", 0) + vault_puzzlehash = get_vault_puzzle_hash( + secp_pk, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, entropy, bls_pk, timelock + ) + + # Get xch coin + amount = uint64(1) + coins = await wallet.select_coins(uint64(amount + fee), tx_config.coin_selection_config) + + # Create singleton launcher + origin = coins.copy().pop() + launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_PUZZLE_HASH, amount) + + vault_full_puzzlehash = create_singleton_puzzle_hash(vault_puzzlehash, launcher_coin.name()) + announcement_message = Program.to([vault_puzzlehash, amount, bytes(0x80)]).get_tree_hash() + + [tx_record] = await wallet.generate_signed_transaction( + amount, + SINGLETON_LAUNCHER_PUZZLE_HASH, + tx_config, + fee, + coins, + None, + origin_id=origin.name(), + extra_conditions=( + AssertCoinAnnouncement(asserted_id=launcher_coin.name(), asserted_msg=announcement_message), + ), + ) + + genesis_launcher_solution = Program.to([vault_puzzlehash, amount, bytes(0x80)]) + + launcher_cs = CoinSpend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution) + launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) + assert tx_record.spend_bundle is not None + full_spend = SpendBundle.aggregate([tx_record.spend_bundle, launcher_sb]) + + vault_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + amount=uint64(amount), + to_puzzle_hash=vault_full_puzzlehash, + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=full_spend, + additions=full_spend.additions(), + removals=full_spend.removals(), + wallet_id=wallet.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=full_spend.name(), + memos=[], + valid_times=ConditionValidTimes(), + ) + + return { + "transactions": [vault_record.to_json_dict_convenience(self.service.config)], + } diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 940468e5d3a0..5c576b034c94 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1681,3 +1681,27 @@ async def crcat_approve_pending( }, ) return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + + async def vault_create( + self, + secp_pk: bytes, + entropy: bytes, + bls_pk: bytes, + timelock: uint64, + tx_config: TXConfig, + fee: uint64 = uint64(0), + push: bool = True, + ) -> List[TransactionRecord]: + response = await self.fetch( + "vault_create", + { + "secp_pk": secp_pk.hex(), + "entropy": entropy.hex(), + "bls_pk": bls_pk.hex(), + "timelock": timelock, + "fee": fee, + "push": push, + **tx_config.to_json_dict(), + }, + ) + return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 106b2ae4c975..dab44ec342f4 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -28,27 +28,43 @@ def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) -def construct_p2_recovery_puzzle(secp_puzzlehash: bytes32, bls_pk: G1Element, timelock: uint64) -> Program: - return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock) +def construct_p2_recovery_puzzle(secp_puzzle_hash: bytes32, bls_pk: G1Element, timelock: uint64) -> Program: + return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzle_hash, bls_pk, timelock) -def construct_vault_puzzle(secp_puzzlehash: bytes32, recovery_puzzlehash: bytes32) -> Program: - return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzlehash, recovery_puzzlehash]).calculate_root()) +def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> Program: + return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root()) + + +def get_vault_puzzle( + secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64 +) -> Program: + secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, entropy).get_tree_hash() + recovery_puzzle_hash = construct_p2_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() + return construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) + + +def get_vault_puzzle_hash( + secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64 +) -> bytes32: + vault_puzzle = get_vault_puzzle(secp_pk, genesis_challenge, entropy, bls_pk, timelock) + vault_puzzle_hash: bytes32 = vault_puzzle.get_tree_hash() + return vault_puzzle_hash # MERKLE -def construct_vault_merkle_tree(secp_puzzlehash: bytes32, recovery_puzzlehash: bytes32) -> MerkleTree: - return MerkleTree([secp_puzzlehash, recovery_puzzlehash]) +def construct_vault_merkle_tree(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> MerkleTree: + return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]) -def get_vault_proof(merkle_tree: MerkleTree, puzzlehash: bytes32) -> Program: - proof = merkle_tree.generate_proof(puzzlehash) +def get_vault_proof(merkle_tree: MerkleTree, puzzle_hash: bytes32) -> Program: + proof = merkle_tree.generate_proof(puzzle_hash) vault_proof: Program = Program.to((proof[0], proof[1][0])) return vault_proof # SECP SIGNATURE def construct_secp_message( - delegated_puzzlehash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, entropy: bytes + delegated_puzzle_hash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, entropy: bytes ) -> bytes: - return delegated_puzzlehash + coin_id + genesis_challenge + entropy + return delegated_puzzle_hash + coin_id + genesis_challenge + entropy diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py new file mode 100644 index 000000000000..f366514b3523 --- /dev/null +++ b/tests/wallet/vault/test_vault_wallet.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from hashlib import sha256 + +import pytest +from clvm.casts import int_to_bytes +from ecdsa import NIST256p, SigningKey + +from chia.util.ints import uint64 +from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG +from tests.conftest import ConsensusMode +from tests.wallet.conftest import WalletTestFramework + +SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) +SECP_PK = SECP_SK.verifying_key.to_string("compressed") + + +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + } + ], + indirect=True, +) +@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") +@pytest.mark.anyio +async def test_vault_creation(wallet_environments: WalletTestFramework) -> None: + # Setup + # full_node_api: FullNodeSimulator = wallet_environments.full_node + env = wallet_environments.environments[0] + # wallet_node = env.wallet_node + # wallet = env.xch_wallet + client = env.rpc_client + + fingerprint = (await client.get_public_keys())[0] + bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] + bls_pk = bytes.fromhex(bls_pk_hex) + + timelock = uint64(1000) + entropy = int_to_bytes(101) + + res = await client.vault_create(SECP_PK, entropy, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG, fee=uint64(10)) + vault_tx = res[0] + assert vault_tx + + eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] + assert eve_coin From 4aa4ee5f86ca2b8e39461e1940f7e51a9c1f9624 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 19 Dec 2023 10:06:56 -0800 Subject: [PATCH 053/274] Fix keychain_proxy test --- chia/daemon/keychain_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index b8fbf8a69a4e..b2f3a1c078d3 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -209,7 +209,8 @@ async def add_public_key(self, pubkey: str, label: Optional[str] = None) -> Tupl "add_public_key", {"public_key": pubkey, "label": label} ) if success: - key = KeyTypes.parse_observation_root(hexstr_to_bytes(pubkey), KeyTypes(response["data"]["key_type"])) + key_type = KeyTypes(response["data"]["key_type"]) + key = KeyTypes.parse_observation_root(hexstr_to_bytes(pubkey), key_type) else: error = response["data"].get("error", None) if error == KEYCHAIN_ERR_KEYERROR: # pragma: no cover From 29f99d686a03214364d5185eec97c5d03e962f24 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 19 Dec 2023 14:36:57 -0800 Subject: [PATCH 054/274] Fix import --- chia/wallet/wallet_protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 30f907e4ca8b..6a8db45537a7 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -16,8 +16,7 @@ from chia.wallet.nft_wallet.nft_info import NFTCoinInfo from chia.wallet.payment import Payment from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata -from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.signer_protocol import ( +from chia.wallet.signer_protocol import ( PathHint, SignedTransaction, SigningInstructions, @@ -25,6 +24,7 @@ Spend, SumHint, ) +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_coin_record import WalletCoinRecord From 1687798b78dff492f2d42f9eea431f6d5d4953cf Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 21 Dec 2023 13:07:40 +1300 Subject: [PATCH 055/274] add hidden puzzle to p2_delegated_secp --- .../puzzles/deployed_puzzle_hashes.json | 2 +- .../puzzles/p2_delegated_or_hidden_secp.clsp | 19 ++++++++ .../p2_delegated_or_hidden_secp.clsp.hex | 1 + chia/wallet/puzzles/p2_delegated_secp.clsp | 18 ------- .../wallet/puzzles/p2_delegated_secp.clsp.hex | 1 - chia/wallet/vault/vault_drivers.py | 2 +- .../vault/p2_delegated_or_hidden_secp.clsp | 19 ++++++++ tests/wallet/vault/test_vault_clsp.py | 48 +++++++++++++------ 8 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp create mode 100644 chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex delete mode 100644 chia/wallet/puzzles/p2_delegated_secp.clsp delete mode 100644 chia/wallet/puzzles/p2_delegated_secp.clsp.hex create mode 100644 tests/wallet/vault/p2_delegated_or_hidden_secp.clsp diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 3dd563612793..04639e84d5fa 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -43,9 +43,9 @@ "p2_announced_delegated_puzzle": "c4d24c3c5349376f3e8f3aba202972091713b4ec4915f0f26192ae4ace0bd04d", "p2_conditions": "1c77d7d5efde60a7a1d2d27db6d746bc8e568aea1ef8586ca967a0d60b83cc36", "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", + "p2_delegated_or_hidden_secp": "fdc746afe69a97548e0ff298209bb000e4a649ab3a2a3e3db18c97cf01e0d8b5", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", - "p2_delegated_secp": "a62156ff2e5672b0bdab35de52d0db14f8ad80c61d4bee26f110e5a629fe6d1a", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", "p2_parent": "b10ce2d0b18dcf8c21ddfaf55d9b9f0adcbf1e0beb55b1a8b9cad9bbff4e5f22", "p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363", diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp new file mode 100644 index 000000000000..ed52941a50bd --- /dev/null +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp @@ -0,0 +1,19 @@ +; p2_delegated with SECP256-R1 signature +; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) + +(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id) + (include *standard-cl-21*) + (include condition_codes.clib) + (include sha256tree.clib) + + (if (= (sha256tree delegated_puzzle) HIDDEN_PUZZLE_HASH) + (a delegated_puzzle delegated_solution) + (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE HIDDEN_PUZZLE_HASH) signature) + (x) ; this doesn't actually run because secp256_verify will raise on failure + (c + (list ASSERT_MY_COIN_ID coin_id) + (a delegated_puzzle delegated_solution) + ) + ) + ) +) diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex new file mode 100644 index 000000000000..ad76d33d0b38 --- /dev/null +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ffff09ffff02ff02ffff04ff02ffff04ff2fff80808080ff1780ffff01ff02ffff01ff02ff2fff5f80ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ff0bffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ff82017fff05ff1780ff8200bf80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ff82017fffff01808080ffff02ff2fff5f8080ff018080ff0180ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp b/chia/wallet/puzzles/p2_delegated_secp.clsp deleted file mode 100644 index f9ae9a13a0fa..000000000000 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp +++ /dev/null @@ -1,18 +0,0 @@ -; p2_delegated with SECP256-R1 signature -; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) - -(mod (GENESIS_CHALLENGE SECP_PK ENTROPY delegated_puzzle delegated_solution signature coin_id) - (include *standard-cl-21*) - (include condition_codes.clib) - (include sha256tree.clib) - - (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE ENTROPY) signature) - ; secp256r1_verify returns 0 if successful. - (x) ; this doesn't actually run because secp256_verify will raise on failure - (c - (list ASSERT_MY_COIN_ID coin_id) - (a delegated_puzzle delegated_solution) - ) - ) - -) diff --git a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_secp.clsp.hex deleted file mode 100644 index 4b32f0b29093..000000000000 --- a/chia/wallet/puzzles/p2_delegated_secp.clsp.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff01ff02ffff03ffff841c3a8f00ff0bffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ff82017fff05ff1780ff8200bf80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ff82017fffff01808080ffff02ff2fff5f8080ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 106b2ae4c975..cca8519544a1 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -10,7 +10,7 @@ # MODS P2_CONDITIONS_MOD: Program = load_clvm("p2_conditions.clsp") -P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_secp.clsp") +P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_or_hidden_secp.clsp") P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() P2_RECOVERY_MOD: Program = load_clvm("vault_p2_recovery.clsp") diff --git a/tests/wallet/vault/p2_delegated_or_hidden_secp.clsp b/tests/wallet/vault/p2_delegated_or_hidden_secp.clsp new file mode 100644 index 000000000000..ed52941a50bd --- /dev/null +++ b/tests/wallet/vault/p2_delegated_or_hidden_secp.clsp @@ -0,0 +1,19 @@ +; p2_delegated with SECP256-R1 signature +; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) + +(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id) + (include *standard-cl-21*) + (include condition_codes.clib) + (include sha256tree.clib) + + (if (= (sha256tree delegated_puzzle) HIDDEN_PUZZLE_HASH) + (a delegated_puzzle delegated_solution) + (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE HIDDEN_PUZZLE_HASH) signature) + (x) ; this doesn't actually run because secp256_verify will raise on failure + (c + (list ASSERT_MY_COIN_ID coin_id) + (a delegated_puzzle delegated_solution) + ) + ) + ) +) diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 2fe7ba798d29..b2de34d1c8bb 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -6,33 +6,47 @@ import pytest from blspy import PrivateKey from chia_rs import ENABLE_SECP_OPS -from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.program import INFINITE_COST, Program +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.condition_opcodes import ConditionOpcode from chia.util.condition_tools import conditions_dict_for_solution from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE_HASH from chia.wallet.util.merkle_tree import MerkleTree from tests.clvm.test_puzzles import secret_exponent_for_index -P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_secp.clsp") +P2_DELEGATED_SECP_MOD: Program = load_clvm("p2_delegated_or_hidden_secp.clsp") P2_1_OF_N_MOD: Program = load_clvm("p2_1_of_n.clsp") -P2_1_OF_N_MOD_HASH = P2_1_OF_N_MOD.get_tree_hash() +P2_1_OF_N_MOD_HASH: bytes32 = P2_1_OF_N_MOD.get_tree_hash() P2_RECOVERY_MOD: Program = load_clvm("vault_p2_recovery.clsp") -P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() +P2_RECOVERY_MOD_HASH: bytes32 = P2_RECOVERY_MOD.get_tree_hash() RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") -RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() -ACS = Program.to(1) -ACS_PH = ACS.get_tree_hash() -ENTROPY = int_to_bytes(101) +RECOVERY_FINISH_MOD_HASH: bytes32 = RECOVERY_FINISH_MOD.get_tree_hash() +ACS: Program = Program.to(1) +ACS_PH: bytes32 = ACS.get_tree_hash() def run_with_secp(puzzle: Program, solution: Program) -> Tuple[int, Program]: return puzzle._run(INFINITE_COST, ENABLE_SECP_OPS, solution) +def test_secp_hidden() -> None: + HIDDEN_PUZZLE: Program = Program.to(1) + HIDDEN_PUZZLE_HASH: bytes32 = HIDDEN_PUZZLE.get_tree_hash() + + secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) + secp_pk = secp_sk.verifying_key.to_string("compressed") + escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, HIDDEN_PUZZLE_HASH) + coin_id = Program.to("coin_id").get_tree_hash() + conditions = Program.to([[51, ACS_PH, 100]]) + hidden_escape_solution = Program.to([HIDDEN_PUZZLE, conditions, 0, coin_id]) + _, hidden_result = run_with_secp(escape_puzzle, hidden_escape_solution) + assert hidden_result == Program.to(conditions) + + def test_recovery_puzzles() -> None: bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) bls_pk = bls_sk.get_g1() @@ -45,7 +59,9 @@ def test_recovery_puzzles() -> None: coin_id = Program.to("coin_id").get_tree_hash() recovery_conditions = Program.to([[51, p2_puzzlehash, amount]]) - escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) + escape_puzzle = P2_DELEGATED_SECP_MOD.curry( + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH + ) escape_puzzlehash = escape_puzzle.get_tree_hash() finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) finish_puzzlehash = finish_puzzle.get_tree_hash() @@ -82,7 +98,7 @@ def test_recovery_puzzles() -> None: delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ENTROPY + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) secp_solution = Program.to( [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] @@ -95,13 +111,13 @@ def test_recovery_puzzles() -> None: def test_p2_delegated_secp() -> None: secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") - secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) coin_id = Program.to("coin_id").get_tree_hash() delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, 1000]]) signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ENTROPY + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) @@ -126,7 +142,7 @@ def test_vault_root_puzzle() -> None: # secp puzzle secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") - secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) + secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) secp_puzzlehash = secp_puzzle.get_tree_hash() # recovery keys @@ -151,7 +167,7 @@ def test_vault_root_puzzle() -> None: delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + ENTROPY + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) @@ -162,7 +178,9 @@ def test_vault_root_puzzle() -> None: # recovery spend path recovery_conditions = Program.to([[51, ACS_PH, amount]]) - curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, ENTROPY) + curried_escape_puzzle = P2_DELEGATED_SECP_MOD.curry( + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH + ) curried_finish_puzzle = RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) recovery_merkle_tree = MerkleTree([curried_escape_puzzle.get_tree_hash(), curried_finish_puzzle.get_tree_hash()]) recovery_merkle_root = recovery_merkle_tree.calculate_root() From 5f60c65c42561dcc1e774ac6675453f1ab50d2b4 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Dec 2023 14:30:16 -0800 Subject: [PATCH 056/274] Add get_main_wallet_driver to WSM --- chia/wallet/wallet_node.py | 2 -- chia/wallet/wallet_state_manager.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index c867023e86c0..a237cef66847 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -77,7 +77,6 @@ subscribe_to_phs, ) from chia.wallet.util.wallet_types import CoinType, WalletType -from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler, get_wp_fork_point @@ -431,7 +430,6 @@ async def _start_with_fingerprint( self.server, self.root_path, self, - Wallet, observation_root, ) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index c022748db366..113676298c58 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -222,7 +222,6 @@ async def create( server: ChiaServer, root_path: Path, wallet_node: WalletNode, - main_wallet_driver: Type[MainWalletProtocol], observation_root: Optional[ObservationRoot] = None, ) -> WalletStateManager: self = WalletStateManager() @@ -292,7 +291,7 @@ async def create( puzzle_decorators = self.config.get("puzzle_decorators", {}).get(fingerprint, []) self.decorator_manager = PuzzleDecoratorManager.create(puzzle_decorators) - self.main_wallet = await main_wallet_driver.create(self, main_wallet_info) + self.main_wallet = await self.get_main_wallet_driver(self.observation_root).create(self, main_wallet_info) self.wallets = {main_wallet_info.id: self.main_wallet} @@ -365,6 +364,13 @@ async def create( return self + def get_main_wallet_driver(self, observation_root: ObservationRoot) -> Type[MainWalletProtocol]: + root_bytes: bytes = bytes(observation_root) + if len(root_bytes) == 48: + return Wallet + + raise ValueError(f"Could not find a valid wallet type for observation_root: {root_bytes.hex()}") + def get_public_key_unhardened(self, index: uint32) -> G1Element: if not isinstance(self.observation_root, G1Element): raise ValueError("Public key derivation is not supported for non-G1Element keys") From e86de987b80589f59d920acec375b9f40bd1ab8f Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Dec 2023 07:52:21 -0800 Subject: [PATCH 057/274] Add a MainWalletProtocol test and sort out implications --- chia/data_layer/data_layer_wallet.py | 6 + chia/pools/pool_wallet.py | 7 + chia/rpc/wallet_rpc_api.py | 2 +- chia/wallet/cat_wallet/cat_wallet.py | 6 + chia/wallet/cat_wallet/dao_cat_wallet.py | 7 + chia/wallet/dao_wallet/dao_wallet.py | 7 + chia/wallet/did_wallet/did_wallet.py | 6 + chia/wallet/nft_wallet/nft_wallet.py | 6 + chia/wallet/vc_wallet/vc_wallet.py | 7 + chia/wallet/wallet.py | 27 +- chia/wallet/wallet_node.py | 16 +- chia/wallet/wallet_protocol.py | 7 + chia/wallet/wallet_state_manager.py | 74 +++--- tests/wallet/conftest.py | 23 +- tests/wallet/test_main_wallet_protocol.py | 301 ++++++++++++++++++++++ 15 files changed, 452 insertions(+), 50 deletions(-) create mode 100644 tests/wallet/test_main_wallet_protocol.py diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index c6442519d51b..bb88521ca208 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -1335,6 +1335,12 @@ async def select_coins( async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return coin.amount % 2 == 1 and await self.wallet_state_manager.dl_store.get_launcher(hint) is not None + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() + def verify_offer( maker: Tuple[StoreProofs, ...], diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 6f43488ff9c8..56e2e13d1953 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -45,6 +45,7 @@ from chia.types.spend_bundle import SpendBundle, estimate_fees from chia.util.ints import uint32, uint64, uint128 from chia.wallet.conditions import AssertCoinAnnouncement, Condition, ConditionValidTimes, parse_timelock_info +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import find_owner_sk from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType @@ -974,3 +975,9 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: # pragma: no cover return False # PoolWallet pre-dates hints + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 23144ca25f60..873d496b0a2d 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -382,7 +382,7 @@ async def log_in(self, request: Dict[str, Any]) -> EndpointResult: if started is True: return {"fingerprint": fingerprint} - return {"success": False, "error": "Unknown Error"} + return {"success": False, "error": f"fingerprint {fingerprint} not found in keychain or keychain is empty"} async def get_logged_in_fingerprint(self, request: Dict[str, Any]) -> EndpointResult: return {"fingerprint": self.service.logged_in_fingerprint} diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index ece019da434b..e5994feaeea5 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -881,3 +881,9 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: ).get_tree_hash_precalc(hint) == coin.puzzle_hash ) + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index 53045f8899ff..63c993448a4c 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -30,6 +30,7 @@ get_innerpuz_from_lockup_puzzle, get_lockup_puzzle, ) +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord @@ -670,3 +671,9 @@ async def save_info(self, dao_cat_info: DAOCATInfo) -> None: def get_name(self) -> str: return self.wallet_info.name + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/wallet/dao_wallet/dao_wallet.py b/chia/wallet/dao_wallet/dao_wallet.py index 688717573e73..e8119d43cfa0 100644 --- a/chia/wallet/dao_wallet/dao_wallet.py +++ b/chia/wallet/dao_wallet/dao_wallet.py @@ -60,6 +60,7 @@ uncurry_proposal, uncurry_treasury, ) +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.singleton import ( get_inner_puzzle_from_singleton, @@ -2106,3 +2107,9 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 raise ValueError(f"Unsupported spend in DAO Wallet: {self.id()}") return True + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 987cab5da802..df2abc7bd6e5 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -1461,3 +1461,9 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: ).get_tree_hash_precalc(hint) == coin.puzzle_hash ) + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index fe2fcd68adff..23c7ec5af876 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -1689,3 +1689,9 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return False + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index aa0db93892ac..ab272e03207b 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -28,6 +28,7 @@ UnknownCondition, parse_timelock_info, ) +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.payment import Payment from chia.wallet.puzzle_drivers import Solver @@ -638,6 +639,12 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return False + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() + if TYPE_CHECKING: _dummy: WalletProtocol[VerifiedCredential] = VCWallet() # pragma: no cover diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 52e6dfec2db5..c0469a80e5ed 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -18,7 +18,13 @@ 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 AssertCoinAnnouncement, Condition, CreateCoinAnnouncement, parse_timelock_info +from chia.wallet.conditions import ( + AssertCoinAnnouncement, + Condition, + CreateCoin, + CreateCoinAnnouncement, + parse_timelock_info, +) from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import ( MAX_POOL_WALLETS, @@ -276,7 +282,12 @@ async def _generate_unsigned_transaction( if primaries_input is not None: primaries.extend(primaries_input) - total_amount = amount + sum(primary.amount for primary in primaries) + fee + total_amount = ( + amount + + sum(primary.amount for primary in primaries) + + fee + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + ) total_balance = await self.get_spendable_balance() if coins is None: if total_amount > total_balance: @@ -407,9 +418,11 @@ async def generate_signed_transaction( 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 + non_change_amount: int = amount else: - non_change_amount = uint64(amount + sum(p.amount for p in primaries)) + non_change_amount = amount + sum(p.amount for p in primaries) + + non_change_amount += sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) self.log.debug("Generating transaction for: %s %s %s", puzzle_hash, amount, repr(coins)) transaction = await self._generate_unsigned_transaction( @@ -634,3 +647,9 @@ async def apply_signatures( ) ], ) + + def handle_own_derivation(self) -> bool: + return False + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index a237cef66847..1d827e13aaca 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -261,14 +261,12 @@ async def get_key( fingerprint, private=private ) - if key is None and fingerprint is not None: - key = await self.get_key_for_fingerprint(None, private=private) - if key is not None: - if isinstance(key, PrivateKey): - fp = key.get_g1().get_fingerprint() - else: - fp = key.get_fingerprint() - self.log.info(f"Using first key found (fingerprint: {fp})") + if fingerprint is None and key is not None: + if isinstance(key, PrivateKey): + fp = key.get_g1().get_fingerprint() + else: + fp = key.get_fingerprint() + self.log.info(f"Using first key found (fingerprint: {fp})") return key @@ -390,6 +388,8 @@ async def _start_with_fingerprint( self._new_peak_queue = NewPeakQueue(inner_queue=asyncio.PriorityQueue()) if not fingerprint: fingerprint = self.get_last_used_fingerprint() + if fingerprint is not None and await self.get_key_for_fingerprint(fingerprint) is None: + fingerprint = None multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log) multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self._weight_proof_handler = WalletWeightProofHandler(self.constants, multiprocessing_context) diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 6a8db45537a7..a2ab5bf7f287 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -13,6 +13,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64, uint128 from chia.wallet.conditions import Condition +from chia.wallet.derivation_record import DerivationRecord from chia.wallet.nft_wallet.nft_info import NFTCoinInfo from chia.wallet.payment import Payment from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata @@ -83,6 +84,12 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: ... + def handle_own_derivation(self) -> bool: + ... + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + ... + wallet_info: WalletInfo # WalletStateManager is only imported for type hinting thus leaving pylint # unable to process this diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 113676298c58..c317cfd08ceb 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -454,52 +454,58 @@ async def create_more_puzzle_hashes( f"Creating puzzle hashes from {start_index} to {last_index - 1} for wallet_id: {wallet_id}" ) self.log.info(f"Start: {creating_msg}") - if self.private_key is not None: - intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) - # This function shoul work for other types of observation roots too - # However to generalize this function beyond pubkeys is beyond the scope of current work - # So we're just going to sanitize and move on - assert isinstance(self.observation_root, G1Element) - intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.observation_root) + if not target_wallet.handle_own_derivation(): + if self.private_key is not None: + intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) + # This function shoul work for other types of observation roots too + # However to generalize this function beyond pubkeys is beyond the scope of current work + # So we're just going to sanitize and move on + assert isinstance(self.observation_root, G1Element) + intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.observation_root) for index in range(start_index, last_index): - if target_wallet.type() == WalletType.POOLING_WALLET: - continue + if target_wallet.handle_own_derivation(): + derivation_paths.extend(target_wallet.derivation_for_index(index)) + else: + if target_wallet.type() == WalletType.POOLING_WALLET: + continue - if self.private_key is not None: - # Hardened - pubkey: G1Element = _derive_path(intermediate_sk, [index]).get_g1() - puzzlehash: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) - self.log.debug(f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash.hex()}") - new_paths = True + if self.private_key is not None: + # Hardened + pubkey: G1Element = _derive_path(intermediate_sk, [index]).get_g1() + puzzlehash: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey) + 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_pk_unhardened(intermediate_pk_un, [index]) + puzzlehash_unhardened: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey_unhardened) + self.log.debug( + f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" + ) derivation_paths.append( DerivationRecord( uint32(index), - puzzlehash, - pubkey, + puzzlehash_unhardened, + pubkey_unhardened, target_wallet.type(), uint32(target_wallet.id()), - True, + False, ) ) - # Unhardened - pubkey_unhardened: G1Element = _derive_pk_unhardened(intermediate_pk_un, [index]) - puzzlehash_unhardened: bytes32 = target_wallet.puzzle_hash_for_pk(pubkey_unhardened) - 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: diff --git a/tests/wallet/conftest.py b/tests/wallet/conftest.py index 0fce4f5e6e13..04ba08e4d539 100644 --- a/tests/wallet/conftest.py +++ b/tests/wallet/conftest.py @@ -59,6 +59,20 @@ class WalletEnvironment: wallet_states: Dict[uint32, WalletState] wallet_aliases: Dict[str, int] = field(default_factory=dict) + async def restart(self, new_fingerprint: Optional[int]) -> None: + old_peer_info = next(v for v in self.wallet_node.server.all_connections.values()).peer_info + await self.rpc_client.log_in( + new_fingerprint + if new_fingerprint is not None + else self.wallet_state_manager.observation_root.get_fingerprint() + ) + + await self.wallet_node.server.start_client(old_peer_info, None) + + self.wallet_state_manager = self.wallet_node.wallet_state_manager + self.xch_wallet = self.wallet_state_manager.main_wallet + self.wallet_states = {} + def dealias_wallet_id(self, wallet_id_or_alias: Union[int, str]) -> uint32: """ This function turns something that is either a wallet id or a wallet alias into a wallet id. @@ -364,9 +378,12 @@ async def wallet_environments( wallet_states: List[WalletState] = [] for service, blocks_needed in zip(wallet_services, request.param["blocks_needed"]): - await full_node[0]._api.farm_blocks_to_wallet( - count=blocks_needed, wallet=service._node.wallet_state_manager.main_wallet - ) + if blocks_needed > 0: + await full_node[0]._api.farm_blocks_to_wallet( + count=blocks_needed, wallet=service._node.wallet_state_manager.main_wallet + ) + else: + await full_node[0]._api.farm_blocks_to_puzzlehash(1) await full_node[0]._api.wait_for_wallet_synced(wallet_node=service._node, timeout=20) wallet_states.append( WalletState( diff --git a/tests/wallet/test_main_wallet_protocol.py b/tests/wallet/test_main_wallet_protocol.py new file mode 100644 index 000000000000..a647cc627b33 --- /dev/null +++ b/tests/wallet/test_main_wallet_protocol.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import logging +import time +import types +from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, Type + +import pytest +from chia_rs import G1Element, G2Element, PrivateKey +from typing_extensions import Unpack + +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_spend import CoinSpend +from chia.types.signing_mode import SigningMode +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint32, uint64 +from chia.util.observation_root import ObservationRoot +from chia.wallet.conditions import Condition, CreateCoin, ReserveFee, parse_timelock_info +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.payment import Payment +from chia.wallet.signer_protocol import ( + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + SumHint, + TransactionInfo, +) +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.tx_config import TXConfig +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import GSTOptionalArgs, MainWalletProtocol +from chia.wallet.wallet_state_manager import WalletStateManager +from tests.wallet.conftest import WalletStateTransition, WalletTestFramework + +ACS: Program = Program.to(1) +ACS_PH: bytes32 = ACS.get_tree_hash() + + +class AnyoneCanSpend(Wallet): + @staticmethod + async def create( + wallet_state_manager: Any, + info: WalletInfo, + name: str = __name__, + ) -> AnyoneCanSpend: + self = AnyoneCanSpend() + self.wallet_state_manager = wallet_state_manager + self.wallet_info = info + self.wallet_id = info.id + self.log = logging.getLogger(name) + return self + + async def get_new_puzzle(self) -> Program: + return ACS + + async def get_new_puzzlehash(self) -> bytes32: + return ACS_PH + + 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, + memos: Optional[List[bytes]] = None, + puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None, + extra_conditions: Tuple[Condition, ...] = tuple(), + **kwargs: Unpack[GSTOptionalArgs], + ) -> List[TransactionRecord]: + condition_list: List[Payment] = [] if primaries is None else primaries + condition_list.append(Payment(puzzle_hash, amount, [] if memos is None else memos)) + non_change_amount: int = ( + sum(c.amount for c in condition_list) + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + + fee + ) + + coins = await self.select_coins(uint64(non_change_amount), tx_config.coin_selection_config) + total_amount = sum(c.amount for c in coins) + + condition_list.append(Payment(ACS_PH, uint64(total_amount - non_change_amount))) + + spend_bundle = SpendBundle( + [ + CoinSpend( + coin, + ACS, + self.make_solution(condition_list, extra_conditions, fee) if i == 0 else Program.to([]), + ) + for i, coin in enumerate(coins) + ], + G2Element(), + ) + + now = uint64(int(time.time())) + 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=spend_bundle.additions(), + removals=spend_bundle.removals(), + 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), + ) + ] + + def puzzle_for_pk(self, pubkey: G1Element) -> Program: + raise ValueError("This won't work") + + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: + if puzzle_hash == ACS_PH: + return ACS + else: + raise ValueError("puzzle hash was not ACS_PH") + + async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> Tuple[G1Element, G2Element]: + raise ValueError("This won't work") + + async def get_puzzle_hash(self, new: bool) -> bytes32: + return ACS_PH + + async def apply_signatures( + self, spends: List[Spend], signing_responses: List[SigningResponse] + ) -> SignedTransaction: + return SignedTransaction( + TransactionInfo(spends), + [], + ) + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> List[SigningResponse]: + if len(signing_instructions.targets) > 0: + raise ValueError("This won't work") + else: + return [] + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + return None + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + return None + + def make_solution( + self, + primaries: List[Payment], + conditions: Tuple[Condition, ...] = tuple(), + fee: uint64 = uint64(0), + ) -> Program: + condition_list: List[Condition] = [CreateCoin(p.puzzle_hash, p.amount, p.memos) for p in primaries] + condition_list.append(ReserveFee(fee)) + condition_list.extend(conditions) + prog: Program = Program.to([c.to_program() for c in condition_list]) + return prog + + async def get_puzzle(self, new: bool) -> Program: + return ACS + + def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: + raise ValueError("This won't work") + + def require_derivation_paths(self) -> bool: + return True + + async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: + if coin.puzzle_hash == ACS_PH or hint == ACS_PH: + return True + else: + return False + + def handle_own_derivation(self) -> bool: + return True + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + return [ + DerivationRecord( + uint32(index), + ACS_PH, + G1Element(), + self.type(), + uint32(self.id()), + False, + ) + ] + + +async def acs_setup(wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch) -> None: + def get_main_wallet_driver(self: WalletStateManager, observation_root: ObservationRoot) -> Type[MainWalletProtocol]: + return AnyoneCanSpend + + monkeypatch.setattr( + WalletStateManager, + "get_main_wallet_driver", + types.MethodType(get_main_wallet_driver, WalletStateManager), + ) + + for env in wallet_environments.environments: + pk = PrivateKey.from_bytes( + bytes.fromhex("548dd25590a19f0a6a294560fc36f2900575fb9d1b2650e6fe80ad9abc1c4a60") + ).get_g1() + await env.wallet_node.keychain_proxy.add_public_key(bytes(pk).hex()) + await env.restart(pk.get_fingerprint()) + + +async def bls_got_setup(wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch) -> None: + return None + + +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [0], + } + ], + indirect=True, +) +@pytest.mark.parametrize("setup_function", [acs_setup, bls_got_setup]) +@pytest.mark.anyio +async def test_main_wallet( + setup_function: Callable[[WalletTestFramework, pytest.MonkeyPatch], Awaitable[None]], + wallet_environments: WalletTestFramework, + monkeypatch: pytest.MonkeyPatch, +) -> None: + await setup_function(wallet_environments, monkeypatch) + main_wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet + ph: bytes32 = await main_wallet.get_puzzle_hash(False) + await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, ph, guarantee_transaction_blocks=True) + await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, guarantee_transaction_blocks=True) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "confirmed_wallet_balance": 2_000_000_000_000, + "unconfirmed_wallet_balance": 2_000_000_000_000, + "max_send_amount": 2_000_000_000_000, + "spendable_balance": 2_000_000_000_000, + "unspent_coin_count": 2, + } + } + ) + ] + ) + txs: List[TransactionRecord] = await main_wallet.generate_signed_transaction( + uint64(1_750_000_000_001), + ph, + wallet_environments.tx_config, + fee=uint64(2), + primaries=[Payment(ph, uint64(3))], + extra_conditions=(CreateCoin(ph, uint64(4)),), + ) + await main_wallet.wallet_state_manager.add_pending_transactions(txs) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "unconfirmed_wallet_balance": -2, # Only thing that actually went out was fee + "max_send_amount": -2_000_000_000_000, # All coins are now pending + "spendable_balance": -2_000_000_000_000, # All coins are now pending + "pending_change": 1_999_999_999_998, + "pending_coin_removal_count": 2, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -2, + "max_send_amount": 1_999_999_999_998, + "spendable_balance": 1_999_999_999_998, + "pending_change": -1_999_999_999_998, + "pending_coin_removal_count": -2, + # Minus: both farmed coins. Plus: Output, change, primary, extra_condition + "unspent_coin_count": -2 + 4, + } + }, + ) + ] + ) From 53c271a070d42fdc0e98f852a274a31f484f7d4a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Dec 2023 10:15:55 -0800 Subject: [PATCH 058/274] remove test (replace later) --- tests/wallet/test_wallet_node.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index 0b9587fdd781..cd38a436d0b3 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -86,28 +86,6 @@ async def test_get_private_key_missing_key( assert key is None -@pytest.mark.anyio -async def test_get_private_key_missing_key_use_default( - root_path_populated_with_config: Path, get_temp_keyring: Keychain -) -> None: - root_path: Path = root_path_populated_with_config - keychain: Keychain = get_temp_keyring - config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") - node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - sk: PrivateKey = keychain.add_private_key(generate_mnemonic()) - fingerprint: int = sk.get_g1().get_fingerprint() - - # Stupid sanity check that the fingerprint we're going to use isn't actually in the keychain - assert fingerprint != 1234567890 - - # When fingerprint is provided and the key is missing, we should get the default (first) key - key = await node.get_key(1234567890) - - assert key is not None - assert isinstance(key, PrivateKey) - assert key.get_g1().get_fingerprint() == fingerprint - - @pytest.mark.anyio async def test_get_public_key(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: root_path: Path = root_path_populated_with_config From 9a1adc66792f3c4fc26a8fed5bce85517457ee0c Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 21 Dec 2023 10:38:05 -0800 Subject: [PATCH 059/274] Take suggestion from altendky Co-authored-by: Kyle Altendorf --- chia/rpc/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index f7cbb661676f..53b0e2555e71 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -24,7 +24,9 @@ def marshal(func: MarshallableRpcEndpoint) -> RpcEndpoint: hints = get_type_hints(func) - request_class: Type[Streamable] = hints["request"] + request_hint = hints["request"] + assert isinstance(request_hint, Streamable) + request_class = request_hint async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: object) -> Dict[str, Any]: response_obj: Streamable = await func( From 938ab32d0c58e05839ddd34d78f5931938b4dd71 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Dec 2023 10:45:33 -0800 Subject: [PATCH 060/274] remove test (replace later) --- tests/wallet/test_wallet_node.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index cd38a436d0b3..bc45f38740ba 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -149,31 +149,6 @@ async def test_get_public_key_missing_key( assert key is None -@pytest.mark.anyio -async def test_get_public_key_missing_key_use_default( - root_path_populated_with_config: Path, get_temp_keyring: Keychain -) -> None: - root_path: Path = root_path_populated_with_config - keychain: Keychain = get_temp_keyring - config: Dict[str, Any] = load_config(root_path, "config.yaml", "wallet") - node: WalletNode = WalletNode(config, root_path, test_constants, keychain) - pk, key_type = keychain.add_public_key( - "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ) - fingerprint: int = pk.get_fingerprint() - - # Stupid sanity check that the fingerprint we're going to use isn't actually in the keychain - assert fingerprint != 1234567890 - - # When fingerprint is provided and the key is missing, we should get the default (first) key - key = await node.get_key(1234567890, private=False) - - assert key is not None - assert isinstance(key, G1Element) - assert key.get_fingerprint() == fingerprint - assert key_type == KeyTypes.G1_ELEMENT - - def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychain) -> None: root_path: Path = root_path_populated_with_config keychain: Keychain = get_temp_keyring From 23a3f341dd7e492df909f4ea92b36d56d1e6cf91 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Dec 2023 10:53:08 -0800 Subject: [PATCH 061/274] Terrible suggestion by @altendky --- chia/rpc/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 53b0e2555e71..f26396722491 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -2,7 +2,7 @@ import logging import traceback -from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, get_type_hints +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, get_type_hints import aiohttp @@ -25,7 +25,7 @@ def marshal(func: MarshallableRpcEndpoint) -> RpcEndpoint: hints = get_type_hints(func) request_hint = hints["request"] - assert isinstance(request_hint, Streamable) + assert issubclass(request_hint, Streamable) request_class = request_hint async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: object) -> Dict[str, Any]: From 1ec3b52212e3adc955f43aebf6e3fbafca8ec693 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 22 Dec 2023 09:41:00 +1300 Subject: [PATCH 062/274] calc delegated_puzhash in a let form --- .../puzzles/deployed_puzzle_hashes.json | 2 +- .../puzzles/p2_delegated_or_hidden_secp.clsp | 20 ++++++++++--------- .../p2_delegated_or_hidden_secp.clsp.hex | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 04639e84d5fa..124a53af516c 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -43,7 +43,7 @@ "p2_announced_delegated_puzzle": "c4d24c3c5349376f3e8f3aba202972091713b4ec4915f0f26192ae4ace0bd04d", "p2_conditions": "1c77d7d5efde60a7a1d2d27db6d746bc8e568aea1ef8586ca967a0d60b83cc36", "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", - "p2_delegated_or_hidden_secp": "fdc746afe69a97548e0ff298209bb000e4a649ab3a2a3e3db18c97cf01e0d8b5", + "p2_delegated_or_hidden_secp": "007b927c693b5594c777ce0a433d5bce0c1c3a101140c1897301f74b0ae103f5", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp index ed52941a50bd..c5b66d264e33 100644 --- a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp @@ -6,14 +6,16 @@ (include condition_codes.clib) (include sha256tree.clib) - (if (= (sha256tree delegated_puzzle) HIDDEN_PUZZLE_HASH) - (a delegated_puzzle delegated_solution) - (if (secp256r1_verify SECP_PK (sha256 (sha256tree delegated_puzzle) coin_id GENESIS_CHALLENGE HIDDEN_PUZZLE_HASH) signature) - (x) ; this doesn't actually run because secp256_verify will raise on failure - (c - (list ASSERT_MY_COIN_ID coin_id) - (a delegated_puzzle delegated_solution) - ) - ) + (let ((delegated_puzzle_hash (sha256tree delegated_puzzle))) + (if (= delegated_puzzle_hash HIDDEN_PUZZLE_HASH) + (a delegated_puzzle delegated_solution) + (if (secp256r1_verify SECP_PK (sha256 delegated_puzzle_hash coin_id GENESIS_CHALLENGE HIDDEN_PUZZLE_HASH) signature) + (x) ; this doesn't actually run because secp256_verify will raise on failure + (c + (list ASSERT_MY_COIN_ID coin_id) + (a delegated_puzzle delegated_solution) + ) + ) + ) ) ) diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex index ad76d33d0b38..d89e050ccc47 100644 --- a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ffff09ffff02ff02ffff04ff02ffff04ff2fff80808080ff1780ffff01ff02ffff01ff02ff2fff5f80ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ff0bffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ff82017fff05ff1780ff8200bf80ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ff82017fffff01808080ffff02ff2fff5f8080ff018080ff0180ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 +ff02ffff01ff02ffff03ffff09ffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ff018080808080ffff01ff02ffff01ff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff0180808080808080ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ffff05ffff06ffff06ff01808080ffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff05ffff06ff018080ffff05ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01808080ffff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff018080808080808080ff018080ff0180ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 From 8752cbdcf613e498e0da47fb4403a571ae0da01e Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 3 Jan 2024 07:48:09 +1300 Subject: [PATCH 063/274] lint --- setup.py | 1 + tests/wallet/vault/test_vault_clsp.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e91293dec140..2922a9c80c7b 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "zstd==1.5.5.1", "packaging==23.2", "psutil==5.9.4", + "ecdsa==0.18.0", # For SECP ] upnp_dependencies = [ diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index b2de34d1c8bb..e6464eb924a2 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -4,8 +4,7 @@ from typing import Tuple import pytest -from blspy import PrivateKey -from chia_rs import ENABLE_SECP_OPS +from chia_rs import ENABLE_SECP_OPS, PrivateKey from ecdsa import NIST256p, SigningKey from chia.consensus.default_constants import DEFAULT_CONSTANTS From 5657e00c600530efdcfc8444fad07b7d0b82033a Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 3 Jan 2024 09:27:27 +1300 Subject: [PATCH 064/274] add config.py for tests --- tests/wallet/vault/config.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/wallet/vault/config.py diff --git a/tests/wallet/vault/config.py b/tests/wallet/vault/config.py new file mode 100644 index 000000000000..e46b82aa493b --- /dev/null +++ b/tests/wallet/vault/config.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +job_timeout = 90 +checkout_blocks_and_plots = True From a3c5a077cca349c2bca7f2546abc541dcb43a5bd Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jan 2024 13:21:02 -0800 Subject: [PATCH 065/274] fix test coverage --- tests/core/daemon/test_daemon.py | 10 ++++++++++ tests/core/daemon/test_keychain_proxy.py | 22 ++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index a9161616c845..0d0303e0bdfc 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -9,6 +9,7 @@ import pkg_resources import pytest from aiohttp.web_ws import WebSocketResponse +from chia_rs import G1Element from pytest_mock import MockerFixture from chia.daemon.client import connect_to_daemon @@ -386,6 +387,9 @@ def mock_daemon_with_config_and_keys(get_keychain_for_function, root_path_popula keychain.add_private_key(test_key_data.mnemonic_str()) keychain.add_private_key(test_key_data_2.mnemonic_str()) + # Throw in an unused pubkey-only entry + keychain.add_public_key(bytes(G1Element()).hex()) + # Mock daemon server with net_config set for mainnet return Daemon(services={}, connections={}, net_config=config) @@ -572,6 +576,12 @@ async def test_get_routes(mock_lonely_daemon): response={ "success": True, "wallet_addresses": { + G1Element().get_fingerprint(): [ + { + "address": "xch1dr2sj4jqdt6nj4l32d4f5dk7mrwak3qw5hsykty5lhhd00053y0szaz8zj", + "hd_path": "m/12381/8444/2/0", + } + ], test_key_data.fingerprint: [ { "address": "xch1zze67l3jgxuvyaxhjhu7326sezxxve7lgzvq0497ddggzhff7c9s2pdcwh", diff --git a/tests/core/daemon/test_keychain_proxy.py b/tests/core/daemon/test_keychain_proxy.py index 4355a48f199c..fb29c1b72780 100644 --- a/tests/core/daemon/test_keychain_proxy.py +++ b/tests/core/daemon/test_keychain_proxy.py @@ -2,14 +2,14 @@ import logging from dataclasses import replace -from typing import AsyncGenerator +from typing import Any, AsyncGenerator import pytest from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate from chia.simulator.block_tools import BlockTools from chia.simulator.setup_services import setup_daemon -from chia.util.errors import KeychainKeyNotFound +from chia.util.errors import KeychainIsEmpty, KeychainKeyNotFound from chia.util.keychain import KeyData TEST_KEY_1 = KeyData.generate(label="🚽🍯") @@ -17,12 +17,14 @@ TEST_KEY_3 = KeyData.generate(label="☕️🍬") -@pytest.fixture(scope="function") -async def keychain_proxy(get_b_tools: BlockTools) -> AsyncGenerator[KeychainProxy, None]: +@pytest.fixture(scope="function", params=[True, False]) +async def keychain_proxy(get_b_tools: BlockTools, request: Any) -> AsyncGenerator[KeychainProxy, None]: async with setup_daemon(btools=get_b_tools) as daemon: log = logging.getLogger("keychain_proxy_fixture") keychain_proxy = await connect_to_keychain_and_validate(daemon.root_path, log) assert keychain_proxy is not None + if request.param: + keychain_proxy.keychain = daemon.keychain_server._default_keychain yield keychain_proxy await keychain_proxy.close() @@ -74,6 +76,18 @@ async def test_get_key(keychain_proxy_with_keys: KeychainProxy, include_secrets: assert key == expected_key +@pytest.mark.anyio +async def test_get_key_for_fingerprint(keychain_proxy: KeychainProxy) -> None: + keychain = keychain_proxy + with pytest.raises(KeychainIsEmpty): + await keychain.get_public_key_for_fingerprint(None) + await keychain_proxy.add_private_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) + assert await keychain.get_public_key_for_fingerprint(TEST_KEY_1.fingerprint) == TEST_KEY_1.public_key + assert await keychain.get_public_key_for_fingerprint(None) == TEST_KEY_1.public_key + with pytest.raises(KeychainKeyNotFound): + await keychain.get_public_key_for_fingerprint(1234567890) + + @pytest.mark.parametrize("include_secrets", [True, False]) @pytest.mark.anyio async def test_get_keys(keychain_proxy_with_keys: KeychainProxy, include_secrets: bool) -> None: From 277c9631dda584ea489397b4cd62b5d8a97111fd Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 3 Jan 2024 10:29:48 +1300 Subject: [PATCH 066/274] remove comments --- tests/wallet/vault/test_vault_wallet.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index f366514b3523..9856e230f32d 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -29,10 +29,7 @@ @pytest.mark.anyio async def test_vault_creation(wallet_environments: WalletTestFramework) -> None: # Setup - # full_node_api: FullNodeSimulator = wallet_environments.full_node env = wallet_environments.environments[0] - # wallet_node = env.wallet_node - # wallet = env.xch_wallet client = env.rpc_client fingerprint = (await client.get_public_keys())[0] From 8b1072f800d6a084b756ce648768d743919f700f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 2 Jan 2024 14:44:38 -0800 Subject: [PATCH 067/274] Fix test coverage --- tests/core/cmds/test_keys.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index a5911f3986a9..2e8f1e4b641d 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -7,6 +7,7 @@ from typing import Dict, List, Optional import pytest +from chia_rs import G1Element from click.testing import CliRunner from chia.cmds.chia import cli @@ -31,6 +32,14 @@ def keyring_with_one_key(empty_keyring): return keychain +@pytest.fixture(scope="function") +def keyring_with_one_sk_one_pk(empty_keyring): + keychain = empty_keyring + keychain.add_private_key(TEST_MNEMONIC_SEED) + keychain.add_public_key(bytes(G1Element()).hex()) + return keychain + + @pytest.fixture(scope="function") def mnemonic_seed_file(tmp_path): seed_file = Path(tmp_path) / "seed.txt" @@ -302,12 +311,12 @@ def test_show_labels(self, empty_keyring, tmp_path): else: assert label == key.label - def test_show(self, keyring_with_one_key, tmp_path): + def test_show(self, keyring_with_one_sk_one_pk, tmp_path): """ Test that the `chia keys show` command shows the correct key. """ - keychain = keyring_with_one_key + keychain = keyring_with_one_sk_one_pk assert len(keychain.get_all_private_keys()) == 1 @@ -327,6 +336,10 @@ def test_show(self, keyring_with_one_key, tmp_path): # assert result.exit_code == 0 assert result.output.find(f"Fingerprint: {TEST_FINGERPRINT}") != -1 + assert result.output.find(f"Fingerprint: {G1Element().get_fingerprint()}") != -1 + + result = runner.invoke(cli, [*base_params, *cmd_params, "--non-observer-derivation"], catch_exceptions=False) + assert result.output.find("First wallet address (non-observer): N/A") != -1 def test_show_fingerprint(self, keyring_with_one_key, tmp_path): """ From ea59cb1d5faab24177876b0d6906c2e47c741567 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 3 Jan 2024 12:25:39 +1300 Subject: [PATCH 068/274] Add keychain support and placeholder for vault wallet --- chia/util/keychain.py | 28 ++++-- chia/wallet/vault/vault_root.py | 17 ++++ chia/wallet/vault/vault_wallet.py | 119 ++++++++++++++++++++++++ tests/wallet/vault/test_vault_wallet.py | 66 ++++++++----- 4 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 chia/wallet/vault/vault_root.py create mode 100644 chia/wallet/vault/vault_wallet.py diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 01f20b3e109a..7b3aad71a5c6 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -30,6 +30,7 @@ from chia.util.keyring_wrapper import KeyringWrapper from chia.util.observation_root import ObservationRoot from chia.util.streamable import Streamable, streamable +from chia.wallet.vault.vault_root import VaultRoot CURRENT_KEY_VERSION = "1.8" DEFAULT_USER = f"user-chia-{CURRENT_KEY_VERSION}" # e.g. user-chia-1.8 @@ -222,11 +223,14 @@ def mnemonic_str(self) -> str: class KeyTypes(str, Enum): G1_ELEMENT = "G1 Element" + VAULT_LAUNCHER = "Vault Launcher" @classmethod def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: if key_type == cls.G1_ELEMENT: return G1Element.from_bytes(pk_bytes) + if key_type == cls.VAULT_LAUNCHER: + return VaultRoot(pk_bytes) else: raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") @@ -249,6 +253,8 @@ class KeyData(Streamable): def observation_root(self) -> ObservationRoot: if self.key_type == KeyTypes.G1_ELEMENT: return G1Element.from_bytes(self.public_key) + if self.key_type == KeyTypes.VAULT_LAUNCHER: + return VaultRoot.from_bytes(self.public_key) raise TypeError(f"Invalid key_type {self.key_type}") def __post_init__(self) -> None: @@ -336,19 +342,26 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: str_bytes = bytes.fromhex(read_str) pk_bytes: bytes = str_bytes[: G1Element.SIZE] - observation_root: ObservationRoot = G1Element.from_bytes(pk_bytes) - fingerprint = observation_root.get_fingerprint() - if len(str_bytes) == G1Element.SIZE + 32: - entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] - else: + if len(pk_bytes) == 32: + observation_root: ObservationRoot = VaultRoot.from_bytes(pk_bytes) + fingerprint = observation_root.get_fingerprint() entropy = None + key_type = KeyTypes.VAULT_LAUNCHER.value + else: + observation_root = G1Element.from_bytes(pk_bytes) + fingerprint = observation_root.get_fingerprint() + if len(str_bytes) == G1Element.SIZE + 32: + entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + else: + entropy = None + key_type = KeyTypes.G1_ELEMENT.value return KeyData( fingerprint=uint32(fingerprint), public_key=pk_bytes, label=self.keyring_wrapper.get_label(fingerprint), secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, - key_type=KeyTypes.G1_ELEMENT.value, + key_type=key_type, ) def _get_free_private_key_index(self) -> int: @@ -405,6 +418,9 @@ def add_public_key(self, pubkey: str, label: Optional[str] = None) -> Tuple[Obse if len(pk_bytes) == 48: key: ObservationRoot = G1Element.from_bytes(pk_bytes) key_type: KeyTypes = KeyTypes.G1_ELEMENT + elif len(pk_bytes) == 32: + key = VaultRoot.from_bytes(pk_bytes) + key_type = KeyTypes.VAULT_LAUNCHER else: raise ValueError(f"Cannot identify type of pubkey {pubkey}") index = self._get_free_private_key_index() diff --git a/chia/wallet/vault/vault_root.py b/chia/wallet/vault/vault_root.py new file mode 100644 index 000000000000..3f6a3b052341 --- /dev/null +++ b/chia/wallet/vault/vault_root.py @@ -0,0 +1,17 @@ +from __future__ import annotations + + +class VaultRoot: + def __init__(self, launcher_id: bytes): + self.launcher_id = launcher_id + + def get_fingerprint(self) -> int: + # Convert the first four bytes of PK into an integer + return int.from_bytes(self.launcher_id[:4], byteorder="big") + + def __bytes__(self) -> bytes: + return self.launcher_id + + @classmethod + def from_bytes(cls, blob: bytes) -> VaultRoot: + return cls(blob) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py new file mode 100644 index 000000000000..d8e3b29aa645 --- /dev/null +++ b/chia/wallet/vault/vault_wallet.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional, Set, Tuple + +from chia_rs import G1Element, G2Element +from typing_extensions import Unpack + +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.signing_mode import SigningMode +from chia.util.ints import uint64 +from chia.wallet.conditions import Condition +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.payment import Payment +from chia.wallet.signer_protocol import ( + PathHint, + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + SumHint, +) +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.tx_config import TXConfig +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_info import WalletInfo +from chia.wallet.wallet_protocol import GSTOptionalArgs + + +class Vault(Wallet): + @staticmethod + async def create( + wallet_state_manager: Any, + info: WalletInfo, + name: str = __name__, + ) -> Vault: + self = Vault() + self.wallet_state_manager = wallet_state_manager + self.wallet_info = info + self.wallet_id = info.id + self.log = logging.getLogger(name) + return self + + async def get_new_puzzle(self) -> Program: + raise NotImplementedError("vault wallet") + + async def get_new_puzzlehash(self) -> bytes32: + raise NotImplementedError("vault wallet") + + 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, + memos: Optional[List[bytes]] = None, + puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None, + extra_conditions: Tuple[Condition, ...] = tuple(), + **kwargs: Unpack[GSTOptionalArgs], + ) -> List[TransactionRecord]: + raise NotImplementedError("vault wallet") + + def puzzle_for_pk(self, pubkey: G1Element) -> Program: + raise NotImplementedError("vault wallet") + + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: + raise NotImplementedError("vault wallet") + + async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> Tuple[G1Element, G2Element]: + raise NotImplementedError("vault wallet") + + async def get_puzzle_hash(self, new: bool) -> bytes32: + raise NotImplementedError("vault wallet") + + async def apply_signatures( + self, spends: List[Spend], signing_responses: List[SigningResponse] + ) -> SignedTransaction: + raise NotImplementedError("vault wallet") + + async def execute_signing_instructions( + self, signing_instructions: SigningInstructions, partial_allowed: bool = False + ) -> List[SigningResponse]: + raise NotImplementedError("vault wallet") + + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + raise NotImplementedError("vault wallet") + + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + raise NotImplementedError("vault wallet") + + def make_solution( + self, + primaries: List[Payment], + conditions: Tuple[Condition, ...] = tuple(), + fee: uint64 = uint64(0), + ) -> Program: + raise NotImplementedError("vault wallet") + + async def get_puzzle(self, new: bool) -> Program: + raise NotImplementedError("vault wallet") + + def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: + raise ValueError("This won't work") + + def require_derivation_paths(self) -> bool: + raise NotImplementedError("vault wallet") + + async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: + raise NotImplementedError("vault wallet") + + def handle_own_derivation(self) -> bool: + raise NotImplementedError("vault wallet") + + def derivation_for_index(self, index: int) -> List[DerivationRecord]: + raise NotImplementedError("vault wallet") diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index f366514b3523..53152136e31a 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -1,13 +1,19 @@ from __future__ import annotations +import types from hashlib import sha256 +from typing import Awaitable, Callable, Type import pytest -from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey from chia.util.ints import uint64 +from chia.util.observation_root import ObservationRoot from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG +from chia.wallet.vault.vault_root import VaultRoot +from chia.wallet.vault.vault_wallet import Vault +from chia.wallet.wallet_protocol import MainWalletProtocol +from chia.wallet.wallet_state_manager import WalletStateManager from tests.conftest import ConsensusMode from tests.wallet.conftest import WalletTestFramework @@ -15,6 +21,36 @@ SECP_PK = SECP_SK.verifying_key.to_string("compressed") +async def vault_setup(wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch) -> None: + def get_main_wallet_driver(self: WalletStateManager, observation_root: ObservationRoot) -> Type[MainWalletProtocol]: + return Vault + + monkeypatch.setattr( + WalletStateManager, + "get_main_wallet_driver", + types.MethodType(get_main_wallet_driver, WalletStateManager), + ) + + for env in wallet_environments.environments: + SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) + SECP_PK = SECP_SK.verifying_key.to_string("compressed") + client = env.rpc_client + fingerprint = (await client.get_public_keys())[0] + bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] + bls_pk = bytes.fromhex(bls_pk_hex) + timelock = uint64(1000) + entropy = b"101" + res = await client.vault_create(SECP_PK, entropy, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG) + vault_tx = res[0] + assert vault_tx + + eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] + launcher_id = eve_coin.name() + vault_root = VaultRoot.from_bytes(launcher_id) + await env.wallet_node.keychain_proxy.add_public_key(launcher_id.hex()) + await env.restart(vault_root.get_fingerprint()) + + @pytest.mark.parametrize( "wallet_environments", [ @@ -25,26 +61,14 @@ ], indirect=True, ) +@pytest.mark.parametrize("setup_function", [vault_setup]) @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") @pytest.mark.anyio -async def test_vault_creation(wallet_environments: WalletTestFramework) -> None: - # Setup - # full_node_api: FullNodeSimulator = wallet_environments.full_node +async def test_vault_creation( + setup_function: Callable[[WalletTestFramework, pytest.MonkeyPatch], Awaitable[None]], + wallet_environments: WalletTestFramework, + monkeypatch: pytest.MonkeyPatch, +) -> None: + await setup_function(wallet_environments, monkeypatch) env = wallet_environments.environments[0] - # wallet_node = env.wallet_node - # wallet = env.xch_wallet - client = env.rpc_client - - fingerprint = (await client.get_public_keys())[0] - bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] - bls_pk = bytes.fromhex(bls_pk_hex) - - timelock = uint64(1000) - entropy = int_to_bytes(101) - - res = await client.vault_create(SECP_PK, entropy, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG, fee=uint64(10)) - vault_tx = res[0] - assert vault_tx - - eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] - assert eve_coin + assert isinstance(env.xch_wallet, Vault) From 1ab4b24140482fae6a28be8fe2bf6d4e1a7d5a14 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 3 Jan 2024 15:40:11 +1300 Subject: [PATCH 069/274] Tidy up --- chia/util/keychain.py | 11 ++++++++--- chia/wallet/vault/vault_root.py | 6 ++++-- tests/core/util/test_keychain.py | 6 +++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 7b3aad71a5c6..e1bafea3ab9f 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -235,7 +235,10 @@ def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTy raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") -TYPES_TO_KEY_TYPES: Dict[Type[ObservationRoot], KeyTypes] = {G1Element: KeyTypes.G1_ELEMENT} +TYPES_TO_KEY_TYPES: Dict[Type[ObservationRoot], KeyTypes] = { + G1Element: KeyTypes.G1_ELEMENT, + VaultRoot: KeyTypes.VAULT_LAUNCHER, +} KEY_TYPES_TO_TYPES: Dict[KeyTypes, Type[ObservationRoot]] = {v: k for k, v in TYPES_TO_KEY_TYPES.items()} @@ -254,7 +257,7 @@ def observation_root(self) -> ObservationRoot: if self.key_type == KeyTypes.G1_ELEMENT: return G1Element.from_bytes(self.public_key) if self.key_type == KeyTypes.VAULT_LAUNCHER: - return VaultRoot.from_bytes(self.public_key) + return VaultRoot(self.public_key) raise TypeError(f"Invalid key_type {self.key_type}") def __post_init__(self) -> None: @@ -347,7 +350,7 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: fingerprint = observation_root.get_fingerprint() entropy = None key_type = KeyTypes.VAULT_LAUNCHER.value - else: + elif len(pk_bytes) == 48: observation_root = G1Element.from_bytes(pk_bytes) fingerprint = observation_root.get_fingerprint() if len(str_bytes) == G1Element.SIZE + 32: @@ -355,6 +358,8 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: else: entropy = None key_type = KeyTypes.G1_ELEMENT.value + else: + raise ValueError(f"Public key must be either 32 or 48 bytes, got {len(pk_bytes)} bytes") return KeyData( fingerprint=uint32(fingerprint), diff --git a/chia/wallet/vault/vault_root.py b/chia/wallet/vault/vault_root.py index 3f6a3b052341..ea4d60fd727c 100644 --- a/chia/wallet/vault/vault_root.py +++ b/chia/wallet/vault/vault_root.py @@ -1,9 +1,11 @@ from __future__ import annotations +from dataclasses import dataclass + +@dataclass(frozen=True) class VaultRoot: - def __init__(self, launcher_id: bytes): - self.launcher_id = launcher_id + launcher_id: bytes def get_fingerprint(self) -> int: # Convert the first four bytes of PK into an integer diff --git a/tests/core/util/test_keychain.py b/tests/core/util/test_keychain.py index f738528c2ad2..e869d22a2be9 100644 --- a/tests/core/util/test_keychain.py +++ b/tests/core/util/test_keychain.py @@ -33,6 +33,7 @@ mnemonic_to_seed, ) from chia.util.observation_root import ObservationRoot +from chia.wallet.vault.vault_root import VaultRoot mnemonic = ( "rapid this oven common drive ribbon bulb urban uncover napkin kitten usage enforce uncle unveil scene " @@ -481,8 +482,11 @@ def test_key_type_support(key_type: str) -> None: The purpose of this test is to make sure that whenever KeyTypes is updated, all relevant functionality is also updated with it. """ + launcher_id = bytes32(b"1" * 32) + vault_root = VaultRoot(launcher_id) generate_test_key_for_key_type: Dict[str, Tuple[int, bytes, ObservationRoot]] = { - KeyTypes.G1_ELEMENT.value: (G1Element().get_fingerprint(), bytes(G1Element()), G1Element()) + KeyTypes.G1_ELEMENT.value: (G1Element().get_fingerprint(), bytes(G1Element()), G1Element()), + KeyTypes.VAULT_LAUNCHER.value: (uint32(vault_root.get_fingerprint()), vault_root.launcher_id, vault_root), } obr_fingerprint, obr_bytes, obr = generate_test_key_for_key_type[key_type] assert KeyData(uint32(obr_fingerprint), obr_bytes, None, None, key_type).observation_root == obr From 18dbab34b1bce3f615a3c724d5b65677d7efe59e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jan 2024 07:53:29 -0800 Subject: [PATCH 070/274] Address test coverage --- chia/cmds/keys_funcs.py | 12 ++++++++---- chia/daemon/server.py | 2 +- chia/util/keychain.py | 6 ++---- tests/core/util/test_keychain.py | 1 + 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index cbc5b6c5ed2a..1dae49f39332 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -167,7 +167,8 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["fingerprint"] = key_data.fingerprint if isinstance(key_data.observation_root, G1Element): key["master_pk"] = key_data.public_key.hex() - else: + else: # pragma: no cover + # TODO: Add test coverage once vault wallet exists key["observation_root"] = key_data.public_key.hex() if sk is not None: key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() @@ -514,7 +515,8 @@ def search_derive( if isinstance(master_key_data.observation_root, G1Element): public_keys = [master_key_data.observation_root] private_keys = [master_key_data.private_key if master_key_data.secrets is not None else None] - else: + else: # pragma: no cover + # TODO: Add test coverage once vault wallet exists print("Cannot currently derive paths from non-BLS keys") return True @@ -649,7 +651,8 @@ def derive_wallet_address( """ if fingerprint is not None: key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=non_observer_derivation) - if not isinstance(key_data.observation_root, G1Element): + if not isinstance(key_data.observation_root, G1Element): # pragma: no cover + # TODO: Add test coverage once vault wallet exists print("Cannot currently derive from non-BLS keys") return if non_observer_derivation and key_data.secrets is None: @@ -716,7 +719,8 @@ def derive_child_key( if fingerprint is not None: key_data: KeyData = Keychain().get_key(fingerprint, include_secrets=True) - if not isinstance(key_data.observation_root, G1Element): + if not isinstance(key_data.observation_root, G1Element): # pragma: no cover + # TODO: Add coverage when vault wallet exists print("Cannot currently derive from non-BLS keys") return current_pk: G1Element = key_data.observation_root diff --git a/chia/daemon/server.py b/chia/daemon/server.py index dfae72ccdc44..27d69cf103e0 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -632,7 +632,7 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict wallet_addresses_by_fingerprint = {} for key in keys: if not isinstance(key.observation_root, G1Element): - continue + continue # pragma: no cover address_entries = [] # we require access to the private key to generate wallet addresses for non observer diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 9a39beafa9d1..1de50a355ba5 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -240,9 +240,7 @@ class KeyData(Streamable): @cached_property def observation_root(self) -> ObservationRoot: - if self.key_type == KeyTypes.G1_ELEMENT: - return G1Element.from_bytes(self.public_key) - raise TypeError(f"Invalid key_type {self.key_type}") + return KeyTypes.parse_observation_root(self.public_key, KeyTypes(self.key_type)) def __post_init__(self) -> None: # This is redundant if `from_*` methods are used but its to make sure there can't be an `KeyData` instance with @@ -399,7 +397,7 @@ def add_public_key(self, pubkey: str, label: Optional[str] = None) -> Tuple[Obse key: ObservationRoot = G1Element.from_bytes(pk_bytes) key_type: KeyTypes = KeyTypes.G1_ELEMENT else: - raise ValueError(f"Cannot identify type of pubkey {pubkey}") + raise ValueError(f"Cannot identify type of pubkey {pubkey}") # pragma: no cover index = self._get_free_private_key_index() fingerprint = key.get_fingerprint() diff --git a/tests/core/util/test_keychain.py b/tests/core/util/test_keychain.py index 96545ea4df4e..6317c65eb736 100644 --- a/tests/core/util/test_keychain.py +++ b/tests/core/util/test_keychain.py @@ -483,3 +483,4 @@ def test_key_type_support(key_type: str) -> None: } obr_fingerprint, obr_bytes, obr = generate_test_key_for_key_type[key_type] assert KeyData(uint32(obr_fingerprint), obr_bytes, None, None, key_type).observation_root == obr + assert KeyTypes.parse_observation_root(obr_bytes, KeyTypes(key_type)) == obr From 2cc43829840858752f94452a6cfeae5ab8193dc7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jan 2024 08:06:22 -0800 Subject: [PATCH 071/274] Add a coverage ignore --- chia/wallet/wallet_state_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 197b14c6525b..c4b67abf36d6 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -365,7 +365,8 @@ async def create( return self def get_public_key_unhardened(self, index: uint32) -> G1Element: - if not isinstance(self.observation_root, G1Element): + if not isinstance(self.observation_root, G1Element): # pragma: no cover + # TODO: Add test coverage when vault wallet exists raise ValueError("Public key derivation is not supported for non-G1Element keys") return master_pk_to_wallet_pk_unhardened(self.observation_root, index) From 183314491bc536a980d6fecf6ff535093283dadb Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jan 2024 10:03:42 -0800 Subject: [PATCH 072/274] Address test coverage --- chia/data_layer/data_layer_wallet.py | 2 +- chia/pools/pool_wallet.py | 4 ++-- chia/wallet/cat_wallet/cat_wallet.py | 2 +- chia/wallet/cat_wallet/dao_cat_wallet.py | 2 +- chia/wallet/dao_wallet/dao_wallet.py | 4 ++-- chia/wallet/did_wallet/did_wallet.py | 2 +- chia/wallet/nft_wallet/nft_wallet.py | 4 ++-- chia/wallet/vc_wallet/vc_wallet.py | 4 ++-- chia/wallet/wallet.py | 2 +- chia/wallet/wallet_node.py | 10 +++++--- chia/wallet/wallet_state_manager.py | 7 +++--- tests/wallet/rpc/test_wallet_rpc.py | 2 ++ tests/wallet/test_main_wallet_protocol.py | 29 ++++++++++++++--------- tests/wallet/test_wallet_node.py | 26 ++++++++++++++++++++ 14 files changed, 69 insertions(+), 31 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index bb88521ca208..f1ee477279b8 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -1338,7 +1338,7 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: def handle_own_derivation(self) -> bool: return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 56e2e13d1953..c15012733b6a 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -976,8 +976,8 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: # pragma: no cover return False # PoolWallet pre-dates hints - def handle_own_derivation(self) -> bool: + def handle_own_derivation(self) -> bool: # pragma: no cover return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index e5994feaeea5..24900c2aa6ae 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -885,5 +885,5 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: def handle_own_derivation(self) -> bool: return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index 63c993448a4c..6ca262bc65eb 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -675,5 +675,5 @@ def get_name(self) -> str: def handle_own_derivation(self) -> bool: return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/dao_wallet/dao_wallet.py b/chia/wallet/dao_wallet/dao_wallet.py index e8119d43cfa0..329b6e1ba547 100644 --- a/chia/wallet/dao_wallet/dao_wallet.py +++ b/chia/wallet/dao_wallet/dao_wallet.py @@ -2108,8 +2108,8 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 return True - def handle_own_derivation(self) -> bool: + def handle_own_derivation(self) -> bool: # pragma: no cover return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index df2abc7bd6e5..bc3e6cac7db1 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -1465,5 +1465,5 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: def handle_own_derivation(self) -> bool: return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 23c7ec5af876..fc2ec86b6c6b 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -1690,8 +1690,8 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return False - def handle_own_derivation(self) -> bool: + def handle_own_derivation(self) -> bool: # pragma: no cover return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index ab272e03207b..2f5b058bfc35 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -639,10 +639,10 @@ def get_name(self) -> str: async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: return False - def handle_own_derivation(self) -> bool: + def handle_own_derivation(self) -> bool: # pragma: no cover return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index c0469a80e5ed..d38001310003 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -651,5 +651,5 @@ async def apply_signatures( def handle_own_derivation(self) -> bool: return False - def derivation_for_index(self, index: int) -> List[DerivationRecord]: + def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 1d827e13aaca..49b68073959f 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -387,9 +387,7 @@ async def _start_with_fingerprint( # got Future attached to a different loop self._new_peak_queue = NewPeakQueue(inner_queue=asyncio.PriorityQueue()) if not fingerprint: - fingerprint = self.get_last_used_fingerprint() - if fingerprint is not None and await self.get_key_for_fingerprint(fingerprint) is None: - fingerprint = None + fingerprint = await self.get_last_used_fingerprint_if_exists() multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log) multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self._weight_proof_handler = WalletWeightProofHandler(self.constants, multiprocessing_context) @@ -675,6 +673,12 @@ def get_last_used_fingerprint(self) -> Optional[int]: self.log.exception("Non-fatal: Unable to read last used fingerprint.") return fingerprint + async def get_last_used_fingerprint_if_exists(self) -> Optional[int]: + fingerprint = self.get_last_used_fingerprint() + if fingerprint is not None and await self.get_key_for_fingerprint(fingerprint) is None: + fingerprint = None + return fingerprint + def get_last_used_fingerprint_path(self) -> Path: db_path: Path = path_from_root(self.root_path, self.config["database_path"]) fingerprint_path = db_path.parent / "last_used_fingerprint" diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index c317cfd08ceb..c0836a06edcf 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -369,7 +369,9 @@ def get_main_wallet_driver(self, observation_root: ObservationRoot) -> Type[Main if len(root_bytes) == 48: return Wallet - raise ValueError(f"Could not find a valid wallet type for observation_root: {root_bytes.hex()}") + raise ValueError( # pragma: no cover + f"Could not find a valid wallet type for observation_root: {root_bytes.hex()}" + ) def get_public_key_unhardened(self, index: uint32) -> G1Element: if not isinstance(self.observation_root, G1Element): @@ -466,9 +468,6 @@ async def create_more_puzzle_hashes( if target_wallet.handle_own_derivation(): derivation_paths.extend(target_wallet.derivation_for_index(index)) else: - if target_wallet.type() == WalletType.POOLING_WALLET: - continue - if self.private_key is not None: # Hardened pubkey: G1Element = _derive_path(intermediate_sk, [index]).get_g1() diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 73972bac64d1..f7b48d02b70e 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1672,6 +1672,8 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn sk_dict = await client.get_private_key(pks[1]) assert sk_dict["fingerprint"] == pks[1] + assert not (await client.log_in(1234567890))["success"] + # Add in reward addresses into farmer and pool for testing delete key checks # set farmer to first private key sk = await wallet_node.get_key_for_fingerprint(pks[0], private=True) diff --git a/tests/wallet/test_main_wallet_protocol.py b/tests/wallet/test_main_wallet_protocol.py index a647cc627b33..d5c8bfed2698 100644 --- a/tests/wallet/test_main_wallet_protocol.py +++ b/tests/wallet/test_main_wallet_protocol.py @@ -57,10 +57,10 @@ async def create( self.log = logging.getLogger(name) return self - async def get_new_puzzle(self) -> Program: + async def get_new_puzzle(self) -> Program: # pragma: no cover return ACS - async def get_new_puzzlehash(self) -> bytes32: + async def get_new_puzzlehash(self) -> bytes32: # pragma: no cover return ACS_PH async def generate_signed_transaction( @@ -124,17 +124,17 @@ async def generate_signed_transaction( ) ] - def puzzle_for_pk(self, pubkey: G1Element) -> Program: + def puzzle_for_pk(self, pubkey: G1Element) -> Program: # pragma: no cover raise ValueError("This won't work") async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: if puzzle_hash == ACS_PH: return ACS else: - raise ValueError("puzzle hash was not ACS_PH") + raise ValueError("puzzle hash was not ACS_PH") # pragma: no cover async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> Tuple[G1Element, G2Element]: - raise ValueError("This won't work") + raise ValueError("This won't work") # pragma: no cover async def get_puzzle_hash(self, new: bool) -> bytes32: return ACS_PH @@ -151,14 +151,14 @@ async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: if len(signing_instructions.targets) > 0: - raise ValueError("This won't work") + raise ValueError("This won't work") # pragma: no cover else: return [] - async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: # pragma: no cover return None - async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: # pragma: no cover return None def make_solution( @@ -173,16 +173,16 @@ def make_solution( prog: Program = Program.to([c.to_program() for c in condition_list]) return prog - async def get_puzzle(self, new: bool) -> Program: + async def get_puzzle(self, new: bool) -> Program: # pragma: no cover return ACS - def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: + def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: # pragma: no cover raise ValueError("This won't work") def require_derivation_paths(self) -> bool: return True - async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: + async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: # pragma: no cover if coin.puzzle_hash == ACS_PH or hint == ACS_PH: return True else: @@ -299,3 +299,10 @@ async def test_main_wallet( ) ] ) + + # Miscellaneous checks + assert [coin.puzzle_hash for tx in txs for coin in tx.removals] == [ + (await main_wallet.puzzle_for_puzzle_hash(coin.puzzle_hash)).get_tree_hash() + for tx in txs + for coin in tx.removals + ] diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index bc45f38740ba..4fead907d682 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -623,3 +623,29 @@ def check_wallet_cache_empty() -> bool: # Disconnect from the peer to make sure their entry in the cache is also deleted await simulator_and_wallet[1][0][0]._server.get_connections()[0].close(120) await time_out_assert(5, check_wallet_cache_empty, True) + + +@pytest.mark.anyio +async def test_get_last_used_fingerprint_if_exists( + self_hostname: str, simulator_and_wallet: SimulatorsAndWallets +) -> None: + [full_node_api], [(node, wallet_server)], _ = simulator_and_wallet + + await wallet_server.start_client(PeerInfo(self_hostname, full_node_api.server.get_port()), None) + + node.update_last_used_fingerprint() + assert node.wallet_state_manager.private_key is not None + assert ( + await node.get_last_used_fingerprint_if_exists() + == node.wallet_state_manager.private_key.get_g1().get_fingerprint() + ) + await node.keychain_proxy.delete_all_keys() + assert await node.get_last_used_fingerprint_if_exists() is None + + sk_2: PrivateKey = await node.keychain_proxy.add_private_key(generate_mnemonic()) + fingerprint_2: int = sk_2.get_g1().get_fingerprint() + + node._close() + await node._await_closed(shutting_down=False) + await node._start_with_fingerprint() + assert node.logged_in_fingerprint == fingerprint_2 From d8cdebe808d7a9f57f0d120f4513ee97bdbc484b Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 3 Jan 2024 10:42:07 -0800 Subject: [PATCH 073/274] test ignore --- chia/util/keychain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 1de50a355ba5..834710b56e0a 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -224,7 +224,8 @@ class KeyTypes(str, Enum): def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: if key_type == cls.G1_ELEMENT: return G1Element.from_bytes(pk_bytes) - else: + else: # pragma: no cover + # mypy should prevent this from ever running raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") From 54a9ef755da4bb4c2cf4e083707faf70f848d76d Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 4 Jan 2024 16:00:20 +1300 Subject: [PATCH 074/274] quex's comments --- chia/rpc/wallet_rpc_api.py | 70 +++------------------- chia/rpc/wallet_rpc_client.py | 4 +- chia/wallet/vault/vault_drivers.py | 14 +++-- chia/wallet/wallet_state_manager.py | 78 ++++++++++++++++++++++++- tests/wallet/vault/test_vault_wallet.py | 33 +++++++++-- 5 files changed, 124 insertions(+), 75 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 7200cd3107a7..2d1cc4a19be8 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -3,7 +3,6 @@ import dataclasses import json import logging -import time import zlib from pathlib import Path from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Union @@ -51,7 +50,6 @@ AssertCoinAnnouncement, AssertPuzzleAnnouncement, Condition, - ConditionValidTimes, CreateCoinAnnouncement, CreatePuzzleAnnouncement, ) @@ -91,10 +89,8 @@ from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings, ClawbackMetadata from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key from chia.wallet.singleton import ( - SINGLETON_LAUNCHER_PUZZLE, SINGLETON_LAUNCHER_PUZZLE_HASH, create_singleton_puzzle, - create_singleton_puzzle_hash, get_inner_puzzle_from_singleton, ) from chia.wallet.trade_record import TradeRecord @@ -109,7 +105,7 @@ from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import CoinType, WalletType -from chia.wallet.vault.vault_drivers import get_vault_puzzle_hash +from chia.wallet.vault.vault_drivers import get_vault_hidden_puzzle_with_index from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_store import VCProofs @@ -4508,7 +4504,7 @@ class CRCATApprovePending(Streamable): ########################################################################################## # VAULT ########################################################################################## - @tx_endpoint(push=True) + @tx_endpoint(push=False) async def vault_create( self, request: Dict[str, Any], @@ -4519,66 +4515,16 @@ async def vault_create( Create a new vault """ assert self.service.wallet_state_manager - wallet = self.service.wallet_state_manager.main_wallet - secp_pk = bytes.fromhex(str(request.get("secp_pk"))) - entropy = bytes.fromhex(str(request.get("entropy"))) + hp_index = request.get("hp_index", 0) + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash() bls_pk = G1Element.from_bytes(bytes.fromhex(str(request.get("bls_pk")))) timelock = uint64(request["timelock"]) - fee = request.get("fee", 0) - vault_puzzlehash = get_vault_puzzle_hash( - secp_pk, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, entropy, bls_pk, timelock - ) - - # Get xch coin - amount = uint64(1) - coins = await wallet.select_coins(uint64(amount + fee), tx_config.coin_selection_config) - - # Create singleton launcher - origin = coins.copy().pop() - launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_PUZZLE_HASH, amount) - - vault_full_puzzlehash = create_singleton_puzzle_hash(vault_puzzlehash, launcher_coin.name()) - announcement_message = Program.to([vault_puzzlehash, amount, bytes(0x80)]).get_tree_hash() - - [tx_record] = await wallet.generate_signed_transaction( - amount, - SINGLETON_LAUNCHER_PUZZLE_HASH, - tx_config, - fee, - coins, - None, - origin_id=origin.name(), - extra_conditions=( - AssertCoinAnnouncement(asserted_id=launcher_coin.name(), asserted_msg=announcement_message), - ), - ) + fee = uint64(request.get("fee", 0)) + genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE - genesis_launcher_solution = Program.to([vault_puzzlehash, amount, bytes(0x80)]) - - launcher_cs = CoinSpend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution) - launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) - assert tx_record.spend_bundle is not None - full_spend = SpendBundle.aggregate([tx_record.spend_bundle, launcher_sb]) - - vault_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - amount=uint64(amount), - to_puzzle_hash=vault_full_puzzlehash, - fee_amount=fee, - confirmed=False, - sent=uint32(0), - spend_bundle=full_spend, - additions=full_spend.additions(), - removals=full_spend.removals(), - wallet_id=wallet.id(), - sent_to=[], - trade_id=None, - type=uint32(TransactionType.INCOMING_TX.value), - name=full_spend.name(), - memos=[], - valid_times=ConditionValidTimes(), + vault_record = await self.service.wallet_state_manager.create_vault_wallet( + secp_pk, hidden_puzzle_hash, bls_pk, timelock, genesis_challenge, tx_config, fee=fee ) return { diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 5c576b034c94..5fb6eff6f2a0 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1685,7 +1685,7 @@ async def crcat_approve_pending( async def vault_create( self, secp_pk: bytes, - entropy: bytes, + hp_index: uint32, bls_pk: bytes, timelock: uint64, tx_config: TXConfig, @@ -1696,7 +1696,7 @@ async def vault_create( "vault_create", { "secp_pk": secp_pk.hex(), - "entropy": entropy.hex(), + "hp_index": hp_index, "bls_pk": bls_pk.hex(), "timelock": timelock, "fee": fee, diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index be03e33232c2..204b50b86085 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -4,8 +4,9 @@ from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE from chia.wallet.util.merkle_tree import MerkleTree # MODS @@ -36,7 +37,12 @@ def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: byte return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root()) -def get_vault_puzzle( +def get_vault_hidden_puzzle_with_index(index: uint32, hidden_puzzle: Program = DEFAULT_HIDDEN_PUZZLE) -> Program: + hidden_puzzle_with_index: Program = Program.to([6, (index, hidden_puzzle)]) + return hidden_puzzle_with_index + + +def get_vault_inner_puzzle( secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64 ) -> Program: secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, entropy).get_tree_hash() @@ -44,10 +50,10 @@ def get_vault_puzzle( return construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) -def get_vault_puzzle_hash( +def get_vault_inner_puzzle_hash( secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64 ) -> bytes32: - vault_puzzle = get_vault_puzzle(secp_pk, genesis_challenge, entropy, bls_pk, timelock) + vault_puzzle = get_vault_inner_puzzle(secp_pk, genesis_challenge, entropy, bls_pk, timelock) vault_puzzle_hash: bytes32 = vault_puzzle.get_tree_hash() return vault_puzzle_hash diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 3193178aeffc..ae7bbafd0de3 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -25,7 +25,7 @@ ) import aiosqlite -from chia_rs import G1Element, G2Element, PrivateKey +from chia_rs import AugSchemeMPL, 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 @@ -110,7 +110,12 @@ 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.singleton import ( + SINGLETON_LAUNCHER_PUZZLE, + 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 @@ -128,6 +133,7 @@ last_change_height_cs, ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType +from chia.wallet.vault.vault_drivers import get_vault_inner_puzzle_hash 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 @@ -2533,3 +2539,71 @@ async def sign_transaction(self, coin_spends: List[CoinSpend]) -> SpendBundle: self.constants.MAX_BLOCK_COST_CLVM, [puzzle_hash_for_synthetic_public_key], ) + + async def create_vault_wallet( + self, + secp_pk: bytes, + hidden_puzzle_hash: bytes32, + bls_pk: G1Element, + timelock: uint64, + genesis_challenge: bytes32, + tx_config: TXConfig, + fee: uint64 = uint64(0), + ) -> TransactionRecord: + """ + Returns a tx record for creating a new vault + """ + wallet = self.main_wallet + vault_inner_puzzle_hash = get_vault_inner_puzzle_hash( + secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock + ) + # Get xch coin + amount = uint64(1) + coins = await wallet.select_coins(uint64(amount + fee), tx_config.coin_selection_config) + + # Create singleton launcher + origin = next(iter(coins)) + launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_HASH, amount) + + genesis_launcher_solution = Program.to([vault_inner_puzzle_hash, amount, None]) + announcement_message = genesis_launcher_solution.get_tree_hash() + + [tx_record] = await wallet.generate_signed_transaction( + amount, + SINGLETON_LAUNCHER_HASH, + tx_config, + fee, + coins, + None, + origin_id=origin.name(), + memos=[secp_pk, hidden_puzzle_hash], + extra_conditions=( + AssertCoinAnnouncement(asserted_id=launcher_coin.name(), asserted_msg=announcement_message), + ), + ) + + launcher_cs = CoinSpend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution) + launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) + assert tx_record.spend_bundle is not None + full_spend = SpendBundle.aggregate([tx_record.spend_bundle, launcher_sb]) + + vault_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + amount=uint64(amount), + to_puzzle_hash=vault_inner_puzzle_hash, + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=full_spend, + additions=full_spend.additions(), + removals=full_spend.removals(), + wallet_id=wallet.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=full_spend.name(), + memos=[], + valid_times=ConditionValidTimes(), + ) + return vault_record diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 9856e230f32d..745db7ac2636 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -3,13 +3,12 @@ from hashlib import sha256 import pytest -from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from tests.conftest import ConsensusMode -from tests.wallet.conftest import WalletTestFramework +from tests.wallet.conftest import WalletStateTransition, WalletTestFramework SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") @@ -37,11 +36,35 @@ async def test_vault_creation(wallet_environments: WalletTestFramework) -> None: bls_pk = bytes.fromhex(bls_pk_hex) timelock = uint64(1000) - entropy = int_to_bytes(101) + hp_index = uint32(1) - res = await client.vault_create(SECP_PK, entropy, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG, fee=uint64(10)) + res = await client.vault_create(SECP_PK, hp_index, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG, fee=uint64(10)) vault_tx = res[0] assert vault_tx eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] assert eve_coin + + await env.wallet_state_manager.add_pending_transactions(res) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "unconfirmed_wallet_balance": -11, # 1 for vault singleton, 10 for fee + "pending_coin_removal_count": 2, + "<=#spendable_balance": -11, + "<=#max_send_amount": -11, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -11, + "set_remainder": True, + } + }, + ) + ] + ) From 609bbfa4badb5a9299200f01a47f09224f5e9556 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 5 Jan 2024 07:56:17 +1300 Subject: [PATCH 075/274] add memos to launcher solution --- chia/wallet/wallet_state_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index ae7bbafd0de3..3bec6d1fa8d0 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -2565,7 +2565,7 @@ async def create_vault_wallet( origin = next(iter(coins)) launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_HASH, amount) - genesis_launcher_solution = Program.to([vault_inner_puzzle_hash, amount, None]) + genesis_launcher_solution = Program.to([vault_inner_puzzle_hash, amount, [secp_pk, hidden_puzzle_hash]]) announcement_message = genesis_launcher_solution.get_tree_hash() [tx_record] = await wallet.generate_signed_transaction( @@ -2576,7 +2576,6 @@ async def create_vault_wallet( coins, None, origin_id=origin.name(), - memos=[secp_pk, hidden_puzzle_hash], extra_conditions=( AssertCoinAnnouncement(asserted_id=launcher_coin.name(), asserted_msg=announcement_message), ), From d1c4f8a90163372b4ab64a9fafdcb89f55bd8176 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jan 2024 12:20:00 -0800 Subject: [PATCH 076/274] merge test fix --- chia/daemon/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 0132c514a00c..5a27b8921d72 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -664,6 +664,8 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: Dic keys_for_plot: Dict[uint32, Any] = {} for key in keys: + if key.secrets is None: + continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() pool_public_key: G1Element = master_sk_to_pool_sk(sk).get_g1() From 55be705675cc560a5deadf64123962417a463960 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jan 2024 13:03:46 -0800 Subject: [PATCH 077/274] bad merge --- tests/core/daemon/test_keychain_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/daemon/test_keychain_proxy.py b/tests/core/daemon/test_keychain_proxy.py index 41b13962ce92..64072572b5cf 100644 --- a/tests/core/daemon/test_keychain_proxy.py +++ b/tests/core/daemon/test_keychain_proxy.py @@ -84,8 +84,8 @@ async def test_get_key_for_fingerprint(keychain_proxy: KeychainProxy) -> None: with pytest.raises(KeychainIsEmpty): await keychain.get_public_key_for_fingerprint(None) await keychain_proxy.add_private_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) - assert await keychain.get_public_key_for_fingerprint(TEST_KEY_1.fingerprint) == TEST_KEY_1.public_key - assert await keychain.get_public_key_for_fingerprint(None) == TEST_KEY_1.public_key + assert await keychain.get_public_key_for_fingerprint(TEST_KEY_1.fingerprint) == TEST_KEY_1.observation_root + assert await keychain.get_public_key_for_fingerprint(None) == TEST_KEY_1.observation_root with pytest.raises(KeychainKeyNotFound): await keychain.get_public_key_for_fingerprint(1234567890) From 8bb83bb234441216ad5e1fb9f918671858f01640 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 4 Jan 2024 14:14:34 -0800 Subject: [PATCH 078/274] Fix test --- chia/daemon/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 27d69cf103e0..89bdd87a0e3f 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -666,6 +666,8 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: Dic keys_for_plot: Dict[uint32, Any] = {} for key in keys: + if key.secrets is None: + continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() pool_public_key: G1Element = master_sk_to_pool_sk(sk).get_g1() From 5d286ef1e7700dc1dc225c595569f7369cbdb18e Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 5 Jan 2024 16:30:19 +1300 Subject: [PATCH 079/274] sync vault singleton --- chia/wallet/vault/vault_info.py | 16 +++++++ chia/wallet/vault/vault_wallet.py | 57 ++++++++++++++++++++++++- tests/wallet/vault/test_vault_wallet.py | 25 ++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 chia/wallet/vault/vault_info.py diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py new file mode 100644 index 000000000000..55a7c617607e --- /dev/null +++ b/chia/wallet/vault/vault_info.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.streamable import Streamable, streamable + + +@streamable +@dataclass(frozen=True) +class VaultInfo(Streamable): + coin: Coin + launcher_id: bytes32 + pubkey: bytes + hidden_puzzle_hash: bytes32 diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index d8e3b29aa645..aaa5f3ae600b 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -1,16 +1,19 @@ from __future__ import annotations +import dataclasses +import json import logging from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element from typing_extensions import Unpack +from chia.protocols.wallet_protocol import CoinState 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.signing_mode import SigningMode -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 from chia.wallet.conditions import Condition from chia.wallet.derivation_record import DerivationRecord from chia.wallet.payment import Payment @@ -24,6 +27,9 @@ ) from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import TXConfig +from chia.wallet.util.wallet_sync_utils import fetch_coin_spend +from chia.wallet.vault.vault_info import VaultInfo +from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.wallet import Wallet from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_protocol import GSTOptionalArgs @@ -107,7 +113,7 @@ def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: raise ValueError("This won't work") def require_derivation_paths(self) -> bool: - raise NotImplementedError("vault wallet") + return False async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: raise NotImplementedError("vault wallet") @@ -117,3 +123,50 @@ def handle_own_derivation(self) -> bool: def derivation_for_index(self, index: int) -> List[DerivationRecord]: raise NotImplementedError("vault wallet") + + async def sync_singleton(self) -> None: + wallet_node: Any = self.wallet_state_manager.wallet_node + peer = wallet_node.get_full_node_peer() + assert peer is not None + + assert isinstance(self.wallet_state_manager.observation_root, VaultRoot) + launcher_id = bytes32(self.wallet_state_manager.observation_root.launcher_id) + + coin_states = await wallet_node.get_coin_state([launcher_id], peer) + if not coin_states: + raise ValueError(f"No coin found for launcher id: {launcher_id}.") + coin_state: CoinState = coin_states[0] + parent_state: CoinState = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] + assert parent_state.spent_height is not None + launcher_spend = await fetch_coin_spend(uint32(parent_state.spent_height), parent_state.coin, peer) + launcher_solution = launcher_spend.solution.to_program() + + secp_pk = launcher_solution.at("rrff").as_atom() + hidden_puzzle_hash = bytes32(launcher_solution.at("rrfrf").as_atom()) + vault_info = VaultInfo(coin_state.coin, launcher_id, secp_pk, hidden_puzzle_hash) + + if coin_state.spent_height: + while coin_state.spent_height is not None: + coin_states = await wallet_node.fetch_children(coin_state.coin.name(), peer=peer) + odd_coin = None + for coin in coin_states: + 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.") + parent_state = coin_state + coin_state = odd_coin + + vault_info = dataclasses.replace(vault_info, coin=coin_state.coin) + await self.save_info(vault_info) + + async def save_info(self, vault_info: VaultInfo) -> None: + self.vault_info = vault_info + current_info = self.wallet_info + data_str = json.dumps(vault_info.to_json_dict()) + wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) + self.wallet_info = wallet_info + # TODO: push new info to user store + # await self.wallet_state_manager.user_store.update_wallet(wallet_info) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 97146a504f72..3540e7d9486d 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -16,7 +16,7 @@ from chia.wallet.wallet_protocol import MainWalletProtocol from chia.wallet.wallet_state_manager import WalletStateManager from tests.conftest import ConsensusMode -from tests.wallet.conftest import WalletTestFramework +from tests.wallet.conftest import WalletStateTransition, WalletTestFramework SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") @@ -48,8 +48,27 @@ def get_main_wallet_driver(self: WalletStateManager, observation_root: Observati eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] launcher_id = eve_coin.name() vault_root = VaultRoot.from_bytes(launcher_id) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -1, + "set_remainder": True, + } + }, + ) + ] + ) await env.wallet_node.keychain_proxy.add_public_key(launcher_id.hex()) await env.restart(vault_root.get_fingerprint()) + await wallet_environments.full_node.wait_for_wallet_synced(env.wallet_node, 20) @pytest.mark.parametrize( @@ -73,3 +92,7 @@ async def test_vault_creation( await setup_function(wallet_environments, monkeypatch) env = wallet_environments.environments[0] assert isinstance(env.xch_wallet, Vault) + + wallet: Vault = env.xch_wallet + await wallet.sync_singleton() + assert wallet.vault_info From 5c5f025052e075fab5794af5251b49b60dbbb187 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Jan 2024 09:30:50 -0800 Subject: [PATCH 080/274] Fix test --- chia/daemon/server.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 27d69cf103e0..89bdd87a0e3f 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -666,6 +666,8 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: Dic keys_for_plot: Dict[uint32, Any] = {} for key in keys: + if key.secrets is None: + continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() pool_public_key: G1Element = master_sk_to_pool_sk(sk).get_g1() From 9c3ca42fad08aa7c8dd9fa1e982ec7d67a2a3384 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Jan 2024 09:55:45 -0800 Subject: [PATCH 081/274] CoinSpend -> make_spend --- tests/wallet/test_main_wallet_protocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/wallet/test_main_wallet_protocol.py b/tests/wallet/test_main_wallet_protocol.py index 11b824337b2b..c19036c704ed 100644 --- a/tests/wallet/test_main_wallet_protocol.py +++ b/tests/wallet/test_main_wallet_protocol.py @@ -12,7 +12,7 @@ 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_spend import CoinSpend +from chia.types.coin_spend import make_spend from chia.types.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64 @@ -91,7 +91,7 @@ async def generate_signed_transaction( spend_bundle = SpendBundle( [ - CoinSpend( + make_spend( coin, ACS, self.make_solution(condition_list, extra_conditions, fee) if i == 0 else Program.to([]), From 52ecbf27af5608dabac2e1ead2fb80fbb5f4597f Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sat, 6 Jan 2024 11:18:49 +1300 Subject: [PATCH 082/274] fix test framework imports and use make_spend for CoinSpend --- chia/wallet/wallet_state_manager.py | 4 ++-- tests/wallet/vault/test_vault_wallet.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index a0d0d68b1c92..025927a6d27d 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -47,7 +47,7 @@ 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.coin_spend import CoinSpend, compute_additions, make_spend from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash @@ -2595,7 +2595,7 @@ async def create_vault_wallet( ), ) - launcher_cs = CoinSpend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution) + launcher_cs = make_spend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution) launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) assert tx_record.spend_bundle is not None full_spend = SpendBundle.aggregate([tx_record.spend_bundle, launcher_sb]) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 745db7ac2636..8c686dbb888b 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -8,7 +8,7 @@ from chia.util.ints import uint32, uint64 from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from tests.conftest import ConsensusMode -from tests.wallet.conftest import WalletStateTransition, WalletTestFramework +from tests.environments.wallet import WalletStateTransition, WalletTestFramework SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") From 1e1610c47be041f8e8cf629224441e5a27362323 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 8 Jan 2024 07:48:25 -0800 Subject: [PATCH 083/274] Repin hsms --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9fbef935805d..5a081af4b211 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ "zstd==1.5.5.1", "packaging==23.2", "psutil==5.9.4", - "quex-hsms==0.1.dev157", + "hsms==0.3.0", "ecdsa==0.18.0", # For SECP ] From 3f2383c55156abe357123b2b794d7b9ee9bcbe00 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 9 Jan 2024 16:19:28 +1300 Subject: [PATCH 084/274] create derivation records and puzzlehashes --- chia/wallet/vault/vault_wallet.py | 42 +++++++++++++++++++++++++----- chia/wallet/wallet_puzzle_store.py | 3 ++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index aaa5f3ae600b..112df49478e2 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -8,6 +8,7 @@ from chia_rs import G1Element, G2Element from typing_extensions import Unpack +from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program @@ -28,6 +29,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend +from chia.wallet.vault.vault_drivers import construct_p2_delegated_secp, get_vault_hidden_puzzle_with_index from chia.wallet.vault.vault_info import VaultInfo from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.wallet import Wallet @@ -50,10 +52,13 @@ async def create( return self async def get_new_puzzle(self) -> Program: - raise NotImplementedError("vault wallet") + dr = await self.wallet_state_manager.get_unused_derivation_record(self.id()) + puzzle = construct_p2_delegated_secp(dr.pubkey, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, dr.puzzle_hash) + return puzzle async def get_new_puzzlehash(self) -> bytes32: - raise NotImplementedError("vault wallet") + puzzle = await self.get_new_puzzle() + return puzzle.get_tree_hash() async def generate_signed_transaction( self, @@ -80,7 +85,15 @@ async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMo raise NotImplementedError("vault wallet") async def get_puzzle_hash(self, new: bool) -> bytes32: - raise NotImplementedError("vault wallet") + 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 apply_signatures( self, spends: List[Spend], signing_responses: List[SigningResponse] @@ -107,22 +120,38 @@ def make_solution( raise NotImplementedError("vault wallet") async def get_puzzle(self, new: bool) -> Program: - raise NotImplementedError("vault wallet") + 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() + puzzle = construct_p2_delegated_secp(record.pubkey, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, record.puzzle_hash) + return puzzle def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: raise ValueError("This won't work") def require_derivation_paths(self) -> bool: + if getattr(self, "vault_info", None): + return True return False async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: raise NotImplementedError("vault wallet") def handle_own_derivation(self) -> bool: - raise NotImplementedError("vault wallet") + return True def derivation_for_index(self, index: int) -> List[DerivationRecord]: - raise NotImplementedError("vault wallet") + hidden_puzzle = get_vault_hidden_puzzle_with_index(uint32(index)) + hidden_puzzle_hash = hidden_puzzle.get_tree_hash() + record = DerivationRecord( + uint32(index), hidden_puzzle_hash, self.vault_info.pubkey, self.type(), self.id(), False + ) + return [record] async def sync_singleton(self) -> None: wallet_node: Any = self.wallet_state_manager.wallet_node @@ -161,6 +190,7 @@ async def sync_singleton(self) -> None: vault_info = dataclasses.replace(vault_info, coin=coin_state.coin) await self.save_info(vault_info) + await self.wallet_state_manager.create_more_puzzle_hashes() async def save_info(self, vault_info: VaultInfo) -> None: self.vault_info = vault_info diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index ea79122ccb61..a76f13da79b5 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -173,10 +173,11 @@ async def puzzle_hash_exists(self, puzzle_hash: bytes32) -> bool: return row is not None def row_to_record(self, row) -> DerivationRecord: + pk_bytes = bytes.fromhex(row[1]) return DerivationRecord( uint32(row[0]), bytes32.fromhex(row[2]), - G1Element.from_bytes(bytes.fromhex(row[1])), + G1Element.from_bytes(pk_bytes) if len(pk_bytes) == 48 else pk_bytes, WalletType(row[3]), uint32(row[4]), bool(row[5]), From 4c1768a56a0f0b61d85db884b92a5a34eaafc471 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 9 Jan 2024 16:46:45 +1300 Subject: [PATCH 085/274] make bls_pk optional through test/rpc/wallet --- chia/rpc/wallet_rpc_api.py | 5 ++-- chia/rpc/wallet_rpc_client.py | 4 +-- .../puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/vault_p2_recovery.clsp | 27 ++++++++++--------- .../wallet/puzzles/vault_p2_recovery.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 22 +++++++++------ chia/wallet/wallet_state_manager.py | 2 +- tests/wallet/vault/test_vault_clsp.py | 16 ++++++++--- tests/wallet/vault/test_vault_wallet.py | 23 ++++++++++------ 9 files changed, 65 insertions(+), 38 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 54e22bb0dce6..81158d9fc8f1 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -4561,13 +4561,14 @@ async def vault_create( secp_pk = bytes.fromhex(str(request.get("secp_pk"))) hp_index = request.get("hp_index", 0) hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash() - bls_pk = G1Element.from_bytes(bytes.fromhex(str(request.get("bls_pk")))) + bls_str = request.get("bls_pk") + bls_pk = G1Element.from_bytes(bytes.fromhex(str(bls_str))) if bls_str else None timelock = uint64(request["timelock"]) fee = uint64(request.get("fee", 0)) genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE vault_record = await self.service.wallet_state_manager.create_vault_wallet( - secp_pk, hidden_puzzle_hash, bls_pk, timelock, genesis_challenge, tx_config, fee=fee + secp_pk, hidden_puzzle_hash, timelock, genesis_challenge, tx_config, bls_pk=bls_pk, fee=fee ) return { diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 9c053e9cf61a..a8062a233048 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1727,9 +1727,9 @@ async def vault_create( self, secp_pk: bytes, hp_index: uint32, - bls_pk: bytes, timelock: uint64, tx_config: TXConfig, + bls_pk: Optional[bytes] = None, fee: uint64 = uint64(0), push: bool = True, ) -> List[TransactionRecord]: @@ -1738,7 +1738,7 @@ async def vault_create( { "secp_pk": secp_pk.hex(), "hp_index": hp_index, - "bls_pk": bls_pk.hex(), + "bls_pk": bls_pk.hex() if bls_pk else None, "timelock": timelock, "fee": fee, "push": push, diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 124a53af516c..0c6128709e4a 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -62,7 +62,7 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", - "vault_p2_recovery": "5bcfac8571e7464ab8852794b37b428ce23c55976d0a6b092227fd0a6b0e07c5", + "vault_p2_recovery": "b34c99238167b8b1d8011e7cdb0b15341621c6c9742675bad2a3675b0b985838", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp index 31d5bf699c99..7071d43efb91 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp @@ -47,19 +47,22 @@ ) ) - (list - (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) - (list CREATE_COIN - (create_recovery_puzzlehash - P2_1_OF_N_MOD_HASH - FINISH_RECOVERY_MOD_HASH - P2_SECP_PUZZLEHASH - TIMELOCK - recovery_conditions + (if BLS_PK + (list + (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) + (list CREATE_COIN + (create_recovery_puzzlehash + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + TIMELOCK + recovery_conditions + ) + my_amount + ) + (list ASSERT_MY_AMOUNT my_amount) ) - my_amount - ) - (list ASSERT_MY_AMOUNT my_amount) + (x) ) diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex index 895752c239de..f860b6f562b8 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff82017fff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ff8200bfffff0180808080ffff04ffff04ffff0149ffff04ff8200bfffff01808080ffff0180808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 +ff02ffff01ff02ffff03ff2fffff01ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff82017fff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ff8200bfffff0180808080ffff04ffff04ffff0149ffff04ff8200bfffff01808080ffff0180808080ff0180ffff01ff02ffff01ff0880ff018080ff0180ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 204b50b86085..0529dea98809 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Optional + from chia_rs import G1Element from chia.types.blockchain_format.program import Program @@ -21,8 +23,8 @@ # PUZZLES -def construct_p2_delegated_secp(secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes) -> Program: - return P2_DELEGATED_SECP_MOD.curry(genesis_challenge, secp_pk, entropy) +def construct_p2_delegated_secp(secp_pk: bytes, genesis_challenge: bytes32, hidden_puzzle_hash: bytes) -> Program: + return P2_DELEGATED_SECP_MOD.curry(genesis_challenge, secp_pk, hidden_puzzle_hash) def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> Program: @@ -43,17 +45,21 @@ def get_vault_hidden_puzzle_with_index(index: uint32, hidden_puzzle: Program = D def get_vault_inner_puzzle( - secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64 + secp_pk: bytes, genesis_challenge: bytes32, hidden_puzzle_hash: bytes, bls_pk: Optional[G1Element], timelock: uint64 ) -> Program: - secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, entropy).get_tree_hash() + secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, hidden_puzzle_hash).get_tree_hash() recovery_puzzle_hash = construct_p2_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() return construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) def get_vault_inner_puzzle_hash( - secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64 + secp_pk: bytes, + genesis_challenge: bytes32, + hidden_puzzle_hash: bytes32, + bls_pk: Optional[G1Element], + timelock: uint64, ) -> bytes32: - vault_puzzle = get_vault_inner_puzzle(secp_pk, genesis_challenge, entropy, bls_pk, timelock) + vault_puzzle = get_vault_inner_puzzle(secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock) vault_puzzle_hash: bytes32 = vault_puzzle.get_tree_hash() return vault_puzzle_hash @@ -71,6 +77,6 @@ def get_vault_proof(merkle_tree: MerkleTree, puzzle_hash: bytes32) -> Program: # SECP SIGNATURE def construct_secp_message( - delegated_puzzle_hash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, entropy: bytes + delegated_puzzle_hash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, hidden_puzzle_hash: bytes ) -> bytes: - return delegated_puzzle_hash + coin_id + genesis_challenge + entropy + return delegated_puzzle_hash + coin_id + genesis_challenge + hidden_puzzle_hash diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index d6161d33b556..c546938abde1 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -2718,10 +2718,10 @@ async def create_vault_wallet( self, secp_pk: bytes, hidden_puzzle_hash: bytes32, - bls_pk: G1Element, timelock: uint64, genesis_challenge: bytes32, tx_config: TXConfig, + bls_pk: Optional[G1Element] = None, fee: uint64 = uint64(0), ) -> TransactionRecord: """ diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index e6464eb924a2..5f49c9950edb 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -1,10 +1,10 @@ from __future__ import annotations from hashlib import sha256 -from typing import Tuple +from typing import Optional, Tuple import pytest -from chia_rs import ENABLE_SECP_OPS, PrivateKey +from chia_rs import ENABLE_SECP_OPS, G1Element, PrivateKey from ecdsa import NIST256p, SigningKey from chia.consensus.default_constants import DEFAULT_CONSTANTS @@ -48,7 +48,7 @@ def test_secp_hidden() -> None: def test_recovery_puzzles() -> None: bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) - bls_pk = bls_sk.get_g1() + bls_pk: Optional[G1Element] = bls_sk.get_g1() secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) secp_pk = secp_sk.verifying_key.to_string("compressed") @@ -106,6 +106,16 @@ def test_recovery_puzzles() -> None: escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH + # Test recovery fails when no recovery key set + bls_pk = None + curried_recovery_puzzle = P2_RECOVERY_MOD.curry( + P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, escape_puzzlehash, bls_pk, timelock + ) + recovery_solution = Program.to([amount, recovery_conditions]) + + with pytest.raises(ValueError, match="clvm raise"): + run_with_secp(curried_recovery_puzzle, recovery_solution) + def test_p2_delegated_secp() -> None: secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 3540e7d9486d..7a487fc3da96 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -7,8 +7,7 @@ import pytest from ecdsa import NIST256p, SigningKey -from chia.types.blockchain_format.program import Program -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 from chia.util.observation_root import ObservationRoot from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot @@ -22,7 +21,9 @@ SECP_PK = SECP_SK.verifying_key.to_string("compressed") -async def vault_setup(wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch) -> None: +async def vault_setup( + wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch, with_recovery: bool +) -> None: def get_main_wallet_driver(self: WalletStateManager, observation_root: ObservationRoot) -> Type[MainWalletProtocol]: return Vault @@ -37,11 +38,15 @@ def get_main_wallet_driver(self: WalletStateManager, observation_root: Observati SECP_PK = SECP_SK.verifying_key.to_string("compressed") client = env.rpc_client fingerprint = (await client.get_public_keys())[0] - bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] - bls_pk = bytes.fromhex(bls_pk_hex) + bls_pk = None + if with_recovery: + bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] + bls_pk = bytes.fromhex(bls_pk_hex) timelock = uint64(1000) - hidden_puzzle_hash = Program.to("hph").get_tree_hash().hex() - res = await client.vault_create(SECP_PK, hidden_puzzle_hash, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG) + hidden_puzzle_index = uint32(1) + res = await client.vault_create( + SECP_PK, hidden_puzzle_index, timelock, bls_pk=bls_pk, tx_config=DEFAULT_TX_CONFIG + ) vault_tx = res[0] assert vault_tx @@ -82,14 +87,16 @@ def get_main_wallet_driver(self: WalletStateManager, observation_root: Observati indirect=True, ) @pytest.mark.parametrize("setup_function", [vault_setup]) +@pytest.mark.parametrize("with_recovery", [True, False]) @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") @pytest.mark.anyio async def test_vault_creation( setup_function: Callable[[WalletTestFramework, pytest.MonkeyPatch], Awaitable[None]], wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch, + with_recovery: bool, ) -> None: - await setup_function(wallet_environments, monkeypatch) + await setup_function(wallet_environments, monkeypatch, with_recovery) env = wallet_environments.environments[0] assert isinstance(env.xch_wallet, Vault) From 59727b1531532ce46a2468ca56e1d661ffc00783 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 11 Jan 2024 10:38:48 +1300 Subject: [PATCH 086/274] forgot optional timelock --- chia/rpc/wallet_rpc_api.py | 5 +++-- chia/rpc/wallet_rpc_client.py | 2 +- chia/wallet/vault/vault_drivers.py | 14 ++++++++++---- chia/wallet/wallet_state_manager.py | 2 +- tests/wallet/vault/test_vault_wallet.py | 5 +++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 81158d9fc8f1..0f96478d6875 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -4563,12 +4563,13 @@ async def vault_create( hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash() bls_str = request.get("bls_pk") bls_pk = G1Element.from_bytes(bytes.fromhex(str(bls_str))) if bls_str else None - timelock = uint64(request["timelock"]) + timelock_int = request.get("timelock") + timelock = uint64(timelock_int) if timelock_int else None fee = uint64(request.get("fee", 0)) genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE vault_record = await self.service.wallet_state_manager.create_vault_wallet( - secp_pk, hidden_puzzle_hash, timelock, genesis_challenge, tx_config, bls_pk=bls_pk, fee=fee + secp_pk, hidden_puzzle_hash, genesis_challenge, tx_config, bls_pk=bls_pk, timelock=timelock, fee=fee ) return { diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index a8062a233048..70fd9d0f3c17 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1727,9 +1727,9 @@ async def vault_create( self, secp_pk: bytes, hp_index: uint32, - timelock: uint64, tx_config: TXConfig, bls_pk: Optional[bytes] = None, + timelock: Optional[uint64] = None, fee: uint64 = uint64(0), push: bool = True, ) -> List[TransactionRecord]: diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 0529dea98809..5fc39d8b0869 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -31,7 +31,9 @@ def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) -def construct_p2_recovery_puzzle(secp_puzzle_hash: bytes32, bls_pk: G1Element, timelock: uint64) -> Program: +def construct_p2_recovery_puzzle( + secp_puzzle_hash: bytes32, bls_pk: Optional[G1Element], timelock: Optional[uint64] +) -> Program: return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzle_hash, bls_pk, timelock) @@ -45,7 +47,11 @@ def get_vault_hidden_puzzle_with_index(index: uint32, hidden_puzzle: Program = D def get_vault_inner_puzzle( - secp_pk: bytes, genesis_challenge: bytes32, hidden_puzzle_hash: bytes, bls_pk: Optional[G1Element], timelock: uint64 + secp_pk: bytes, + genesis_challenge: bytes32, + hidden_puzzle_hash: bytes, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, ) -> Program: secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, hidden_puzzle_hash).get_tree_hash() recovery_puzzle_hash = construct_p2_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() @@ -56,8 +62,8 @@ def get_vault_inner_puzzle_hash( secp_pk: bytes, genesis_challenge: bytes32, hidden_puzzle_hash: bytes32, - bls_pk: Optional[G1Element], - timelock: uint64, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, ) -> bytes32: vault_puzzle = get_vault_inner_puzzle(secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock) vault_puzzle_hash: bytes32 = vault_puzzle.get_tree_hash() diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index c546938abde1..0499bcbcc68c 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -2718,10 +2718,10 @@ async def create_vault_wallet( self, secp_pk: bytes, hidden_puzzle_hash: bytes32, - timelock: uint64, genesis_challenge: bytes32, tx_config: TXConfig, bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, fee: uint64 = uint64(0), ) -> TransactionRecord: """ diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 7a487fc3da96..3f7c3cd7b653 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -39,13 +39,14 @@ def get_main_wallet_driver(self: WalletStateManager, observation_root: Observati client = env.rpc_client fingerprint = (await client.get_public_keys())[0] bls_pk = None + timelock = None if with_recovery: bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] bls_pk = bytes.fromhex(bls_pk_hex) - timelock = uint64(1000) + timelock = uint64(1000) hidden_puzzle_index = uint32(1) res = await client.vault_create( - SECP_PK, hidden_puzzle_index, timelock, bls_pk=bls_pk, tx_config=DEFAULT_TX_CONFIG + SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG ) vault_tx = res[0] assert vault_tx From 2abd023523647631136414de04b26168fceb5ee0 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 11 Jan 2024 11:38:31 +1300 Subject: [PATCH 087/274] wrap vault inner with singleton --- chia/wallet/puzzles/singleton_top_layer_v1_1.py | 10 ++++++++++ chia/wallet/vault/vault_drivers.py | 11 +++++++++++ chia/wallet/wallet_state_manager.py | 12 +++++++----- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/chia/wallet/puzzles/singleton_top_layer_v1_1.py b/chia/wallet/puzzles/singleton_top_layer_v1_1.py index 99f7f9594530..c92b5e7f55fa 100644 --- a/chia/wallet/puzzles/singleton_top_layer_v1_1.py +++ b/chia/wallet/puzzles/singleton_top_layer_v1_1.py @@ -12,6 +12,7 @@ from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile from chia.wallet.uncurried_puzzle import UncurriedPuzzle +from chia.wallet.util.curry_and_treehash import calculate_hash_of_quoted_mod_hash, curry_and_treehash SINGLETON_MOD = load_clvm_maybe_recompile("singleton_top_layer_v1_1.clsp") SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() @@ -257,6 +258,15 @@ def puzzle_for_singleton( ) +# Convenience to calculate the singleton puzzle hash with just inner puzhash and launcher id +def puzzle_hash_for_singleton( + launcher_id: bytes32, inner_puzzle_hash: bytes32, launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH +) -> bytes32: + hash_of_quoted_mod_hash = calculate_hash_of_quoted_mod_hash(SINGLETON_MOD_HASH) + hashed_args = [Program.to((SINGLETON_MOD_HASH, (launcher_id, launcher_hash))).get_tree_hash(), inner_puzzle_hash] + return curry_and_treehash(hash_of_quoted_mod_hash, *hashed_args) + + # Return a solution to spend a singleton def solution_for_singleton( lineage_proof: LineageProof, diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 5fc39d8b0869..f222841865af 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -9,6 +9,7 @@ from chia.util.ints import uint32, uint64 from chia.wallet.puzzles.load_clvm import load_clvm from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE +from chia.wallet.puzzles.singleton_top_layer_v1_1 import puzzle_for_singleton, puzzle_hash_for_singleton from chia.wallet.util.merkle_tree import MerkleTree # MODS @@ -70,6 +71,16 @@ def get_vault_inner_puzzle_hash( return vault_puzzle_hash +def get_vault_full_puzzle(launcher_id: bytes32, inner_puzzle: Program) -> Program: + full_puzzle = puzzle_for_singleton(launcher_id, inner_puzzle) + return full_puzzle + + +def get_vault_full_puzzle_hash(launcher_id: bytes32, inner_puzzle_hash: bytes32) -> bytes32: + puzzle_hash = puzzle_hash_for_singleton(launcher_id, inner_puzzle_hash) + return puzzle_hash + + # MERKLE def construct_vault_merkle_tree(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> MerkleTree: return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 0499bcbcc68c..684105881a2d 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -143,7 +143,7 @@ last_change_height_cs, ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType -from chia.wallet.vault.vault_drivers import get_vault_inner_puzzle_hash +from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash 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 @@ -2728,9 +2728,7 @@ async def create_vault_wallet( Returns a tx record for creating a new vault """ wallet = self.main_wallet - vault_inner_puzzle_hash = get_vault_inner_puzzle_hash( - secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock - ) + # Get xch coin amount = uint64(1) coins = await wallet.select_coins(uint64(amount + fee), tx_config.coin_selection_config) @@ -2739,7 +2737,11 @@ async def create_vault_wallet( origin = next(iter(coins)) launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_HASH, amount) - genesis_launcher_solution = Program.to([vault_inner_puzzle_hash, amount, [secp_pk, hidden_puzzle_hash]]) + vault_inner_puzzle_hash = get_vault_inner_puzzle_hash( + secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock + ) + vault_full_puzzle_hash = get_vault_full_puzzle_hash(launcher_coin.name(), vault_inner_puzzle_hash) + genesis_launcher_solution = Program.to([vault_full_puzzle_hash, amount, [secp_pk, hidden_puzzle_hash]]) announcement_message = genesis_launcher_solution.get_tree_hash() [tx_record] = await wallet.generate_signed_transaction( From dded584bfe4588e53e1a1489fa5c4939db6c00e3 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 11 Jan 2024 12:17:37 +1300 Subject: [PATCH 088/274] prep for creating recovery spends --- chia/wallet/vault/vault_info.py | 11 +++++++ chia/wallet/vault/vault_wallet.py | 43 +++++++++++++++++++++---- chia/wallet/wallet_state_manager.py | 7 +++- tests/wallet/vault/test_vault_wallet.py | 6 ++++ 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index 55a7c617607e..1a8827eabed8 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -2,8 +2,11 @@ from dataclasses import dataclass +from chia_rs import G1Element + from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 from chia.util.streamable import Streamable, streamable @@ -14,3 +17,11 @@ class VaultInfo(Streamable): launcher_id: bytes32 pubkey: bytes hidden_puzzle_hash: bytes32 + inner_puzzle_hash: bytes32 + is_recoverable: bool + + +@dataclass(frozen=True) +class RecoveryInfo: + bls_pk: G1Element + timelock: uint64 diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 112df49478e2..1cf82f18e453 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -29,8 +29,12 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend -from chia.wallet.vault.vault_drivers import construct_p2_delegated_secp, get_vault_hidden_puzzle_with_index -from chia.wallet.vault.vault_info import VaultInfo +from chia.wallet.vault.vault_drivers import ( + construct_p2_delegated_secp, + get_vault_hidden_puzzle_with_index, + get_vault_inner_puzzle_hash, +) +from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.wallet import Wallet from chia.wallet.wallet_info import WalletInfo @@ -145,11 +149,24 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: def handle_own_derivation(self) -> bool: return True + def get_recovery_info(self) -> Tuple[Optional[G1Element], Optional[uint64]]: + if self.vault_info.is_recoverable: + return self.recovery_info.bls_pk, self.recovery_info.timelock + return None, None + def derivation_for_index(self, index: int) -> List[DerivationRecord]: hidden_puzzle = get_vault_hidden_puzzle_with_index(uint32(index)) hidden_puzzle_hash = hidden_puzzle.get_tree_hash() + bls_pk, timelock = self.get_recovery_info() + inner_puzzle_hash = get_vault_inner_puzzle_hash( + self.vault_info.pubkey, + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + hidden_puzzle_hash, + bls_pk, + timelock, + ) record = DerivationRecord( - uint32(index), hidden_puzzle_hash, self.vault_info.pubkey, self.type(), self.id(), False + uint32(index), inner_puzzle_hash, self.vault_info.pubkey, self.type(), self.id(), False ) return [record] @@ -170,9 +187,23 @@ async def sync_singleton(self) -> None: launcher_spend = await fetch_coin_spend(uint32(parent_state.spent_height), parent_state.coin, peer) launcher_solution = launcher_spend.solution.to_program() - secp_pk = launcher_solution.at("rrff").as_atom() - hidden_puzzle_hash = bytes32(launcher_solution.at("rrfrf").as_atom()) - vault_info = VaultInfo(coin_state.coin, launcher_id, secp_pk, hidden_puzzle_hash) + is_recoverable = False + bls_pk = None + timelock = None + memos = launcher_solution.at("rrf") + secp_pk = memos.at("f").as_atom() + hidden_puzzle_hash = bytes32(memos.at("rf").as_atom()) + if memos.list_len() == 4: + is_recoverable = True + bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) + timelock = uint64(memos.at("rrrf").as_int()) + self.recovery_info = RecoveryInfo(bls_pk, timelock) + inner_puzzle_hash = get_vault_inner_puzzle_hash( + secp_pk, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock + ) + vault_info = VaultInfo( + coin_state.coin, launcher_id, secp_pk, hidden_puzzle_hash, inner_puzzle_hash, is_recoverable + ) if coin_state.spent_height: while coin_state.spent_height is not None: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 684105881a2d..0b80458c6b77 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -26,6 +26,7 @@ import aiosqlite from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey +from clvm.casts import int_to_bytes from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.consensus.coinbase import farmer_parent_id, pool_parent_id @@ -2741,7 +2742,11 @@ async def create_vault_wallet( secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock ) vault_full_puzzle_hash = get_vault_full_puzzle_hash(launcher_coin.name(), vault_inner_puzzle_hash) - genesis_launcher_solution = Program.to([vault_full_puzzle_hash, amount, [secp_pk, hidden_puzzle_hash]]) + memos = [secp_pk, hidden_puzzle_hash] + if bls_pk: + memos.extend([bls_pk.to_bytes(), int_to_bytes(timelock)]) + + genesis_launcher_solution = Program.to([vault_full_puzzle_hash, amount, memos]) announcement_message = genesis_launcher_solution.get_tree_hash() [tx_record] = await wallet.generate_signed_transaction( diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 3f7c3cd7b653..7a13fc49f995 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -104,3 +104,9 @@ async def test_vault_creation( wallet: Vault = env.xch_wallet await wallet.sync_singleton() assert wallet.vault_info + + if with_recovery: + assert wallet.vault_info.is_recoverable + assert wallet.recovery_info is not None + else: + assert not wallet.vault_info.is_recoverable From 216f749f22bd851def7593458b37996436a1fe9f Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 12 Jan 2024 09:21:13 +1300 Subject: [PATCH 089/274] get recovery coin spends --- chia/wallet/vault/vault_drivers.py | 48 +++++++++++++- chia/wallet/vault/vault_info.py | 1 + chia/wallet/vault/vault_wallet.py | 86 ++++++++++++++++++++++++- tests/wallet/vault/test_vault_wallet.py | 5 +- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index f222841865af..2b4c6eeb5745 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -7,9 +7,14 @@ from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 +from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.load_clvm import load_clvm -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE -from chia.wallet.puzzles.singleton_top_layer_v1_1 import puzzle_for_singleton, puzzle_hash_for_singleton +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE, puzzle_hash_for_pk +from chia.wallet.puzzles.singleton_top_layer_v1_1 import ( + puzzle_for_singleton, + puzzle_hash_for_singleton, + solution_for_singleton, +) from chia.wallet.util.merkle_tree import MerkleTree # MODS @@ -81,6 +86,45 @@ def get_vault_full_puzzle_hash(launcher_id: bytes32, inner_puzzle_hash: bytes32) return puzzle_hash +def get_recovery_puzzle( + secp_puzzle_hash: bytes32, + bls_pk: G1Element, + timelock: uint64, + amount: uint64, +) -> Program: + recovery_finish = get_recovery_finish_puzzle(bls_pk, timelock, amount).get_tree_hash() + recovery_puzzle = P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_finish]).calculate_root()) + return recovery_puzzle + + +def get_recovery_conditions(bls_pk: G1Element, amount: uint64) -> Program: + puzzle_hash = puzzle_hash_for_pk(bls_pk) + recovery_conditions: Program = Program.to([[51, puzzle_hash, amount]]) + return recovery_conditions + + +def get_recovery_finish_puzzle(bls_pk: G1Element, timelock: uint64, amount: uint64) -> Program: + recovery_condition = get_recovery_conditions(bls_pk, amount) + return RECOVERY_FINISH_MOD.curry(timelock, recovery_condition) + + +# SOLUTIONS +def get_recovery_solution(amount: uint64, bls_pk: G1Element) -> Program: + recovery_conditions = get_recovery_conditions(bls_pk, amount) + recovery_solution: Program = Program.to([amount, recovery_conditions]) + return recovery_solution + + +def get_vault_inner_solution(puzzle_to_run: Program, solution: Program, proof: Program) -> Program: + inner_solution: Program = Program.to([proof, puzzle_to_run, solution]) + return inner_solution + + +def get_vault_full_solution(lineage_proof: LineageProof, amount: uint64, inner_solution: Program) -> Program: + full_solution: Program = solution_for_singleton(lineage_proof, amount, inner_solution) + return full_solution + + # MERKLE def construct_vault_merkle_tree(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> MerkleTree: return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]) diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index 1a8827eabed8..428d9313e689 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -19,6 +19,7 @@ class VaultInfo(Streamable): hidden_puzzle_hash: bytes32 inner_puzzle_hash: bytes32 is_recoverable: bool + launcher_coin_id: bytes32 @dataclass(frozen=True) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 1cf82f18e453..d461ce3a65e0 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -13,10 +13,12 @@ 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_spend import CoinSpend from chia.types.signing_mode import SigningMode from chia.util.ints import uint32, uint64 from chia.wallet.conditions import Condition from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment from chia.wallet.signer_protocol import ( PathHint, @@ -31,8 +33,17 @@ from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, + construct_vault_merkle_tree, + get_recovery_finish_puzzle, + get_recovery_puzzle, + get_recovery_solution, + get_vault_full_puzzle, + get_vault_full_solution, get_vault_hidden_puzzle_with_index, + get_vault_inner_puzzle, get_vault_inner_puzzle_hash, + get_vault_inner_solution, + get_vault_proof, ) from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo from chia.wallet.vault.vault_root import VaultRoot @@ -170,6 +181,73 @@ def derivation_for_index(self, index: int) -> List[DerivationRecord]: ) return [record] + async def create_recovery_spends(self) -> Tuple[CoinSpend, CoinSpend]: + """ + Returns two spendbundles + 1. The spend recovering the vault which can be taken to the appropriate BLS wallet for signing + 2. The spend that completes the recovery after the timelock has elapsed + """ + assert self.vault_info.is_recoverable + wallet_node: Any = self.wallet_state_manager.wallet_node + peer = wallet_node.get_full_node_peer() + assert peer is not None + # 1. Generate the recovery spend + # Get the current vault coin, ensure it's unspent + vault_coin = self.vault_info.coin + amount = uint64(self.vault_info.coin.amount) + vault_coin_state = (await wallet_node.get_coin_state([vault_coin.name()], peer))[0] + assert vault_coin_state.spent_height is None + # Generate the current inner puzzle + inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + self.recovery_info.bls_pk, + self.recovery_info.timelock, + ) + assert inner_puzzle.get_tree_hash() == self.vault_info.inner_puzzle_hash + + secp_puzzle_hash = construct_p2_delegated_secp( + self.vault_info.pubkey, + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ).get_tree_hash() + + recovery_puzzle = get_recovery_puzzle( + secp_puzzle_hash, self.recovery_info.bls_pk, self.recovery_info.timelock, amount + ) + recovery_puzzle_hash = recovery_puzzle.get_tree_hash() + # TODO: Once we have get_wallet_balance we can use it for the amount field in the solution + recovery_solution = get_recovery_solution(amount, self.recovery_info.bls_pk) + + merkle_tree = construct_vault_merkle_tree(secp_puzzle_hash, recovery_puzzle_hash) + proof = get_vault_proof(merkle_tree, recovery_puzzle_hash) + inner_solution = get_vault_inner_solution(recovery_puzzle, recovery_solution, proof) + + full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, inner_puzzle) + assert full_puzzle.get_tree_hash() == vault_coin.puzzle_hash + + # TODO: handle lineage proofs + lineage = LineageProof(self.vault_info.coin.parent_coin_info, inner_puzzle.get_tree_hash(), amount) + full_solution = get_vault_full_solution(lineage, amount, inner_solution) + recovery_spend = CoinSpend(vault_coin, full_puzzle, full_solution) + + # 2. Generate the Finish Recovery Spend + # get the recovery puzzle and coin + full_recovery_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, recovery_puzzle) + recovery_coin = Coin(self.vault_info.coin.name(), full_recovery_puzzle.get_tree_hash(), amount) + # create the finish solution + recovery_finish_puzzle = get_recovery_finish_puzzle( + self.recovery_info.bls_pk, self.recovery_info.timelock, amount + ) + recovery_finish_solution = Program.to([]) + recovery_solution = get_vault_inner_solution(recovery_finish_puzzle, recovery_finish_solution, proof) + lineage = LineageProof(self.vault_info.coin.name(), inner_puzzle.get_tree_hash(), amount) + full_recovery_solution = get_vault_full_solution(lineage, uint64(1), recovery_solution) + finish_spend = CoinSpend(recovery_coin, full_recovery_puzzle, full_recovery_solution) + + return recovery_spend, finish_spend + async def sync_singleton(self) -> None: wallet_node: Any = self.wallet_state_manager.wallet_node peer = wallet_node.get_full_node_peer() @@ -202,7 +280,13 @@ async def sync_singleton(self) -> None: secp_pk, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock ) vault_info = VaultInfo( - coin_state.coin, launcher_id, secp_pk, hidden_puzzle_hash, inner_puzzle_hash, is_recoverable + coin_state.coin, + launcher_id, + secp_pk, + hidden_puzzle_hash, + inner_puzzle_hash, + is_recoverable, + parent_state.coin.name(), ) if coin_state.spent_height: diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 7a13fc49f995..4c42f1340772 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -44,7 +44,7 @@ def get_main_wallet_driver(self: WalletStateManager, observation_root: Observati bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] bls_pk = bytes.fromhex(bls_pk_hex) timelock = uint64(1000) - hidden_puzzle_index = uint32(1) + hidden_puzzle_index = uint32(0) res = await client.vault_create( SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG ) @@ -108,5 +108,8 @@ async def test_vault_creation( if with_recovery: assert wallet.vault_info.is_recoverable assert wallet.recovery_info is not None + recovery_spend, finish_spend = await wallet.create_recovery_spends() + assert recovery_spend + assert finish_spend else: assert not wallet.vault_info.is_recoverable From c3296e11e30300b39e5b484b54642b38fe9d5c2e Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sun, 14 Jan 2024 11:37:11 +1300 Subject: [PATCH 090/274] minor fixes from quex comments --- chia/wallet/vault/vault_drivers.py | 31 +++----- chia/wallet/vault/vault_wallet.py | 89 +++++++++++++++++----- tests/wallet/vault/test_vault_lifecycle.py | 4 +- tests/wallet/vault/test_vault_wallet.py | 2 +- 4 files changed, 83 insertions(+), 43 deletions(-) diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 2b4c6eeb5745..0bbf760de595 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -37,16 +37,14 @@ def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) -def construct_p2_recovery_puzzle( - secp_puzzle_hash: bytes32, bls_pk: Optional[G1Element], timelock: Optional[uint64] -) -> Program: - return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzle_hash, bls_pk, timelock) - - def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> Program: return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root()) +def get_recovery_puzzle(secp_puzzle_hash: bytes32, bls_pk: Optional[G1Element], timelock: Optional[uint64]) -> Program: + return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzle_hash, bls_pk, timelock) + + def get_vault_hidden_puzzle_with_index(index: uint32, hidden_puzzle: Program = DEFAULT_HIDDEN_PUZZLE) -> Program: hidden_puzzle_with_index: Program = Program.to([6, (index, hidden_puzzle)]) return hidden_puzzle_with_index @@ -60,8 +58,9 @@ def get_vault_inner_puzzle( timelock: Optional[uint64] = None, ) -> Program: secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, hidden_puzzle_hash).get_tree_hash() - recovery_puzzle_hash = construct_p2_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() - return construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) + recovery_puzzle_hash = get_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() + vault_inner = construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) + return vault_inner def get_vault_inner_puzzle_hash( @@ -76,6 +75,11 @@ def get_vault_inner_puzzle_hash( return vault_puzzle_hash +def get_recovery_inner_puzzle(secp_puzzle_hash: bytes32, recovery_finish_hash: bytes32) -> Program: + puzzle = construct_vault_puzzle(secp_puzzle_hash, recovery_finish_hash) + return puzzle + + def get_vault_full_puzzle(launcher_id: bytes32, inner_puzzle: Program) -> Program: full_puzzle = puzzle_for_singleton(launcher_id, inner_puzzle) return full_puzzle @@ -86,17 +90,6 @@ def get_vault_full_puzzle_hash(launcher_id: bytes32, inner_puzzle_hash: bytes32) return puzzle_hash -def get_recovery_puzzle( - secp_puzzle_hash: bytes32, - bls_pk: G1Element, - timelock: uint64, - amount: uint64, -) -> Program: - recovery_finish = get_recovery_finish_puzzle(bls_pk, timelock, amount).get_tree_hash() - recovery_puzzle = P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_finish]).calculate_root()) - return recovery_puzzle - - def get_recovery_conditions(bls_pk: G1Element, amount: uint64) -> Program: puzzle_hash = puzzle_hash_for_pk(bls_pk) recovery_conditions: Program = Program.to([[51, puzzle_hash, amount]]) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index d461ce3a65e0..da75836216d7 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -3,20 +3,21 @@ import dataclasses import json import logging +import time from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element from typing_extensions import Unpack -from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.protocols.wallet_protocol import CoinState 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_spend import CoinSpend from chia.types.signing_mode import SigningMode +from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64 -from chia.wallet.conditions import Condition +from chia.wallet.conditions import Condition, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment @@ -29,12 +30,14 @@ SumHint, ) from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, construct_vault_merkle_tree, get_recovery_finish_puzzle, + get_recovery_inner_puzzle, get_recovery_puzzle, get_recovery_solution, get_vault_full_puzzle, @@ -68,7 +71,9 @@ async def create( async def get_new_puzzle(self) -> Program: dr = await self.wallet_state_manager.get_unused_derivation_record(self.id()) - puzzle = construct_p2_delegated_secp(dr.pubkey, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, dr.puzzle_hash) + puzzle = construct_p2_delegated_secp( + dr.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, dr.puzzle_hash + ) return puzzle async def get_new_puzzlehash(self) -> bytes32: @@ -143,7 +148,9 @@ async def get_puzzle(self, new: bool) -> Program: ] = await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id()) if record is None: return await self.get_new_puzzle() - puzzle = construct_p2_delegated_secp(record.pubkey, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, record.puzzle_hash) + puzzle = construct_p2_delegated_secp( + record.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, record.puzzle_hash + ) return puzzle def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: @@ -171,7 +178,7 @@ def derivation_for_index(self, index: int) -> List[DerivationRecord]: bls_pk, timelock = self.get_recovery_info() inner_puzzle_hash = get_vault_inner_puzzle_hash( self.vault_info.pubkey, - DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock, @@ -181,7 +188,7 @@ def derivation_for_index(self, index: int) -> List[DerivationRecord]: ) return [record] - async def create_recovery_spends(self) -> Tuple[CoinSpend, CoinSpend]: + async def create_recovery_spends(self) -> List[TransactionRecord]: """ Returns two spendbundles 1. The spend recovering the vault which can be taken to the appropriate BLS wallet for signing @@ -200,7 +207,7 @@ async def create_recovery_spends(self) -> Tuple[CoinSpend, CoinSpend]: # Generate the current inner puzzle inner_puzzle = get_vault_inner_puzzle( self.vault_info.pubkey, - DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, self.vault_info.hidden_puzzle_hash, self.recovery_info.bls_pk, self.recovery_info.timelock, @@ -209,15 +216,13 @@ async def create_recovery_spends(self) -> Tuple[CoinSpend, CoinSpend]: secp_puzzle_hash = construct_p2_delegated_secp( self.vault_info.pubkey, - DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, self.vault_info.hidden_puzzle_hash, ).get_tree_hash() - recovery_puzzle = get_recovery_puzzle( - secp_puzzle_hash, self.recovery_info.bls_pk, self.recovery_info.timelock, amount - ) + recovery_puzzle = get_recovery_puzzle(secp_puzzle_hash, self.recovery_info.bls_pk, self.recovery_info.timelock) recovery_puzzle_hash = recovery_puzzle.get_tree_hash() - # TODO: Once we have get_wallet_balance we can use it for the amount field in the solution + recovery_solution = get_recovery_solution(amount, self.recovery_info.bls_pk) merkle_tree = construct_vault_merkle_tree(secp_puzzle_hash, recovery_puzzle_hash) @@ -230,23 +235,65 @@ async def create_recovery_spends(self) -> Tuple[CoinSpend, CoinSpend]: # TODO: handle lineage proofs lineage = LineageProof(self.vault_info.coin.parent_coin_info, inner_puzzle.get_tree_hash(), amount) full_solution = get_vault_full_solution(lineage, amount, inner_solution) - recovery_spend = CoinSpend(vault_coin, full_puzzle, full_solution) + recovery_spend = SpendBundle([CoinSpend(vault_coin, full_puzzle, full_solution)], G2Element()) # 2. Generate the Finish Recovery Spend - # get the recovery puzzle and coin - full_recovery_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, recovery_puzzle) - recovery_coin = Coin(self.vault_info.coin.name(), full_recovery_puzzle.get_tree_hash(), amount) - # create the finish solution recovery_finish_puzzle = get_recovery_finish_puzzle( self.recovery_info.bls_pk, self.recovery_info.timelock, amount ) recovery_finish_solution = Program.to([]) + recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) + full_recovery_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, recovery_inner_puzzle) + recovery_coin = Coin(self.vault_info.coin.name(), full_recovery_puzzle.get_tree_hash(), amount) recovery_solution = get_vault_inner_solution(recovery_finish_puzzle, recovery_finish_solution, proof) lineage = LineageProof(self.vault_info.coin.name(), inner_puzzle.get_tree_hash(), amount) - full_recovery_solution = get_vault_full_solution(lineage, uint64(1), recovery_solution) - finish_spend = CoinSpend(recovery_coin, full_recovery_puzzle, full_recovery_solution) + full_recovery_solution = get_vault_full_solution(lineage, amount, recovery_solution) + finish_spend = SpendBundle( + [CoinSpend(recovery_coin, full_recovery_puzzle, full_recovery_solution)], G2Element() + ) + + # make the tx records + recovery_tx = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=full_puzzle.get_tree_hash(), + amount=amount, + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=recovery_spend, + additions=recovery_spend.additions(), + removals=recovery_spend.removals(), + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=recovery_spend.name(), + valid_times=parse_timelock_info(tuple()), + ) + + finish_tx = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=full_puzzle.get_tree_hash(), + amount=amount, + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=finish_spend, + additions=finish_spend.additions(), + removals=finish_spend.removals(), + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=finish_spend.name(), + valid_times=parse_timelock_info(tuple()), + ) - return recovery_spend, finish_spend + return [recovery_tx, finish_tx] async def sync_singleton(self) -> None: wallet_node: Any = self.wallet_state_manager.wallet_node @@ -277,7 +324,7 @@ async def sync_singleton(self) -> None: timelock = uint64(memos.at("rrrf").as_int()) self.recovery_info = RecoveryInfo(bls_pk, timelock) inner_puzzle_hash = get_vault_inner_puzzle_hash( - secp_pk, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock + secp_pk, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock ) vault_info = VaultInfo( coin_state.coin, diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index fc118b13f2dd..f4da01ee0953 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -20,11 +20,11 @@ from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, - construct_p2_recovery_puzzle, construct_recovery_finish, construct_secp_message, construct_vault_merkle_tree, construct_vault_puzzle, + get_recovery_puzzle, get_vault_proof, ) from tests.clvm.test_puzzles import secret_exponent_for_index @@ -49,7 +49,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: # Setup puzzles secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, ENTROPY) secp_puzzlehash = secp_puzzle.get_tree_hash() - p2_recovery_puzzle = construct_p2_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) + p2_recovery_puzzle = get_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) p2_recovery_puzzlehash = p2_recovery_puzzle.get_tree_hash() vault_puzzle = construct_vault_puzzle(secp_puzzlehash, p2_recovery_puzzlehash) vault_puzzlehash = vault_puzzle.get_tree_hash() diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 4c42f1340772..8e6a18334d89 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -108,7 +108,7 @@ async def test_vault_creation( if with_recovery: assert wallet.vault_info.is_recoverable assert wallet.recovery_info is not None - recovery_spend, finish_spend = await wallet.create_recovery_spends() + [recovery_spend, finish_spend] = await wallet.create_recovery_spends() assert recovery_spend assert finish_spend else: From 7ec35cd04310db9c56e7c54b5e24112b342e8fc6 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sun, 14 Jan 2024 11:44:39 +1300 Subject: [PATCH 091/274] return Vault as main wallet driver in WSM --- chia/wallet/wallet_state_manager.py | 3 +++ tests/wallet/vault/test_vault_wallet.py | 24 ++++-------------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 0b80458c6b77..fc073bed55fc 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -145,6 +145,7 @@ ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash +from chia.wallet.vault.vault_wallet import Vault 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 @@ -375,6 +376,8 @@ def get_main_wallet_driver(self, observation_root: ObservationRoot) -> Type[Main root_bytes: bytes = bytes(observation_root) if len(root_bytes) == 48: return Wallet + if len(root_bytes) == 32: + return Vault raise ValueError(f"Could not find a valid wallet type for observation_root: {root_bytes.hex()}") diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 8e6a18334d89..084e434840ab 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -1,19 +1,15 @@ from __future__ import annotations -import types from hashlib import sha256 -from typing import Awaitable, Callable, Type +from typing import Awaitable, Callable import pytest from ecdsa import NIST256p, SigningKey from chia.util.ints import uint32, uint64 -from chia.util.observation_root import ObservationRoot from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault -from chia.wallet.wallet_protocol import MainWalletProtocol -from chia.wallet.wallet_state_manager import WalletStateManager from tests.conftest import ConsensusMode from tests.wallet.conftest import WalletStateTransition, WalletTestFramework @@ -21,18 +17,7 @@ SECP_PK = SECP_SK.verifying_key.to_string("compressed") -async def vault_setup( - wallet_environments: WalletTestFramework, monkeypatch: pytest.MonkeyPatch, with_recovery: bool -) -> None: - def get_main_wallet_driver(self: WalletStateManager, observation_root: ObservationRoot) -> Type[MainWalletProtocol]: - return Vault - - monkeypatch.setattr( - WalletStateManager, - "get_main_wallet_driver", - types.MethodType(get_main_wallet_driver, WalletStateManager), - ) - +async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: bool) -> None: for env in wallet_environments.environments: SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") @@ -92,12 +77,11 @@ def get_main_wallet_driver(self: WalletStateManager, observation_root: Observati @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") @pytest.mark.anyio async def test_vault_creation( - setup_function: Callable[[WalletTestFramework, pytest.MonkeyPatch], Awaitable[None]], + setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], wallet_environments: WalletTestFramework, - monkeypatch: pytest.MonkeyPatch, with_recovery: bool, ) -> None: - await setup_function(wallet_environments, monkeypatch, with_recovery) + await setup_function(wallet_environments, with_recovery) env = wallet_environments.environments[0] assert isinstance(env.xch_wallet, Vault) From 27e4f2b6a6a7bee385e0d69db567348309d73783 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sun, 14 Jan 2024 12:17:10 +1300 Subject: [PATCH 092/274] use deterministic secp keys in tests --- tests/wallet/vault/test_vault_clsp.py | 22 ++++++++-------------- tests/wallet/vault/test_vault_lifecycle.py | 4 +++- tests/wallet/vault/test_vault_wallet.py | 7 +++---- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 5f49c9950edb..b7da61f3137a 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -6,6 +6,7 @@ import pytest from chia_rs import ENABLE_SECP_OPS, G1Element, PrivateKey from ecdsa import NIST256p, SigningKey +from ecdsa.util import PRNG from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.program import INFINITE_COST, Program @@ -27,6 +28,11 @@ ACS: Program = Program.to(1) ACS_PH: bytes32 = ACS.get_tree_hash() +# setup keys +seed = b"chia_secp" +secp_sk = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) +secp_pk = secp_sk.verifying_key.to_string("compressed") + def run_with_secp(puzzle: Program, solution: Program) -> Tuple[int, Program]: return puzzle._run(INFINITE_COST, ENABLE_SECP_OPS, solution) @@ -35,9 +41,6 @@ def run_with_secp(puzzle: Program, solution: Program) -> Tuple[int, Program]: def test_secp_hidden() -> None: HIDDEN_PUZZLE: Program = Program.to(1) HIDDEN_PUZZLE_HASH: bytes32 = HIDDEN_PUZZLE.get_tree_hash() - - secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) - secp_pk = secp_sk.verifying_key.to_string("compressed") escape_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, HIDDEN_PUZZLE_HASH) coin_id = Program.to("coin_id").get_tree_hash() conditions = Program.to([[51, ACS_PH, 100]]) @@ -49,9 +52,6 @@ def test_secp_hidden() -> None: def test_recovery_puzzles() -> None: bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) bls_pk: Optional[G1Element] = bls_sk.get_g1() - secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) - secp_pk = secp_sk.verifying_key.to_string("compressed") - p2_puzzlehash = ACS_PH amount = 10000 timelock = 5000 @@ -118,8 +118,6 @@ def test_recovery_puzzles() -> None: def test_p2_delegated_secp() -> None: - secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) - secp_pk = secp_sk.verifying_key.to_string("compressed") secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) coin_id = Program.to("coin_id").get_tree_hash() @@ -149,15 +147,11 @@ def test_p2_delegated_secp() -> None: def test_vault_root_puzzle() -> None: # create the secp and recovery puzzles # secp puzzle - secp_sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) - secp_pk = secp_sk.verifying_key.to_string("compressed") + bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) + bls_pk: Optional[G1Element] = bls_sk.get_g1() secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) secp_puzzlehash = secp_puzzle.get_tree_hash() - # recovery keys - bls_sk = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) - bls_pk = bls_sk.get_g1() - timelock = 5000 amount = 10000 coin_id = Program.to("coin_id").get_tree_hash() diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index f4da01ee0953..6fe524b4967f 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -7,6 +7,7 @@ from chia_rs import AugSchemeMPL, G2Element, PrivateKey from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey +from ecdsa.util import PRNG from chia.clvm.spend_sim import CostLogger, sim_and_client from chia.consensus.default_constants import DEFAULT_CONSTANTS @@ -29,7 +30,8 @@ ) from tests.clvm.test_puzzles import secret_exponent_for_index -SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) +seed = b"chia_secp" +SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") BLS_SK = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 084e434840ab..478b4b2851d1 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -5,6 +5,7 @@ import pytest from ecdsa import NIST256p, SigningKey +from ecdsa.util import PRNG from chia.util.ints import uint32, uint64 from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG @@ -13,13 +14,11 @@ from tests.conftest import ConsensusMode from tests.wallet.conftest import WalletStateTransition, WalletTestFramework -SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) -SECP_PK = SECP_SK.verifying_key.to_string("compressed") - async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: bool) -> None: for env in wallet_environments.environments: - SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256) + seed = b"chia_secp" + SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") client = env.rpc_client fingerprint = (await client.get_public_keys())[0] From 4c734dda4f168cda26d12d47ae93d5f26cb74a31 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sun, 14 Jan 2024 12:27:33 +1300 Subject: [PATCH 093/274] use hidden puz hash in lifecycle test --- tests/wallet/vault/test_vault_lifecycle.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index 6fe524b4967f..131da8c85c39 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -5,7 +5,6 @@ import pytest from chia_rs import AugSchemeMPL, G2Element, PrivateKey -from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG @@ -40,7 +39,7 @@ TIMELOCK = uint64(1000) ACS = Program.to(0) ACS_PH = ACS.get_tree_hash() -ENTROPY = int_to_bytes(101) +HIDDEN_PUZZLE_HASH = Program.to("hph").get_tree_hash() @pytest.mark.anyio @@ -49,7 +48,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: sim.pass_blocks(DEFAULT_CONSTANTS.SOFT_FORK2_HEIGHT) # Make sure secp_verify is available # Setup puzzles - secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, ENTROPY) + secp_puzzle = construct_p2_delegated_secp(SECP_PK, DEFAULT_CONSTANTS.GENESIS_CHALLENGE, HIDDEN_PUZZLE_HASH) secp_puzzlehash = secp_puzzle.get_tree_hash() p2_recovery_puzzle = get_recovery_puzzle(secp_puzzlehash, BLS_PK, TIMELOCK) p2_recovery_puzzlehash = p2_recovery_puzzle.get_tree_hash() @@ -70,7 +69,10 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) secp_signature = SECP_SK.sign_deterministic( construct_secp_message( - secp_delegated_puzzle.get_tree_hash(), vault_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE, ENTROPY + secp_delegated_puzzle.get_tree_hash(), + vault_coin.name(), + DEFAULT_CONSTANTS.GENESIS_CHALLENGE, + HIDDEN_PUZZLE_HASH, ) ) @@ -158,7 +160,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_puzzle.get_tree_hash(), recovery_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE, - ENTROPY, + HIDDEN_PUZZLE_HASH, ) ) secp_solution = Program.to( From 31bd2e964d535432e05d11c892254035773252a7 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 17 Jan 2024 20:56:33 +1300 Subject: [PATCH 094/274] make recovery spend path optional --- .../puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/vault_p2_recovery.clsp | 28 ++++++++----------- .../wallet/puzzles/vault_p2_recovery.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 10 +++++-- tests/wallet/vault/test_vault_clsp.py | 10 ------- 5 files changed, 21 insertions(+), 31 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 0c6128709e4a..124a53af516c 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -62,7 +62,7 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", - "vault_p2_recovery": "b34c99238167b8b1d8011e7cdb0b15341621c6c9742675bad2a3675b0b985838", + "vault_p2_recovery": "5bcfac8571e7464ab8852794b37b428ce23c55976d0a6b092227fd0a6b0e07c5", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp index 7071d43efb91..847a2f8e9291 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp @@ -47,23 +47,19 @@ ) ) - (if BLS_PK - (list - (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) - (list CREATE_COIN - (create_recovery_puzzlehash - P2_1_OF_N_MOD_HASH - FINISH_RECOVERY_MOD_HASH - P2_SECP_PUZZLEHASH - TIMELOCK - recovery_conditions - ) - my_amount - ) - (list ASSERT_MY_AMOUNT my_amount) + (list + (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) + (list CREATE_COIN + (create_recovery_puzzlehash + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + TIMELOCK + recovery_conditions ) - (x) + my_amount + ) + (list ASSERT_MY_AMOUNT my_amount) ) - ) diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex index f860b6f562b8..895752c239de 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ff2fffff01ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff82017fff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ff8200bfffff0180808080ffff04ffff04ffff0149ffff04ff8200bfffff01808080ffff0180808080ff0180ffff01ff02ffff01ff0880ff018080ff0180ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 +ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff82017fff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ff8200bfffff0180808080ffff04ffff04ffff0149ffff04ff8200bfffff01808080ffff0180808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 0bbf760de595..1ae1fc232299 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -37,8 +37,12 @@ def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) -> return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions) -def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> Program: - return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root()) +def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: Optional[bytes32]) -> Program: + if recovery_puzzle_hash: + merkle_root = MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root() + else: + merkle_root = MerkleTree([secp_puzzle_hash]).calculate_root() + return P2_1_OF_N_MOD.curry(merkle_root) def get_recovery_puzzle(secp_puzzle_hash: bytes32, bls_pk: Optional[G1Element], timelock: Optional[uint64]) -> Program: @@ -58,7 +62,7 @@ def get_vault_inner_puzzle( timelock: Optional[uint64] = None, ) -> Program: secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, hidden_puzzle_hash).get_tree_hash() - recovery_puzzle_hash = get_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() + recovery_puzzle_hash = get_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash() if bls_pk else None vault_inner = construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash) return vault_inner diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index b7da61f3137a..a32bae69c773 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -106,16 +106,6 @@ def test_recovery_puzzles() -> None: escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH - # Test recovery fails when no recovery key set - bls_pk = None - curried_recovery_puzzle = P2_RECOVERY_MOD.curry( - P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, escape_puzzlehash, bls_pk, timelock - ) - recovery_solution = Program.to([amount, recovery_conditions]) - - with pytest.raises(ValueError, match="clvm raise"): - run_with_secp(curried_recovery_puzzle, recovery_solution) - def test_p2_delegated_secp() -> None: secp_puzzle = P2_DELEGATED_SECP_MOD.curry(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, secp_pk, DEFAULT_HIDDEN_PUZZLE_HASH) From 39bab6df8fd902e0119f6613dbd8cc091afca233 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 19 Jan 2024 10:39:05 -0800 Subject: [PATCH 095/274] Add to sdist only allowed list --- build_scripts/check_dependency_artifacts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_scripts/check_dependency_artifacts.py b/build_scripts/check_dependency_artifacts.py index 7386a1c0cefe..bdad1a003b94 100644 --- a/build_scripts/check_dependency_artifacts.py +++ b/build_scripts/check_dependency_artifacts.py @@ -9,6 +9,9 @@ excepted_packages = { "dnslib", # pure python + "chialisp_loader", + "chialisp_puzzles", + "chia_base", } From 1e1d9a7d6eacea0c57e691f0d55f60e41c15eb22 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jan 2024 10:53:26 -0800 Subject: [PATCH 096/274] Signer protocol tweaks --- chia/wallet/signer_protocol.py | 3 ++- tests/wallet/test_signer_protocol.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/chia/wallet/signer_protocol.py b/chia/wallet/signer_protocol.py index 3055df70f969..59f237b6f1f7 100644 --- a/chia/wallet/signer_protocol.py +++ b/chia/wallet/signer_protocol.py @@ -51,7 +51,7 @@ class TransactionInfo(ClvmStreamable): class SigningTarget(ClvmStreamable): - pubkey: bytes + fingerprint: bytes message: bytes hook: bytes32 @@ -59,6 +59,7 @@ class SigningTarget(ClvmStreamable): class SumHint(ClvmStreamable): fingerprints: List[bytes] synthetic_offset: bytes + final_pubkey: bytes class PathHint(ClvmStreamable): diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 77ee47eb82fe..c5ffceec18d5 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -41,7 +41,7 @@ def test_signing_lifecycle() -> None: TransactionInfo([Spend.from_coin_spend(coin_spend)]), SigningInstructions( KeyHints([], []), - [SigningTarget(bytes(pubkey), message, bytes32([1] * 32))], + [SigningTarget(pubkey.get_fingerprint().to_bytes(4, "big"), message, bytes32([1] * 32))], ), ) From fb3f2ba2ebb765db880d38095fce94d6b3015c75 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 24 Jan 2024 10:33:01 +1300 Subject: [PATCH 097/274] p2_singletons and singleton_store --- chia/wallet/vault/vault_drivers.py | 13 +++ chia/wallet/vault/vault_wallet.py | 32 +++++- chia/wallet/wallet_state_manager.py | 3 + tests/wallet/vault/test_vault_wallet.py | 127 +++++++++++++++--------- 4 files changed, 128 insertions(+), 47 deletions(-) diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 1ae1fc232299..3d414f061371 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -11,6 +11,8 @@ from chia.wallet.puzzles.load_clvm import load_clvm from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE, puzzle_hash_for_pk from chia.wallet.puzzles.singleton_top_layer_v1_1 import ( + SINGLETON_LAUNCHER_HASH, + SINGLETON_MOD_HASH, puzzle_for_singleton, puzzle_hash_for_singleton, solution_for_singleton, @@ -26,6 +28,8 @@ P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton.clsp") +P2_SINGLETON_MOD_HASH = P2_SINGLETON_MOD.get_tree_hash() # PUZZLES @@ -105,6 +109,15 @@ def get_recovery_finish_puzzle(bls_pk: G1Element, timelock: uint64, amount: uint return RECOVERY_FINISH_MOD.curry(timelock, recovery_condition) +def get_p2_singleton_puzzle(launcher_id: bytes32) -> Program: + puzzle = P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, SINGLETON_LAUNCHER_HASH) + return puzzle + + +def get_p2_singleton_puzzle_hash(launcher_id: bytes32) -> bytes32: + return get_p2_singleton_puzzle(launcher_id).get_tree_hash() + + # SOLUTIONS def get_recovery_solution(amount: uint64, bls_pk: G1Element) -> Program: recovery_conditions = get_recovery_conditions(bls_pk, amount) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index da75836216d7..1aa47e06827c 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -16,7 +16,8 @@ from chia.types.coin_spend import CoinSpend from chia.types.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint32, uint64 +from chia.util.ints import uint32, uint64, uint128 +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.lineage_proof import LineageProof @@ -31,11 +32,12 @@ ) from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.tx_config import TXConfig +from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, construct_vault_merkle_tree, + get_p2_singleton_puzzle_hash, get_recovery_finish_puzzle, get_recovery_inner_puzzle, get_recovery_puzzle, @@ -172,6 +174,27 @@ def get_recovery_info(self) -> Tuple[Optional[G1Element], Optional[uint64]]: return self.recovery_info.bls_pk, self.recovery_info.timelock return None, None + def get_p2_singleton_puzzle_hash(self) -> bytes32: + return get_p2_singleton_puzzle_hash(self.vault_info.launcher_coin_id) + + async def select_coins(self, amount: uint64, coin_selection_config: CoinSelectionConfig) -> Set[Coin]: + unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( + self.id() + ) + puzhash = self.get_p2_singleton_puzzle_hash() + records = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash(puzhash) + assert records is not None + spendable_amount = uint128(sum([rec.coin.amount for rec in records])) + coins = await select_coins( + spendable_amount, + coin_selection_config, + records, + unconfirmed_removals, + self.log, + uint128(amount), + ) + return coins + def derivation_for_index(self, index: int) -> List[DerivationRecord]: hidden_puzzle = get_vault_hidden_puzzle_with_index(uint32(index)) hidden_puzzle_hash = hidden_puzzle.get_tree_hash() @@ -308,6 +331,7 @@ async def sync_singleton(self) -> None: raise ValueError(f"No coin found for launcher id: {launcher_id}.") coin_state: CoinState = coin_states[0] parent_state: CoinState = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] + assert parent_state.spent_height is not None launcher_spend = await fetch_coin_spend(uint32(parent_state.spent_height), parent_state.coin, peer) launcher_solution = launcher_spend.solution.to_program() @@ -354,6 +378,10 @@ async def sync_singleton(self) -> None: await self.save_info(vault_info) await self.wallet_state_manager.create_more_puzzle_hashes() + # subscribe to p2_singleton puzzle hash + p2_puzzle_hash = self.get_p2_singleton_puzzle_hash() + await self.wallet_state_manager.add_interested_puzzle_hashes([p2_puzzle_hash], [self.id()]) + async def save_info(self, vault_info: VaultInfo) -> None: self.vault_info = vault_info current_info = self.wallet_info diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index fc073bed55fc..2f027ba67c62 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -162,6 +162,7 @@ from chia.wallet.wallet_protocol import MainWalletProtocol, WalletProtocol from chia.wallet.wallet_puzzle_store import WalletPuzzleStore from chia.wallet.wallet_retry_store import WalletRetryStore +from chia.wallet.wallet_singleton_store import WalletSingletonStore from chia.wallet.wallet_transaction_store import WalletTransactionStore from chia.wallet.wallet_user_store import WalletUserStore @@ -216,6 +217,7 @@ class WalletStateManager: wallet_node: WalletNode pool_store: WalletPoolStore dl_store: DataLayerStore + singleton_store: WalletSingletonStore default_cats: Dict[str, Any] asset_to_wallet_map: Dict[AssetType, Any] initial_num_public_keys: int @@ -271,6 +273,7 @@ async def create( 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.singleton_store = await WalletSingletonStore.create(self.db_wrapper) self.default_cats = DEFAULT_CATS self.wallet_node = wallet_node diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 478b4b2851d1..50683bbe7c81 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -8,7 +8,7 @@ from ecdsa.util import PRNG from chia.util.ints import uint32, uint64 -from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG +from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault from tests.conftest import ConsensusMode @@ -16,57 +16,57 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: bool) -> None: - for env in wallet_environments.environments: - seed = b"chia_secp" - SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) - SECP_PK = SECP_SK.verifying_key.to_string("compressed") - client = env.rpc_client - fingerprint = (await client.get_public_keys())[0] - bls_pk = None - timelock = None - if with_recovery: - bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] - bls_pk = bytes.fromhex(bls_pk_hex) - timelock = uint64(1000) - hidden_puzzle_index = uint32(0) - res = await client.vault_create( - SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG - ) - vault_tx = res[0] - assert vault_tx + env = wallet_environments.environments[0] + seed = b"chia_secp" + SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) + SECP_PK = SECP_SK.verifying_key.to_string("compressed") + client = env.rpc_client + fingerprint = (await client.get_public_keys())[0] + bls_pk = None + timelock = None + if with_recovery: + bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] + bls_pk = bytes.fromhex(bls_pk_hex) + timelock = uint64(1000) + hidden_puzzle_index = uint32(0) + res = await client.vault_create( + SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG + ) + vault_tx = res[0] + assert vault_tx - eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] - launcher_id = eve_coin.name() - vault_root = VaultRoot.from_bytes(launcher_id) - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - 1: { - "init": True, - "set_remainder": True, - } - }, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": -1, - "set_remainder": True, - } - }, - ) - ] - ) - await env.wallet_node.keychain_proxy.add_public_key(launcher_id.hex()) - await env.restart(vault_root.get_fingerprint()) - await wallet_environments.full_node.wait_for_wallet_synced(env.wallet_node, 20) + eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] + launcher_id = eve_coin.name() + vault_root = VaultRoot.from_bytes(launcher_id) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -1, + "set_remainder": True, + } + }, + ) + ] + ) + await env.wallet_node.keychain_proxy.add_public_key(launcher_id.hex()) + await env.restart(vault_root.get_fingerprint()) + await wallet_environments.full_node.wait_for_wallet_synced(env.wallet_node, 20) @pytest.mark.parametrize( "wallet_environments", [ { - "num_environments": 1, - "blocks_needed": [1], + "num_environments": 2, + "blocks_needed": [1, 1], } ], indirect=True, @@ -88,6 +88,11 @@ async def test_vault_creation( await wallet.sync_singleton() assert wallet.vault_info + # get a p2_singleton + p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() + await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) + # launcher_id = wallet.vault_info.launcher_id + if with_recovery: assert wallet.vault_info.is_recoverable assert wallet.recovery_info is not None @@ -96,3 +101,35 @@ async def test_vault_creation( assert finish_spend else: assert not wallet.vault_info.is_recoverable + + funding_amount = uint64(1000000000) + funding_wallet = wallet_environments.environments[1].xch_wallet + funding_tx = await funding_wallet.generate_signed_transaction( + funding_amount, + p2_singleton_puzzle_hash, + DEFAULT_TX_CONFIG, + memos=[wallet.vault_info.pubkey], + ) + await funding_wallet.wallet_state_manager.add_pending_transactions(funding_tx) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + # "confirmed_wallet_balance": -funding_amount, + "set_remainder": True, + } + }, + ) + ], + ) + + recs = await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG) + assert recs From 297115dffcc3d4e271d547025ff93a34566572d4 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jan 2024 08:41:34 -0800 Subject: [PATCH 098/274] Convert wallet RPC client to deserialized types --- chia/cmds/cmds_util.py | 2 +- chia/cmds/coin_funcs.py | 14 +- chia/cmds/dao_funcs.py | 86 +++--- chia/cmds/wallet_funcs.py | 91 +++--- chia/data_layer/data_layer.py | 26 +- chia/rpc/wallet_request_types.py | 254 ++++++++++++++++- chia/rpc/wallet_rpc_api.py | 1 + chia/rpc/wallet_rpc_client.py | 194 ++++++------- chia/wallet/transaction_record.py | 9 +- tests/cmds/cmd_test_utils.py | 46 +-- tests/cmds/wallet/test_consts.py | 8 +- tests/cmds/wallet/test_dao.py | 66 +++-- tests/cmds/wallet/test_did.py | 37 ++- tests/cmds/wallet/test_nft.py | 54 +++- tests/cmds/wallet/test_vcs.py | 41 +-- tests/cmds/wallet/test_wallet.py | 76 +++-- tests/pools/test_pool_rpc.py | 8 +- tests/wallet/cat_wallet/test_cat_wallet.py | 2 +- tests/wallet/cat_wallet/test_trades.py | 16 +- tests/wallet/dao_wallet/test_dao_wallets.py | 137 ++++----- tests/wallet/did_wallet/test_did.py | 1 - tests/wallet/nft_wallet/test_nft_bulk_mint.py | 31 +- tests/wallet/rpc/test_wallet_rpc.py | 266 +++++++++--------- tests/wallet/vc_wallet/test_vc_wallet.py | 74 ++--- 24 files changed, 942 insertions(+), 598 deletions(-) diff --git a/chia/cmds/cmds_util.py b/chia/cmds/cmds_util.py index ea44b7221bbb..82b6c402f4ab 100644 --- a/chia/cmds/cmds_util.py +++ b/chia/cmds/cmds_util.py @@ -58,7 +58,7 @@ def transaction_submitted_msg(tx: TransactionRecord) -> str: def transaction_status_msg(fingerprint: int, tx_id: bytes32) -> str: - return f"Run 'chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}' to get status" + return f"Run 'chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id.hex()}' to get status" async def validate_client_connection( diff --git a/chia/cmds/coin_funcs.py b/chia/cmds/coin_funcs.py index 5b0ca8f5ea90..f7893518bbe5 100644 --- a/chia/cmds/coin_funcs.py +++ b/chia/cmds/coin_funcs.py @@ -184,9 +184,9 @@ async def async_combine( return target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, False)) additions = [{"amount": (total_amount - final_fee) if is_xch else total_amount, "puzzle_hash": target_ph}] - transaction: TransactionRecord = await wallet_client.send_transaction_multi( - wallet_id, additions, tx_config, removals, final_fee - ) + transaction: TransactionRecord = ( + await wallet_client.send_transaction_multi(wallet_id, additions, tx_config, removals, final_fee) + ).transaction tx_id = transaction.name.hex() print(f"Transaction sent: {tx_id}") print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") @@ -242,9 +242,11 @@ async def async_split( # TODO: [add TXConfig args] ).to_tx_config(mojo_per_unit, config, fingerprint) - transaction: TransactionRecord = await wallet_client.send_transaction_multi( - wallet_id, additions, tx_config, [removal_coin_record.coin], final_fee - ) + transaction: TransactionRecord = ( + await wallet_client.send_transaction_multi( + wallet_id, additions, tx_config, [removal_coin_record.coin], final_fee + ) + ).transaction tx_id = transaction.name.hex() print(f"Transaction sent: {tx_id}") print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") diff --git a/chia/cmds/dao_funcs.py b/chia/cmds/dao_funcs.py index c86620ebcdf0..d2504c657e22 100644 --- a/chia/cmds/dao_funcs.py +++ b/chia/cmds/dao_funcs.py @@ -39,10 +39,10 @@ async def add_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], f ) print("Successfully created DAO Wallet") - print("DAO Treasury ID: {treasury_id}".format(**res)) - print("DAO Wallet ID: {wallet_id}".format(**res)) - print("CAT Wallet ID: {cat_wallet_id}".format(**res)) - print("DAOCAT Wallet ID: {dao_cat_wallet_id}".format(**res)) + print(f"DAO Treasury ID: {res.treasury_id.hex()}") + print(f"DAO Wallet ID: {res.wallet_id}") + print(f"CAT Wallet ID: {res.cat_wallet_id}") + print(f"DAOCAT Wallet ID: {res.dao_cat_wallet_id}") async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -98,10 +98,10 @@ async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int] ) print("Successfully created DAO Wallet") - print("DAO Treasury ID: {treasury_id}".format(**res)) - print("DAO Wallet ID: {wallet_id}".format(**res)) - print("CAT Wallet ID: {cat_wallet_id}".format(**res)) - print("DAOCAT Wallet ID: {dao_cat_wallet_id}".format(**res)) + print(f"DAO Treasury ID: {res.treasury_id.hex()}") + print(f"DAO Wallet ID: {res.wallet_id}") + print(f"CAT Wallet ID: {res.cat_wallet_id}") + print(f"DAOCAT Wallet ID: {res.dao_cat_wallet_id}") async def get_treasury_id(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -156,17 +156,16 @@ async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[ ).to_tx_config(units["chia"], config, fingerprint), ) - tx_id = res["tx_id"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id)) + tx = await wallet_client.get_transaction(wallet_id, res.tx_id) if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id[2:])) + print(transaction_status_msg(fingerprint, res.tx_id)) return None - print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover async def get_treasury_balance(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -309,17 +308,16 @@ async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], } ).to_tx_config(units["chia"], config, fingerprint), ) - tx_id = res["tx_id"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id)) + tx = await wallet_client.get_transaction(wallet_id, res.tx_id) if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id[2:])) + print(transaction_status_msg(fingerprint, res.tx_id)) return None - print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -344,17 +342,16 @@ async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], f } ).to_tx_config(units["chia"], config, fingerprint), ) - tx_id = res["tx_id"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id)) + tx = await wallet_client.get_transaction(wallet_id, res.tx_id) if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id[2:])) + print(transaction_status_msg(fingerprint, res.tx_id)) return None - print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -378,17 +375,16 @@ async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: } ).to_tx_config(units["chia"], config, fingerprint), ) - tx_id = res["tx_id"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id)) + tx = await wallet_client.get_transaction(wallet_id, res.tx_id) if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id[2:])) + print(transaction_status_msg(fingerprint, res.tx_id)) return None - print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -409,16 +405,15 @@ async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp } ).to_tx_config(units["chia"], config, fingerprint), ) - tx_id = res["tx_id"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id)) + tx = await wallet_client.get_transaction(wallet_id, res.tx_id) if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id[2:])) + print(transaction_status_msg(fingerprint, res.tx_id)) return None - print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -440,16 +435,15 @@ async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: } ).to_tx_config(units["chia"], config, fingerprint), ) - tx_id = res["tx_id"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id)) + tx = await wallet_client.get_transaction(wallet_id, res.tx_id) if len(tx.sent_to) > 0: print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id[2:])) + print(transaction_status_msg(fingerprint, res.tx_id)) return None - print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -496,13 +490,11 @@ async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[ } ).to_tx_config(units["chia"], config, fingerprint), ) - if res["success"]: - asset_id_name = asset_id if asset_id else "XCH" - print(f"Created spend proposal for asset: {asset_id_name}") - print("Successfully created proposal.") - print("Proposal ID: {}".format(res["proposal_id"])) - else: # pragma: no cover - print("Failed to create proposal.") + + asset_id_name = asset_id if asset_id else "XCH" + print(f"Created spend proposal for asset: {asset_id_name}") + print("Successfully created proposal.") + print(f"Proposal ID: {res.proposal_id.hex()}") async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -541,11 +533,9 @@ async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional } ).to_tx_config(units["chia"], config, fingerprint), ) - if res["success"]: - print("Successfully created proposal.") - print("Proposal ID: {}".format(res["proposal_id"])) - else: # pragma: no cover - print("Failed to create proposal.") + + print("Successfully created proposal.") + print(f"Proposal ID: {res.proposal_id.hex()}") async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -573,8 +563,6 @@ async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[i } ).to_tx_config(units["chia"], config, fingerprint), ) - if res["success"]: - print("Successfully created proposal.") - print("Proposal ID: {}".format(res["proposal_id"])) - else: # pragma: no cover - print("Failed to create proposal.") + + print("Successfully created proposal.") + print(f"Proposal ID: {res.proposal_id.hex()}") diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 7588e4188fec..adaac12045fc 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -19,6 +19,7 @@ ) from chia.cmds.peer_funcs import print_connections from chia.cmds.units import units +from chia.rpc.wallet_request_types import CATSpendResponse, SendTransactionResponse from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import bech32_decode, decode_puzzle_hash, encode_puzzle_hash @@ -273,7 +274,7 @@ async def send( excluded_coin_ids: Sequence[str], reuse_puzhash: Optional[bool], clawback_time_lock: int, -) -> None: # pragma: no cover +) -> None: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if memo is None: memos = None @@ -303,7 +304,7 @@ async def send( final_amount: uint64 = uint64(int(amount * mojo_per_unit)) if typ == WalletType.STANDARD_WALLET: print("Submitting transaction...") - res = await wallet_client.send_transaction( + res: Union[CATSpendResponse, SendTransactionResponse] = await wallet_client.send_transaction( wallet_id, final_amount, address, @@ -340,7 +341,7 @@ async def send( print("Only standard wallet and CAT wallets are supported") return - tx_id = res.name + tx_id = res.transaction.name start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) @@ -549,7 +550,7 @@ async def make_offer( cli_confirm("Confirm (y/n): ", "Not creating offer...") - offer, trade_record = await wallet_client.create_offer_for_ids( + res = await wallet_client.create_offer_for_ids( offer_dict, driver_dict=driver_dict, fee=fee, @@ -557,12 +558,13 @@ async def make_offer( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), ) - if offer is not None: + if res.offer is not None: with open(pathlib.Path(filepath), "w") as file: - file.write(offer.to_bech32()) - print(f"Created offer with ID {trade_record.trade_id}") + file.write(res.offer.to_bech32()) + print(f"Created offer with ID {res.trade_record.trade_id}") print( - f"Use chia wallet get_offers --id " f"{trade_record.trade_id} -f {fingerprint} to view status" + "Use chia wallet get_offers --id " + f"{res.trade_record.trade_id} -f {fingerprint} to view status" ) else: print("Error creating offer") @@ -769,11 +771,13 @@ async def take_offer( if not examine_only: print() cli_confirm("Would you like to take this offer? (y/n): ") - trade_record = await wallet_client.take_offer( - offer, - fee=fee, - tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), - ) + trade_record = ( + await wallet_client.take_offer( + offer, + fee=fee, + tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + ) + ).trade_record print(f"Accepted offer with ID {trade_record.trade_id}") print(f"Use chia wallet get_offers --id {trade_record.trade_id} -f {fingerprint} to view its status") @@ -972,7 +976,8 @@ async def update_did_metadata( ).to_tx_config(units["chia"], config, fingerprint), ) print( - f"Successfully updated DID wallet ID: {response['wallet_id']}, Spend Bundle: {response['spend_bundle']}" + f"Successfully updated DID wallet ID: {response.wallet_id}, " + f"Spend Bundle: {response.spend_bundle.to_json_dict()}" ) except Exception as e: print(f"Failed to update DID metadata: {e}") @@ -995,7 +1000,7 @@ async def did_message_spend( *(CreatePuzzleAnnouncement(hexstr_to_bytes(pa)) for pa in puzzle_announcements), ), ) - print(f"Message Spend Bundle: {response['spend_bundle']}") + print(f"Message Spend Bundle: {response.spend_bundle.to_json_dict()}") except Exception as e: print(f"Failed to update DID metadata: {e}") @@ -1023,8 +1028,8 @@ async def transfer_did( ).to_tx_config(units["chia"], config, fingerprint), ) print(f"Successfully transferred DID to {target_address}") - print(f"Transaction ID: {response['transaction_id']}") - print(f"Transaction: {response['transaction']}") + print(f"Transaction ID: {response.transaction_id.hex()}") + print(f"Transaction: {response.transaction.to_json_dict_convenience(config)}") except Exception as e: print(f"Failed to transfer DID: {e}") @@ -1113,7 +1118,7 @@ async def mint_nft( if not wallet_has_did: did_id = "" - response = await wallet_client.mint_nft( + mint_response = await wallet_client.mint_nft( wallet_id, royalty_address, target_address, @@ -1132,7 +1137,7 @@ async def mint_nft( royalty_percentage, did_id, ) - spend_bundle = response["spend_bundle"] + spend_bundle = mint_response.spend_bundle print(f"NFT minted Successfully with spend bundle: {spend_bundle}") except Exception as e: print(f"Failed to mint NFT: {e}") @@ -1176,7 +1181,7 @@ async def add_uri_to_nft( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), ) - spend_bundle = response["spend_bundle"] + spend_bundle = response.spend_bundle.to_json_dict() print(f"URI added successfully with spend bundle: {spend_bundle}") except Exception as e: print(f"Failed to add URI to NFT: {e}") @@ -1205,7 +1210,7 @@ async def transfer_nft( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), ) - spend_bundle = response["spend_bundle"] + spend_bundle = response.spend_bundle.to_json_dict() print(f"NFT transferred successfully with spend bundle: {spend_bundle}") except Exception as e: print(f"Failed to transfer NFT: {e}") @@ -1287,7 +1292,7 @@ async def set_nft_did( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), ) - spend_bundle = response["spend_bundle"] + spend_bundle = response.spend_bundle.to_json_dict() print(f"Transaction to set DID on NFT has been initiated with: {spend_bundle}") except Exception as e: print(f"Failed to set DID on NFT: {e}") @@ -1465,7 +1470,7 @@ async def mint_vc( wallet_rpc_port: Optional[int], fp: Optional[int], did: str, d_fee: Decimal, target_address: Optional[str] ) -> None: # pragma: no cover async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): - vc_record, txs = await wallet_client.vc_mint( + res = await wallet_client.vc_mint( decode_puzzle_hash(ensure_valid_address(did, allowed_types={AddressType.DID}, config=config)), CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), None @@ -1476,10 +1481,10 @@ async def mint_vc( uint64(int(d_fee * units["chia"])), ) - print(f"New VC with launcher ID minted: {vc_record.vc.launcher_id}") + print(f"New VC with launcher ID minted: {res.vc_record.vc.launcher_id.hex()}") print("Relevant TX records:") print("") - for tx in txs: + for tx in res.transactions: print_transaction( tx, verbose=False, @@ -1525,15 +1530,17 @@ async def spend_vc( reuse_puzhash: bool, ) -> None: # pragma: no cover async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): - txs = await wallet_client.vc_spend( - bytes32.from_hexstr(vc_id), - new_puzhash=None if new_puzhash is None else bytes32.from_hexstr(new_puzhash), - new_proof_hash=bytes32.from_hexstr(new_proof_hash), - fee=uint64(int(d_fee * units["chia"])), - tx_config=CMDTXConfigLoader( - reuse_puzhash=reuse_puzhash, - ).to_tx_config(units["chia"], config, fingerprint), - ) + txs = ( + await wallet_client.vc_spend( + bytes32.from_hexstr(vc_id), + new_puzhash=None if new_puzhash is None else bytes32.from_hexstr(new_puzhash), + new_proof_hash=bytes32.from_hexstr(new_proof_hash), + fee=uint64(int(d_fee * units["chia"])), + tx_config=CMDTXConfigLoader( + reuse_puzhash=reuse_puzhash, + ).to_tx_config(units["chia"], config, fingerprint), + ) + ).transactions print("Proofs successfully updated!") print("Relevant TX records:") @@ -1596,13 +1603,15 @@ async def revoke_vc( parent_id: bytes32 = bytes32(record.vc.coin.parent_coin_info) else: parent_id = bytes32.from_hexstr(parent_coin_id) - txs = await wallet_client.vc_revoke( - parent_id, - fee=uint64(fee * units["chia"]), - tx_config=CMDTXConfigLoader( - reuse_puzhash=reuse_puzhash, - ).to_tx_config(units["chia"], config, fingerprint), - ) + txs = ( + await wallet_client.vc_revoke( + parent_id, + fee=uint64(fee * units["chia"]), + tx_config=CMDTXConfigLoader( + reuse_puzhash=reuse_puzhash, + ).to_tx_config(units["chia"], config, fingerprint), + ) + ).transactions print("VC successfully revoked!") print("Relevant TX records:") diff --git a/chia/data_layer/data_layer.py b/chia/data_layer/data_layer.py index 3e21a3830273..11c9297ee4e1 100644 --- a/chia/data_layer/data_layer.py +++ b/chia/data_layer/data_layer.py @@ -889,7 +889,7 @@ async def make_offer( for our_offer_store in maker } - wallet_offer, trade_record = await self.wallet_rpc.create_offer_for_ids( + res = await self.wallet_rpc.create_offer_for_ids( offer_dict=offer_dict, solver=solver, driver_dict={}, @@ -899,12 +899,10 @@ async def make_offer( # This is not a change in behavior, the default was already implicit. tx_config=DEFAULT_TX_CONFIG, ) - if wallet_offer is None: - raise Exception("offer is None despite validate_only=False") offer = Offer( - trade_id=trade_record.trade_id, - offer=bytes(wallet_offer), + trade_id=res.trade_record.trade_id, + offer=bytes(res.offer), taker=taker, maker=tuple(our_store_proofs.values()), ) @@ -978,14 +976,16 @@ async def take_offer( # after the transaction is submitted to the chain. If we roll back data we # may lose published data. - trade_record = await self.wallet_rpc.take_offer( - offer=offer, - solver=solver, - fee=fee, - # TODO: probably shouldn't be default but due to peculiarities in the RPC, we're using a stop gap. - # This is not a change in behavior, the default was already implicit. - tx_config=DEFAULT_TX_CONFIG, - ) + trade_record = ( + await self.wallet_rpc.take_offer( + offer=offer, + solver=solver, + fee=fee, + # TODO: probably shouldn't be default but due to peculiarities in the RPC, we're using a stop gap. + # This is not a change in behavior, the default was already implicit. + tx_config=DEFAULT_TX_CONFIG, + ) + ).trade_record return trade_record diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 50881274b098..b4d0121937ef 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -1,11 +1,26 @@ from __future__ import annotations from dataclasses import dataclass -from typing import List +from typing import Any, Dict, List, Type, TypeVar from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.spend_bundle import SpendBundle +from chia.util.byte_types import hexstr_to_bytes +from chia.util.ints import uint32 from chia.util.streamable import Streamable, streamable -from chia.wallet.signer_protocol import SignedTransaction, SigningInstructions, SigningResponse, Spend +from chia.wallet.signer_protocol import ( + SignedTransaction, + SigningInstructions, + SigningResponse, + Spend, + UnsignedTransaction, +) +from chia.wallet.trade_record import TradeRecord +from chia.wallet.trading.offer import Offer +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.vc_wallet.vc_store import VCRecord + +_T_OfferEndpointResponse = TypeVar("_T_OfferEndpointResponse", bound="_OfferEndpointResponse") @streamable @@ -43,3 +58,238 @@ class SubmitTransactions(Streamable): @dataclass(frozen=True) class SubmitTransactionsResponse(Streamable): mempool_ids: List[bytes32] + + +@streamable +@dataclass(frozen=True) +class ExecuteSigningInstructions(Streamable): + signing_instructions: SigningInstructions + partial_allowed: bool + + +@streamable +@dataclass(frozen=True) +class ExecuteSigningInstructionsResponse(Streamable): + signing_responses: List[SigningResponse] + + +@streamable +@dataclass(frozen=True) +class TransactionEndpointResponse(Streamable): + unsigned_transactions: List[UnsignedTransaction] + transactions: List[TransactionRecord] + + +# TODO: The section below needs corresponding request types +# TODO: The section below should be added to the API (currently only for client) +@streamable +@dataclass(frozen=True) +class SendTransactionResponse(TransactionEndpointResponse): + transaction: TransactionRecord + transaction_id: bytes32 + + +@streamable +@dataclass(frozen=True) +class SendTransactionMultiResponse(TransactionEndpointResponse): + transaction: TransactionRecord + transaction_id: bytes32 + + +@streamable +@dataclass(frozen=True) +class CreateSignedTransactionsResponse(TransactionEndpointResponse): + signed_txs: List[TransactionRecord] + signed_tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class DIDUpdateRecoveryIDsResponse(TransactionEndpointResponse): + pass + + +@streamable +@dataclass(frozen=True) +class DIDMessageSpendResponse(TransactionEndpointResponse): + spend_bundle: SpendBundle + + +@streamable +@dataclass(frozen=True) +class DIDUpdateMetadataResponse(TransactionEndpointResponse): + spend_bundle: SpendBundle + wallet_id: uint32 + + +@streamable +@dataclass(frozen=True) +class DIDTransferDIDResponse(TransactionEndpointResponse): + transaction: TransactionRecord + transaction_id: bytes32 + + +@streamable +@dataclass(frozen=True) +class CATSpendResponse(TransactionEndpointResponse): + transaction: TransactionRecord + transaction_id: bytes32 + + +@streamable +@dataclass(frozen=True) +class _OfferEndpointResponse(TransactionEndpointResponse): + offer: Offer + trade_record: TradeRecord + + @classmethod + def from_json_dict(cls: Type[_T_OfferEndpointResponse], json_dict: Dict[str, Any]) -> _T_OfferEndpointResponse: + tx_endpoint: TransactionEndpointResponse = TransactionEndpointResponse.from_json_dict(json_dict) + try: + offer: Offer = Offer.from_bech32(json_dict["offer"]) + except Exception: + offer = Offer.from_bytes(hexstr_to_bytes(json_dict["offer"])) + + return cls( + **tx_endpoint.__dict__, + offer=offer, + trade_record=TradeRecord.from_json_dict_convenience(json_dict["trade_record"], bytes(offer).hex()), + ) + + +@streamable +@dataclass(frozen=True) +class CreateOfferForIDsResponse(_OfferEndpointResponse): + pass + + +@streamable +@dataclass(frozen=True) +class TakeOfferResponse(_OfferEndpointResponse): # Inheriting for de-dup sake + pass + + +@streamable +@dataclass(frozen=True) +class CancelOfferResponse(TransactionEndpointResponse): + pass + + +@streamable +@dataclass(frozen=True) +class CancelOffersResponse(TransactionEndpointResponse): + pass + + +@streamable +@dataclass(frozen=True) +class NFTMintNFTResponse(TransactionEndpointResponse): + wallet_id: uint32 + spend_bundle: SpendBundle + nft_id: str + + +@streamable +@dataclass(frozen=True) +class NFTAddURIResponse(TransactionEndpointResponse): + wallet_id: uint32 + spend_bundle: SpendBundle + + +@streamable +@dataclass(frozen=True) +class NFTTransferNFTResponse(TransactionEndpointResponse): + wallet_id: uint32 + spend_bundle: SpendBundle + + +@streamable +@dataclass(frozen=True) +class NFTSetNFTDIDResponse(TransactionEndpointResponse): + wallet_id: uint32 + spend_bundle: SpendBundle + + +@streamable +@dataclass(frozen=True) +class NFTMintBulkResponse(TransactionEndpointResponse): + spend_bundle: SpendBundle + nft_id_list: List[str] + + +@streamable +@dataclass(frozen=True) +class CreateNewDAOWalletResponse(TransactionEndpointResponse): + type: uint32 + wallet_id: uint32 + treasury_id: bytes32 + cat_wallet_id: uint32 + dao_cat_wallet_id: uint32 + + +@streamable +@dataclass(frozen=True) +class DAOCreateProposalResponse(TransactionEndpointResponse): + proposal_id: bytes32 + tx_id: bytes32 + tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class DAOVoteOnProposalResponse(TransactionEndpointResponse): + tx_id: bytes32 + tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class DAOCloseProposalResponse(TransactionEndpointResponse): + tx_id: bytes32 + tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class DAOFreeCoinsFromFinishedProposalsResponse(TransactionEndpointResponse): + tx_id: bytes32 + tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class DAOAddFundsToTreasuryResponse(TransactionEndpointResponse): + tx_id: bytes32 + tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class DAOSendToLockupResponse(TransactionEndpointResponse): + tx_id: bytes32 + txs: List[TransactionRecord] + + +@streamable +@dataclass(frozen=True) +class DAOExitLockupResponse(TransactionEndpointResponse): + tx_id: bytes32 + tx: TransactionRecord + + +@streamable +@dataclass(frozen=True) +class VCMintResponse(TransactionEndpointResponse): + vc_record: VCRecord + + +@streamable +@dataclass(frozen=True) +class VCSpendResponse(TransactionEndpointResponse): + pass + + +@streamable +@dataclass(frozen=True) +class VCRevokeResponse(TransactionEndpointResponse): + pass diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 95d39838c725..d74c60f653cb 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1187,6 +1187,7 @@ async def send_transaction_multi(self, request: Dict[str, Any]) -> EndpointResul "transaction": transaction, "transaction_id": TransactionRecord.from_json_dict_convenience(transaction).name, "transactions": transactions, + "unsigned_transactions": response["unsigned_transactions"], } @tx_endpoint(push=True, merge_spends=False) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index d7b2250b0cf5..e09fe2c36d61 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -8,10 +8,38 @@ from chia.rpc.wallet_request_types import ( ApplySignatures, ApplySignaturesResponse, + CancelOfferResponse, + CancelOffersResponse, + CATSpendResponse, + CreateNewDAOWalletResponse, + CreateOfferForIDsResponse, + CreateSignedTransactionsResponse, + DAOAddFundsToTreasuryResponse, + DAOCloseProposalResponse, + DAOCreateProposalResponse, + DAOExitLockupResponse, + DAOFreeCoinsFromFinishedProposalsResponse, + DAOSendToLockupResponse, + DAOVoteOnProposalResponse, + DIDMessageSpendResponse, + DIDTransferDIDResponse, + DIDUpdateMetadataResponse, + DIDUpdateRecoveryIDsResponse, GatherSigningInfo, GatherSigningInfoResponse, + NFTAddURIResponse, + NFTMintBulkResponse, + NFTMintNFTResponse, + NFTSetNFTDIDResponse, + NFTTransferNFTResponse, + SendTransactionMultiResponse, + SendTransactionResponse, SubmitTransactions, SubmitTransactionsResponse, + TakeOfferResponse, + VCMintResponse, + VCRevokeResponse, + VCSpendResponse, ) from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program @@ -218,7 +246,7 @@ async def send_transaction( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> TransactionRecord: + ) -> SendTransactionResponse: request = { "wallet_id": wallet_id, "amount": amount, @@ -233,7 +261,7 @@ async def send_transaction( if memos is not None: request["memos"] = memos response = await self.fetch("send_transaction", request) - return TransactionRecord.from_json_dict_convenience(response["transaction"]) + return SendTransactionResponse.from_json_dict(response) async def send_transaction_multi( self, @@ -243,7 +271,7 @@ async def send_transaction_multi( coins: Optional[List[Coin]] = None, fee: uint64 = uint64(0), push: bool = True, - ) -> TransactionRecord: + ) -> SendTransactionMultiResponse: # Converts bytes to hex for puzzle hashes additions_hex = [] for ad in additions: @@ -261,7 +289,7 @@ async def send_transaction_multi( coins_json = [c.to_json_dict() for c in coins] request["coins"] = coins_json response = await self.fetch("send_transaction_multi", request) - return TransactionRecord.from_json_dict_convenience(response["transaction"]) + return SendTransactionMultiResponse.from_json_dict(response) async def spend_clawback_coins( self, @@ -307,7 +335,7 @@ async def create_signed_transactions( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = False, - ) -> List[TransactionRecord]: + ) -> CreateSignedTransactionsResponse: # Converts bytes to hex for puzzle hashes additions_hex = [] for ad in additions: @@ -332,33 +360,7 @@ async def create_signed_transactions( request["wallet_id"] = wallet_id response = await self.fetch("create_signed_transaction", request) - return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["signed_txs"]] - - async def create_signed_transaction( - self, - additions: List[Dict[str, Any]], - tx_config: TXConfig, - coins: Optional[List[Coin]] = None, - fee: uint64 = uint64(0), - wallet_id: Optional[int] = None, - push: bool = False, - extra_conditions: Tuple[Condition, ...] = tuple(), - timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> TransactionRecord: - txs: List[TransactionRecord] = await self.create_signed_transactions( - additions=additions, - tx_config=tx_config, - coins=coins, - fee=fee, - wallet_id=wallet_id, - push=push, - extra_conditions=extra_conditions, - timelock_info=timelock_info, - ) - if len(txs) == 0: - raise ValueError("`create_signed_transaction` returned empty list!") - - return txs[0] + return CreateSignedTransactionsResponse.from_json_dict(response) async def select_coins(self, amount: int, wallet_id: int, coin_selection_config: CoinSelectionConfig) -> List[Coin]: request = {"amount": amount, "wallet_id": wallet_id, **coin_selection_config.to_json_dict()} @@ -443,7 +445,7 @@ async def update_did_recovery_list( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> DIDUpdateRecoveryIDsResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "new_list": recovery_list, @@ -454,7 +456,7 @@ async def update_did_recovery_list( **timelock_info.to_json_dict(), } response = await self.fetch("did_update_recovery_ids", request) - return response + return DIDUpdateRecoveryIDsResponse.from_json_dict(response) async def get_did_recovery_list(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -468,7 +470,7 @@ async def did_message_spend( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = False, - ) -> Dict[str, Any]: + ) -> DIDMessageSpendResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "extra_conditions": conditions_to_json_dicts(extra_conditions), @@ -477,7 +479,7 @@ async def did_message_spend( **timelock_info.to_json_dict(), } response = await self.fetch("did_message_spend", request) - return response + return DIDMessageSpendResponse.from_json_dict(response) async def update_did_metadata( self, @@ -487,7 +489,7 @@ async def update_did_metadata( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> DIDUpdateMetadataResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "metadata": metadata, @@ -497,7 +499,7 @@ async def update_did_metadata( **timelock_info.to_json_dict(), } response = await self.fetch("did_update_metadata", request) - return response + return DIDUpdateMetadataResponse.from_json_dict(response) async def get_did_metadata(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -563,7 +565,7 @@ async def did_transfer_did( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> DIDTransferDIDResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "inner_address": address, @@ -575,7 +577,7 @@ async def did_transfer_did( **timelock_info.to_json_dict(), } response = await self.fetch("did_transfer_did", request) - return response + return DIDTransferDIDResponse.from_json_dict(response) async def did_set_wallet_name(self, wallet_id: int, name: str) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "name": name} @@ -715,8 +717,8 @@ async def cat_spend( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> TransactionRecord: - send_dict = { + ) -> CATSpendResponse: + send_dict: Dict[str, Any] = { "wallet_id": wallet_id, "fee": fee, "memos": memos if memos is not None else [], @@ -744,7 +746,7 @@ async def cat_spend( send_dict["tail_reveal"] = bytes(cat_discrepancy[1]).hex() send_dict["tail_solution"] = bytes(cat_discrepancy[2]).hex() res = await self.fetch("cat_spend", send_dict) - return TransactionRecord.from_json_dict_convenience(res["transaction"]) + return CATSpendResponse.from_json_dict(res) # Offers async def create_offer_for_ids( @@ -757,7 +759,7 @@ async def create_offer_for_ids( validate_only: bool = False, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> Tuple[Optional[Offer], TradeRecord]: + ) -> CreateOfferForIDsResponse: send_dict: Dict[str, int] = {str(key): value for key, value in offer_dict.items()} req = { @@ -773,9 +775,7 @@ async def create_offer_for_ids( if solver is not None: req["solver"] = solver res = await self.fetch("create_offer_for_ids", req) - offer: Optional[Offer] = None if validate_only else Offer.from_bech32(res["offer"]) - offer_str: str = "" if offer is None else bytes(offer).hex() - return offer, TradeRecord.from_json_dict_convenience(res["trade_record"], offer_str) + return CreateOfferForIDsResponse.from_json_dict(res) async def get_offer_summary( self, offer: Offer, advanced: bool = False @@ -796,7 +796,7 @@ async def take_offer( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> TradeRecord: + ) -> TakeOfferResponse: req = { "offer": offer.to_bech32(), "fee": fee, @@ -808,7 +808,7 @@ async def take_offer( if solver is not None: req["solver"] = solver res = await self.fetch("take_offer", req) - return TradeRecord.from_json_dict_convenience(res["trade_record"]) + return TakeOfferResponse.from_json_dict(res) async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> TradeRecord: res = await self.fetch("get_offer", {"trade_id": trade_id.hex(), "file_contents": file_contents}) @@ -859,8 +859,8 @@ async def cancel_offer( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> None: - await self.fetch( + ) -> CancelOfferResponse: + res = await self.fetch( "cancel_offer", { "trade_id": trade_id.hex(), @@ -873,6 +873,8 @@ async def cancel_offer( }, ) + return CancelOfferResponse.from_json_dict(res) + async def cancel_offers( self, tx_config: TXConfig, @@ -884,8 +886,8 @@ async def cancel_offers( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> None: - await self.fetch( + ) -> CancelOffersResponse: + res = await self.fetch( "cancel_offers", { "secure": secure, @@ -901,6 +903,8 @@ async def cancel_offers( }, ) + return CancelOffersResponse.from_json_dict(res) + # NFT wallet async def create_new_nft_wallet(self, did_id: Optional[str], name: Optional[str] = None) -> Dict[str, Any]: request = {"wallet_type": "nft_wallet", "did_id": did_id, "name": name} @@ -927,7 +931,7 @@ async def mint_nft( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> NFTMintNFTResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "royalty_address": royalty_address, @@ -949,7 +953,7 @@ async def mint_nft( **timelock_info.to_json_dict(), } response = await self.fetch("nft_mint_nft", request) - return response + return NFTMintNFTResponse.from_json_dict(response) async def add_uri_to_nft( self, @@ -962,7 +966,7 @@ async def add_uri_to_nft( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> NFTAddURIResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "nft_coin_id": nft_coin_id, @@ -975,7 +979,7 @@ async def add_uri_to_nft( **timelock_info.to_json_dict(), } response = await self.fetch("nft_add_uri", request) - return response + return NFTAddURIResponse.from_json_dict(response) async def nft_calculate_royalties( self, @@ -1008,7 +1012,7 @@ async def transfer_nft( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> NFTTransferNFTResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "nft_coin_id": nft_coin_id, @@ -1020,7 +1024,7 @@ async def transfer_nft( **timelock_info.to_json_dict(), } response = await self.fetch("nft_transfer_nft", request) - return response + return NFTTransferNFTResponse.from_json_dict(response) async def count_nfts(self, wallet_id: Optional[int]) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1042,7 +1046,7 @@ async def set_nft_did( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Dict[str, Any]: + ) -> NFTSetNFTDIDResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "did_id": did_id, @@ -1054,7 +1058,7 @@ async def set_nft_did( **timelock_info.to_json_dict(), } response = await self.fetch("nft_set_nft_did", request) - return response + return NFTSetNFTDIDResponse.from_json_dict(response) async def get_nft_wallet_did(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1081,7 +1085,7 @@ async def nft_mint_bulk( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = False, - ) -> Dict[str, Any]: + ) -> NFTMintBulkResponse: request = { "wallet_id": wallet_id, "metadata_list": metadata_list, @@ -1103,7 +1107,7 @@ async def nft_mint_bulk( **timelock_info.to_json_dict(), } response = await self.fetch("nft_mint_bulk", request) - return response + return NFTMintBulkResponse.from_json_dict(response) # DataLayer async def create_new_dl( @@ -1318,11 +1322,11 @@ async def create_new_dao_wallet( fee: uint64 = uint64(0), fee_for_cat: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> CreateNewDAOWalletResponse: + request: Dict[str, Any] = { "wallet_type": "dao_wallet", "mode": mode, - "treasury_id": treasury_id, + "treasury_id": treasury_id.hex() if treasury_id is not None else treasury_id, "dao_rules": dao_rules, "amount_of_cats": amount_of_cats, "filter_amount": filter_amount, @@ -1333,7 +1337,7 @@ async def create_new_dao_wallet( **tx_config.to_json_dict(), } response = await self.fetch("create_new_wallet", request) - return response + return CreateNewDAOWalletResponse.from_json_dict(response) async def dao_get_treasury_id(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1359,8 +1363,8 @@ async def dao_create_proposal( new_dao_rules: Optional[Dict[str, Optional[uint64]]] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOCreateProposalResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "proposal_type": proposal_type, "additions": additions, @@ -1376,7 +1380,7 @@ async def dao_create_proposal( } response = await self.fetch("dao_create_proposal", request) - return response + return DAOCreateProposalResponse.from_json_dict(response) async def dao_get_proposal_state(self, wallet_id: int, proposal_id: str) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "proposal_id": proposal_id} @@ -1397,8 +1401,8 @@ async def dao_vote_on_proposal( is_yes_vote: bool = True, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOVoteOnProposalResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "proposal_id": proposal_id, "vote_amount": vote_amount, @@ -1408,7 +1412,7 @@ async def dao_vote_on_proposal( **tx_config.to_json_dict(), } response = await self.fetch("dao_vote_on_proposal", request) - return response + return DAOVoteOnProposalResponse.from_json_dict(response) async def dao_get_proposals(self, wallet_id: int, include_closed: bool = True) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "include_closed": include_closed} @@ -1423,8 +1427,8 @@ async def dao_close_proposal( self_destruct: Optional[bool] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOCloseProposalResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "proposal_id": proposal_id, "self_destruct": self_destruct, @@ -1433,7 +1437,7 @@ async def dao_close_proposal( **tx_config.to_json_dict(), } response = await self.fetch("dao_close_proposal", request) - return response + return DAOCloseProposalResponse.from_json_dict(response) async def dao_free_coins_from_finished_proposals( self, @@ -1441,15 +1445,15 @@ async def dao_free_coins_from_finished_proposals( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOFreeCoinsFromFinishedProposalsResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "fee": fee, "extra_conditions": list(extra_conditions), **tx_config.to_json_dict(), } response = await self.fetch("dao_free_coins_from_finished_proposals", request) - return response + return DAOFreeCoinsFromFinishedProposalsResponse.from_json_dict(response) async def dao_get_treasury_balance(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1464,8 +1468,8 @@ async def dao_add_funds_to_treasury( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOAddFundsToTreasuryResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "funding_wallet_id": funding_wallet_id, "amount": amount, @@ -1474,7 +1478,7 @@ async def dao_add_funds_to_treasury( **tx_config.to_json_dict(), } response = await self.fetch("dao_add_funds_to_treasury", request) - return response + return DAOAddFundsToTreasuryResponse.from_json_dict(response) async def dao_send_to_lockup( self, @@ -1483,8 +1487,8 @@ async def dao_send_to_lockup( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOSendToLockupResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "amount": amount, "fee": fee, @@ -1492,7 +1496,7 @@ async def dao_send_to_lockup( **tx_config.to_json_dict(), } response = await self.fetch("dao_send_to_lockup", request) - return response + return DAOSendToLockupResponse.from_json_dict(response) async def dao_exit_lockup( self, @@ -1501,8 +1505,8 @@ async def dao_exit_lockup( coins: Optional[List[Dict[str, Any]]] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Dict[str, Any]: - request = { + ) -> DAOExitLockupResponse: + request: Dict[str, Any] = { "wallet_id": wallet_id, "coins": coins, "fee": fee, @@ -1510,7 +1514,7 @@ async def dao_exit_lockup( **tx_config.to_json_dict(), } response = await self.fetch("dao_exit_lockup", request) - return response + return DAOExitLockupResponse.from_json_dict(response) async def dao_adjust_filter_level(self, wallet_id: int, filter_level: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "filter_level": filter_level} @@ -1526,7 +1530,7 @@ async def vc_mint( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> Tuple[VCRecord, List[TransactionRecord]]: + ) -> VCMintResponse: response = await self.fetch( "vc_mint", { @@ -1539,9 +1543,7 @@ async def vc_mint( **timelock_info.to_json_dict(), }, ) - return VCRecord.from_json_dict(response["vc_record"]), [ - TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"] - ] + return VCMintResponse.from_json_dict(response) async def vc_get(self, vc_id: bytes32) -> Optional[VCRecord]: response = await self.fetch("vc_get", {"vc_id": vc_id.hex()}) @@ -1562,7 +1564,7 @@ async def vc_spend( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> List[TransactionRecord]: + ) -> VCSpendResponse: response = await self.fetch( "vc_spend", { @@ -1579,7 +1581,7 @@ async def vc_spend( **timelock_info.to_json_dict(), }, ) - return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + return VCSpendResponse.from_json_dict(response) async def vc_add_proofs(self, proofs: Dict[str, Any]) -> None: await self.fetch("vc_add_proofs", {"proofs": proofs}) @@ -1596,7 +1598,7 @@ async def vc_revoke( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> List[TransactionRecord]: + ) -> VCRevokeResponse: response = await self.fetch( "vc_revoke", { @@ -1608,7 +1610,7 @@ async def vc_revoke( **timelock_info.to_json_dict(), }, ) - return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + return VCRevokeResponse.from_json_dict(response) async def crcat_approve_pending( self, diff --git a/chia/wallet/transaction_record.py b/chia/wallet/transaction_record.py index 6e039dbd67b1..c093383caf0f 100644 --- a/chia/wallet/transaction_record.py +++ b/chia/wallet/transaction_record.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, Generic, List, Optional, Tuple, Type, TypeVar +from typing import Any, Dict, Generic, List, Optional, Tuple, Type, TypeVar from chia.consensus.coinbase import farmer_parent_id, pool_parent_id from chia.types.blockchain_format.coin import Coin @@ -101,6 +101,13 @@ def from_json_dict_convenience(cls: Type[_T_TransactionRecord], modified_tx_inpu modified_tx["memos"] = memos_list return cls.from_json_dict(modified_tx) + @classmethod + def from_json_dict(cls: Type[_T_TransactionRecord], json_dict: Dict[str, Any]) -> _T_TransactionRecord: + try: + return super().from_json_dict(json_dict) + except Exception: + return cls.from_json_dict_convenience(json_dict) + def to_json_dict_convenience(self, config: Dict) -> Dict: selected = config["selected_network"] prefix = config["network_overrides"]["config"][selected]["address_prefix"] diff --git a/tests/cmds/cmd_test_utils.py b/tests/cmds/cmd_test_utils.py index c21c9f415b74..19578e369970 100644 --- a/tests/cmds/cmd_test_utils.py +++ b/tests/cmds/cmd_test_utils.py @@ -17,6 +17,7 @@ from chia.rpc.farmer_rpc_client import FarmerRpcClient from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.rpc.rpc_client import RpcClient +from chia.rpc.wallet_request_types import SendTransactionMultiResponse from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.simulator.simulator_full_node_rpc_client import SimulatorFullNodeRpcClient from chia.types.blockchain_format.sized_bytes import bytes32 @@ -34,6 +35,7 @@ from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_types import WalletType from tests.cmds.testing_classes import create_test_block_record +from tests.cmds.wallet.test_consts import STD_TX, STD_UTX # Any functions that are the same for every command being tested should be below. # Functions that are specific to a command should be in the test file for that command. @@ -252,26 +254,32 @@ async def send_transaction_multi( tx_config: TXConfig, coins: Optional[List[Coin]] = None, fee: uint64 = uint64(0), - ) -> TransactionRecord: + ) -> SendTransactionMultiResponse: self.add_to_log("send_transaction_multi", (wallet_id, additions, tx_config, coins, fee)) - return TransactionRecord( - confirmed_at_height=uint32(1), - created_at_time=uint64(1234), - to_puzzle_hash=bytes32([1] * 32), - amount=uint64(12345678), - fee_amount=uint64(1234567), - confirmed=False, - sent=uint32(0), - spend_bundle=SpendBundle([], G2Element()), - additions=[Coin(bytes32([1] * 32), bytes32([2] * 32), uint64(12345678))], - removals=[Coin(bytes32([2] * 32), bytes32([4] * 32), uint64(12345678))], - wallet_id=uint32(1), - sent_to=[("aaaaa", uint8(1), None)], - trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), - name=bytes32([2] * 32), - memos=[(bytes32([3] * 32), [bytes([4] * 32)])], - valid_times=ConditionValidTimes(), + name = bytes32([2] * 32) + return SendTransactionMultiResponse( + [STD_UTX], + [STD_TX], + TransactionRecord( + confirmed_at_height=uint32(1), + created_at_time=uint64(1234), + to_puzzle_hash=bytes32([1] * 32), + amount=uint64(12345678), + fee_amount=uint64(1234567), + confirmed=False, + sent=uint32(0), + spend_bundle=SpendBundle([], G2Element()), + additions=[Coin(bytes32([1] * 32), bytes32([2] * 32), uint64(12345678))], + removals=[Coin(bytes32([2] * 32), bytes32([4] * 32), uint64(12345678))], + wallet_id=uint32(1), + sent_to=[("aaaaa", uint8(1), None)], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=name, + memos=[(bytes32([3] * 32), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), + ), + name, ) diff --git a/tests/cmds/wallet/test_consts.py b/tests/cmds/wallet/test_consts.py index 9aab7724f94b..d51da4fc9dc1 100644 --- a/tests/cmds/wallet/test_consts.py +++ b/tests/cmds/wallet/test_consts.py @@ -4,8 +4,9 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint8, uint32, uint64 +from chia.util.ints import uint32, uint64 from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.signer_protocol import KeyHints, SigningInstructions, TransactionInfo, UnsignedTransaction from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType @@ -34,10 +35,13 @@ def get_bytes32(bytes_index: int) -> bytes32: additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], wallet_id=uint32(1), - sent_to=[("aaaaa", uint8(1), None)], + sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=get_bytes32(2), memos=[(get_bytes32(3), [bytes([4] * 32)])], valid_times=ConditionValidTimes(), ) + + +STD_UTX = UnsignedTransaction(TransactionInfo([]), SigningInstructions(KeyHints([], []), [])) diff --git a/tests/cmds/wallet/test_dao.py b/tests/cmds/wallet/test_dao.py index 4b2a5e6546c1..6fc62fc4b7f0 100644 --- a/tests/cmds/wallet/test_dao.py +++ b/tests/cmds/wallet/test_dao.py @@ -7,6 +7,16 @@ import pytest +from chia.rpc.wallet_request_types import ( + CreateNewDAOWalletResponse, + DAOAddFundsToTreasuryResponse, + DAOCloseProposalResponse, + DAOCreateProposalResponse, + DAOExitLockupResponse, + DAOFreeCoinsFromFinishedProposalsResponse, + DAOSendToLockupResponse, + DAOVoteOnProposalResponse, +) from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint8, uint32, uint64 @@ -16,7 +26,7 @@ from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_types import WalletType from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert -from tests.cmds.wallet.test_consts import FINGERPRINT_ARG +from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, STD_UTX # DAO Commands @@ -37,17 +47,21 @@ async def create_new_dao_wallet( name: Optional[str] = None, fee: uint64 = uint64(0), fee_for_cat: uint64 = uint64(0), - ) -> Dict[str, Union[str, int, bytes32]]: + ) -> CreateNewDAOWalletResponse: if not treasury_id: treasury_id = bytes32(token_bytes(32)) - return { - "success": True, - "type": "DAO", - "wallet_id": 2, - "treasury_id": treasury_id, - "cat_wallet_id": 3, - "dao_cat_wallet_id": 4, - } + return CreateNewDAOWalletResponse.from_json_dict( + { + "success": True, + "transactions": [STD_TX.to_json_dict()], + "unsigned_transactions": [STD_UTX.to_json_dict()], + "type": WalletType.DAO, + "wallet_id": 2, + "treasury_id": treasury_id, + "cat_wallet_id": 3, + "dao_cat_wallet_id": 4, + } + ) inst_rpc_client = DAOCreateRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -125,8 +139,8 @@ async def dao_add_funds_to_treasury( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, - ) -> Dict[str, Union[str, bool]]: - return {"success": True, "tx_id": bytes32(b"1" * 32).hex()} + ) -> DAOAddFundsToTreasuryResponse: + return DAOAddFundsToTreasuryResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) async def dao_get_rules( self, @@ -262,8 +276,8 @@ async def dao_vote_on_proposal( tx_config: TXConfig, is_yes_vote: bool, fee: uint64 = uint64(0), - ) -> Dict[str, Union[str, bool]]: - return {"success": True, "tx_id": bytes32(b"1" * 32).hex()} + ) -> DAOVoteOnProposalResponse: + return DAOVoteOnProposalResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) async def dao_close_proposal( self, @@ -273,8 +287,8 @@ async def dao_close_proposal( fee: uint64 = uint64(0), self_destruct: bool = False, reuse_puzhash: Optional[bool] = None, - ) -> Dict[str, Union[str, bool]]: - return {"success": True, "tx_id": bytes32(b"1" * 32).hex()} + ) -> DAOCloseProposalResponse: + return DAOCloseProposalResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) async def dao_create_proposal( self, @@ -290,8 +304,8 @@ async def dao_create_proposal( new_dao_rules: Optional[Dict[str, uint64]] = None, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, - ) -> Dict[str, Union[str, bool]]: - return {"success": True, "proposal_id": "0xCAFEF00D"} + ) -> DAOCreateProposalResponse: + return DAOCreateProposalResponse([STD_UTX], [STD_TX], bytes32([0] * 32), STD_TX.name, STD_TX) async def get_wallets(self, wallet_type: Optional[WalletType] = None) -> List[Dict[str, Union[str, int]]]: return [{"id": 1, "type": 0}, {"id": 2, "type": 14}] @@ -402,7 +416,7 @@ async def get_transaction(self, wallet_id: int, transaction_id: bytes32) -> Tran "-m 0.1", "--reuse", ] - proposal_asserts = ["Successfully created proposal", "Proposal ID: 0xCAFEF00D"] + proposal_asserts = ["Successfully created proposal", f"Proposal ID: {bytes32([0] * 32).hex()}"] run_cli_command_and_assert(capsys, root_dir, spend_args, proposal_asserts) bad_spend_args = [ @@ -420,7 +434,7 @@ async def get_transaction(self, wallet_id: int, transaction_id: bytes32) -> Tran "-m 0.1", "--reuse", ] - proposal_asserts = ["Successfully created proposal", "Proposal ID: 0xCAFEF00D"] + proposal_asserts = ["Successfully created proposal", f"Proposal ID: {bytes32([0] * 32).hex()}"] with pytest.raises(ValueError) as e_info: run_cli_command_and_assert(capsys, root_dir, bad_spend_args, proposal_asserts) assert e_info.value.args[0] == "Must include a json specification or an address / amount pair." @@ -472,8 +486,8 @@ async def dao_send_to_lockup( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, - ) -> Dict[str, Union[str, int]]: - return {"success": True, "tx_id": bytes32(b"x" * 32).hex()} + ) -> DAOSendToLockupResponse: + return DAOSendToLockupResponse([STD_UTX], [STD_TX], STD_TX.name, [STD_TX]) async def dao_free_coins_from_finished_proposals( self, @@ -481,8 +495,8 @@ async def dao_free_coins_from_finished_proposals( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, - ) -> Dict[str, Union[str, int]]: - return {"success": True, "tx_id": bytes32(b"x" * 32).hex()} + ) -> DAOFreeCoinsFromFinishedProposalsResponse: + return DAOFreeCoinsFromFinishedProposalsResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) async def dao_exit_lockup( self, @@ -491,8 +505,8 @@ async def dao_exit_lockup( coins: Optional[List[Dict[str, Union[str, int]]]] = None, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, - ) -> Dict[str, Union[str, int]]: - return {"success": True, "tx_id": bytes32(b"x" * 32).hex()} + ) -> DAOExitLockupResponse: + return DAOExitLockupResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) async def get_transaction(self, wallet_id: int, transaction_id: bytes32) -> TransactionRecord: return TransactionRecord( diff --git a/tests/cmds/wallet/test_did.py b/tests/cmds/wallet/test_did.py index a4092bcb3b4b..cc9c393008e3 100644 --- a/tests/cmds/wallet/test_did.py +++ b/tests/cmds/wallet/test_did.py @@ -3,13 +3,19 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple, Union +from chia_rs import G2Element + +from chia.rpc.wallet_request_types import DIDMessageSpendResponse, DIDTransferDIDResponse, DIDUpdateMetadataResponse from chia.types.blockchain_format.sized_bytes import bytes48 from chia.types.signing_mode import SigningMode +from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash +from chia.util.config import load_config +from chia.util.ints import uint32 from chia.wallet.conditions import Condition, CreateCoinAnnouncement, CreatePuzzleAnnouncement from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, logType, run_cli_command_and_assert -from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, get_bytes32 +from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, STD_UTX, get_bytes32 # DID Commands @@ -174,9 +180,9 @@ async def update_did_metadata( wallet_id: int, metadata: Dict[str, object], tx_config: TXConfig, - ) -> Dict[str, object]: + ) -> DIDUpdateMetadataResponse: self.add_to_log("update_did_metadata", (wallet_id, metadata, tx_config)) - return {"wallet_id": wallet_id, "spend_bundle": "spend bundle here"} + return DIDUpdateMetadataResponse([STD_UTX], [STD_TX], SpendBundle([], G2Element()), uint32(wallet_id)) inst_rpc_client = DidUpdateMetadataRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -193,7 +199,8 @@ async def update_did_metadata( "--reuse", ] # these are various things that should be in the output - assert_list = [f"Successfully updated DID wallet ID: {w_id}, Spend Bundle: spend bundle here"] + assert STD_TX.spend_bundle is not None + assert_list = [f"Successfully updated DID wallet ID: {w_id}, Spend Bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "update_did_metadata": [(w_id, {"test": True}, DEFAULT_TX_CONFIG.override(reuse_puzhash=True))], @@ -246,9 +253,9 @@ def test_did_message_spend(capsys: object, get_test_cli_clients: Tuple[TestRpcCl class DidMessageSpendRpcClient(TestWalletRpcClient): async def did_message_spend( self, wallet_id: int, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] - ) -> Dict[str, object]: + ) -> DIDMessageSpendResponse: self.add_to_log("did_message_spend", (wallet_id, tx_config, extra_conditions)) - return {"spend_bundle": "spend bundle here"} + return DIDMessageSpendResponse([STD_UTX], [STD_TX], SpendBundle([], G2Element())) inst_rpc_client = DidMessageSpendRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -267,7 +274,8 @@ async def did_message_spend( ",".join([announcement.hex() for announcement in puz_announcements]), ] # these are various things that should be in the output - assert_list = ["Message Spend Bundle: spend bundle here"] + assert STD_TX.spend_bundle is not None + assert_list = [f"Message Spend Bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "did_message_spend": [ @@ -296,9 +304,14 @@ async def did_transfer_did( fee: int, with_recovery: bool, tx_config: TXConfig, - ) -> Dict[str, object]: + ) -> DIDTransferDIDResponse: self.add_to_log("did_transfer_did", (wallet_id, address, fee, with_recovery, tx_config)) - return {"transaction_id": get_bytes32(2).hex(), "transaction": "transaction here"} + return DIDTransferDIDResponse( + [STD_UTX], + [STD_TX], + STD_TX, + STD_TX.name, + ) inst_rpc_client = DidTransferRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -316,10 +329,14 @@ async def did_transfer_did( t_address, ] # these are various things that should be in the output + config = load_config( + root_dir, + "config.yaml", + ) assert_list = [ f"Successfully transferred DID to {t_address}", f"Transaction ID: {get_bytes32(2).hex()}", - "Transaction: transaction here", + f"Transaction: {STD_TX.to_json_dict_convenience(config)}", ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { diff --git a/tests/cmds/wallet/test_nft.py b/tests/cmds/wallet/test_nft.py index 9544d124fa8b..3d6e16433b35 100644 --- a/tests/cmds/wallet/test_nft.py +++ b/tests/cmds/wallet/test_nft.py @@ -3,14 +3,23 @@ from pathlib import Path from typing import Any, List, Optional, Tuple +from chia_rs import G2Element + +from chia.rpc.wallet_request_types import ( + NFTAddURIResponse, + NFTMintNFTResponse, + NFTSetNFTDIDResponse, + NFTTransferNFTResponse, +) from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.signing_mode import SigningMode +from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.wallet.nft_wallet.nft_info import NFTInfo from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, logType, run_cli_command_and_assert -from tests.cmds.wallet.test_consts import FINGERPRINT, FINGERPRINT_ARG, get_bytes32 +from tests.cmds.wallet.test_consts import FINGERPRINT, FINGERPRINT_ARG, STD_TX, STD_UTX, get_bytes32 # NFT Commands @@ -87,7 +96,7 @@ async def mint_nft( royalty_percentage: int = 0, did_id: Optional[str] = None, reuse_puzhash: Optional[bool] = None, - ) -> dict[str, object]: + ) -> NFTMintNFTResponse: self.add_to_log( "mint_nft", ( @@ -108,7 +117,13 @@ async def mint_nft( reuse_puzhash, ), ) - return {"spend_bundle": "spend bundle here"} + return NFTMintNFTResponse( + [STD_UTX], + [STD_TX], + uint32(wallet_id), + SpendBundle([], G2Element()), + bytes32([0] * 32).hex(), + ) inst_rpc_client = NFTCreateRpcClient() # pylint: disable=no-value-for-parameter target_addr = encode_puzzle_hash(get_bytes32(2), "xch") @@ -129,7 +144,7 @@ async def mint_nft( "--reuse", ] # these are various things that should be in the output - assert_list = ["NFT minted Successfully with spend bundle: spend bundle here"] + assert_list = [f"NFT minted Successfully with spend bundle: {STD_TX.spend_bundle}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "get_nft_wallet_did": [(4,)], @@ -175,9 +190,9 @@ async def add_uri_to_nft( uri: str, fee: int, tx_config: TXConfig, - ) -> dict[str, object]: + ) -> NFTAddURIResponse: self.add_to_log("add_uri_to_nft", (wallet_id, nft_coin_id, key, uri, fee, tx_config)) - return {"spend_bundle": "spend bundle here"} + return NFTAddURIResponse([STD_UTX], [STD_TX], uint32(wallet_id), SpendBundle([], G2Element())) inst_rpc_client = NFTAddUriRpcClient() # pylint: disable=no-value-for-parameter nft_coin_id = get_bytes32(2).hex() @@ -196,7 +211,8 @@ async def add_uri_to_nft( "--reuse", ] # these are various things that should be in the output - assert_list = ["URI added successfully with spend bundle: spend bundle here"] + assert STD_TX.spend_bundle is not None + assert_list = [f"URI added successfully with spend bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "add_uri_to_nft": [ @@ -225,9 +241,14 @@ async def transfer_nft( target_address: str, fee: int, tx_config: TXConfig, - ) -> dict[str, object]: + ) -> NFTTransferNFTResponse: self.add_to_log("transfer_nft", (wallet_id, nft_coin_id, target_address, fee, tx_config)) - return {"spend_bundle": "spend bundle here"} + return NFTTransferNFTResponse( + [STD_UTX], + [STD_TX], + uint32(wallet_id), + SpendBundle([], G2Element()), + ) inst_rpc_client = NFTTransferRpcClient() # pylint: disable=no-value-for-parameter nft_coin_id = get_bytes32(2).hex() @@ -247,7 +268,8 @@ async def transfer_nft( "--reuse", ] # these are various things that should be in the output - assert_list = ["NFT transferred successfully with spend bundle: spend bundle here"] + assert STD_TX.spend_bundle is not None + assert_list = [f"NFT transferred successfully with spend bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "transfer_nft": [ @@ -331,9 +353,14 @@ async def set_nft_did( nft_coin_id: str, fee: int, tx_config: TXConfig, - ) -> dict[str, object]: + ) -> NFTSetNFTDIDResponse: self.add_to_log("set_nft_did", (wallet_id, did_id, nft_coin_id, fee, tx_config)) - return {"spend_bundle": "this is a spend bundle"} + return NFTSetNFTDIDResponse( + [STD_UTX], + [STD_TX], + uint32(wallet_id), + SpendBundle([], G2Element()), + ) inst_rpc_client = NFTSetDidRpcClient() # pylint: disable=no-value-for-parameter nft_coin_id = get_bytes32(2).hex() @@ -353,7 +380,8 @@ async def set_nft_did( "--reuse", ] # these are various things that should be in the output - assert_list = ["Transaction to set DID on NFT has been initiated with: this is a spend bundle"] + assert STD_TX.spend_bundle is not None + assert_list = [f"Transaction to set DID on NFT has been initiated with: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "set_nft_did": [(4, did_id, nft_coin_id, 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True))], diff --git a/tests/cmds/wallet/test_vcs.py b/tests/cmds/wallet/test_vcs.py index ef21c7bd81d8..404bc6ebf270 100644 --- a/tests/cmds/wallet/test_vcs.py +++ b/tests/cmds/wallet/test_vcs.py @@ -5,14 +5,17 @@ from chia_rs import Coin +from chia.rpc.wallet_request_types import VCMintResponse, VCRevokeResponse, VCSpendResponse from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint32, uint64 +from chia.wallet.lineage_proof import LineageProof from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, TXConfig +from chia.wallet.vc_wallet.vc_drivers import VCLineageProof, VerifiedCredential from chia.wallet.vc_wallet.vc_store import VCRecord from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, logType, run_cli_command_and_assert -from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, get_bytes32 +from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, STD_UTX, get_bytes32 # VC Commands @@ -28,19 +31,25 @@ async def vc_mint( tx_config: TXConfig, target_address: Optional[bytes32] = None, fee: uint64 = uint64(0), - ) -> Tuple[VCRecord, List[TransactionRecord]]: + ) -> VCMintResponse: self.add_to_log("vc_mint", (did_id, tx_config, target_address, fee)) - class FakeVC: - def __init__(self) -> None: - self.launcher_id = get_bytes32(3) - - def __getattr__(self, item: str) -> Any: - if item == "vc": - return self - - txs = [STD_TX] - return cast(VCRecord, FakeVC()), txs + return VCMintResponse( + [STD_UTX], + [STD_TX], + VCRecord( + VerifiedCredential( + STD_TX.removals[0], + LineageProof(None, None, None), + VCLineageProof(None, None, None, None), + bytes32([3] * 32), + bytes32([0] * 32), + bytes32([1] * 32), + None, + ), + uint32(0), + ), + ) inst_rpc_client = VcsMintRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -108,9 +117,9 @@ async def vc_spend( new_proof_hash: Optional[bytes32] = None, provider_inner_puzhash: Optional[bytes32] = None, fee: uint64 = uint64(0), - ) -> List[TransactionRecord]: + ) -> VCSpendResponse: self.add_to_log("vc_spend", (vc_id, tx_config, new_puzhash, new_proof_hash, provider_inner_puzhash, fee)) - return [STD_TX] + return VCSpendResponse([STD_UTX], [STD_TX]) inst_rpc_client = VcsUpdateProofsRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -210,9 +219,9 @@ async def vc_revoke( vc_parent_id: bytes32, tx_config: TXConfig, fee: uint64 = uint64(0), - ) -> List[TransactionRecord]: + ) -> VCRevokeResponse: self.add_to_log("vc_revoke", (vc_parent_id, tx_config, fee)) - return [STD_TX] + return VCRevokeResponse([STD_UTX], [STD_TX]) inst_rpc_client = VcsRevokeRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client diff --git a/tests/cmds/wallet/test_wallet.py b/tests/cmds/wallet/test_wallet.py index 0e497328c6d0..bec318f75aba 100644 --- a/tests/cmds/wallet/test_wallet.py +++ b/tests/cmds/wallet/test_wallet.py @@ -1,11 +1,18 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union, cast +from typing import Any, Dict, List, Optional, Tuple, Union import pkg_resources from chia_rs import Coin, G2Element +from chia.rpc.wallet_request_types import ( + CancelOfferResponse, + CATSpendResponse, + CreateOfferForIDsResponse, + SendTransactionResponse, + TakeOfferResponse, +) from chia.server.outbound_message import NodeType from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 @@ -30,6 +37,7 @@ FINGERPRINT, FINGERPRINT_ARG, STD_TX, + STD_UTX, WALLET_ID, WALLET_ID_ARG, bytes32_hexstr, @@ -302,7 +310,8 @@ async def send_transaction( fee: uint64 = uint64(0), memos: Optional[List[str]] = None, puzzle_decorator_override: Optional[List[Dict[str, Union[str, int, bool]]]] = None, - ) -> TransactionRecord: + push: bool = True, + ) -> SendTransactionResponse: self.add_to_log( "send_transaction", ( @@ -313,8 +322,10 @@ async def send_transaction( fee, memos, puzzle_decorator_override, + push, ), ) + name = get_bytes32(2) tx_rec = TransactionRecord( confirmed_at_height=uint32(1), created_at_time=uint64(1234), @@ -330,11 +341,11 @@ async def send_transaction( sent_to=[("aaaaa", uint8(1), None)], trade_id=None, type=uint32(TransactionType.OUTGOING_CLAWBACK.value), - name=get_bytes32(2), + name=name, memos=[(get_bytes32(3), [bytes([4] * 32)])], valid_times=ConditionValidTimes(), ) - return tx_rec + return SendTransactionResponse([STD_UTX], [STD_TX], tx_rec, name) async def cat_spend( self, @@ -347,7 +358,8 @@ async def cat_spend( additions: Optional[List[Dict[str, Any]]] = None, removals: Optional[List[Coin]] = None, cat_discrepancy: Optional[Tuple[int, Program, Program]] = None, # (extra_delta, tail_reveal, tail_solution) - ) -> TransactionRecord: + push: bool = True, + ) -> CATSpendResponse: self.add_to_log( "cat_spend", ( @@ -360,9 +372,10 @@ async def cat_spend( additions, removals, cat_discrepancy, + push, ), ) - return STD_TX + return CATSpendResponse([STD_UTX], [STD_TX], STD_TX, STD_TX.name) inst_rpc_client = SendWalletRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -391,8 +404,10 @@ async def cat_spend( "Transaction submitted to nodes: [{'peer_id': 'aaaaa'", f"-f 789101 -tx 0x{get_bytes32(2).hex()}", ] + run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG], assert_list) run_cli_command_and_assert(capsys, root_dir, command_args + [CAT_FINGERPRINT_ARG], cat_assert_list) + # these are various things that should be in the output expected_calls: logType = { "get_wallets": [(None,), (None,)], @@ -411,6 +426,7 @@ async def cat_spend( 1000000000000, ["0x6262626262626262626262626262626262626262626262626262626262626262"], [{"decorator": "CLAWBACK", "clawback_timelock": 60}], + True, ) ], "cat_spend": [ @@ -430,6 +446,7 @@ async def cat_spend( None, None, None, + True, ) ], "get_transaction": [(1, get_bytes32(2)), (1, get_bytes32(2))], @@ -640,17 +657,13 @@ async def create_offer_for_ids( solver: Optional[Dict[str, Any]] = None, fee: uint64 = uint64(0), validate_only: bool = False, - ) -> Tuple[Optional[Offer], TradeRecord]: + ) -> CreateOfferForIDsResponse: self.add_to_log( "create_offer_for_ids", (offer_dict, tx_config, driver_dict, solver, fee, validate_only), ) - class FakeOffer: - def to_bech32(self) -> str: - return "offer string" - - created_offer = cast(Offer, FakeOffer()) + created_offer = Offer({}, SpendBundle([], G2Element()), {}) trade_offer: TradeRecord = TradeRecord( confirmed_at_index=uint32(0), accepted_at_time=None, @@ -666,7 +679,7 @@ def to_bech32(self) -> str: valid_times=ConditionValidTimes(), ) - return created_offer, trade_offer + return CreateOfferForIDsResponse([STD_UTX], [STD_TX], created_offer, trade_offer) inst_rpc_client = MakeOfferRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -848,21 +861,26 @@ async def take_offer( tx_config: TXConfig, solver: Optional[Dict[str, Any]] = None, fee: uint64 = uint64(0), - ) -> TradeRecord: + ) -> TakeOfferResponse: self.add_to_log("take_offer", (offer, tx_config, solver, fee)) - return TradeRecord( - confirmed_at_index=uint32(0), - accepted_at_time=uint64(123456789), - created_at_time=uint64(12345678), - is_my_offer=False, - sent=uint32(1), - sent_to=[("aaaaa", uint8(1), None)], - offer=bytes(offer), - taken_offer=None, - coins_of_interest=offer.get_involved_coins(), - trade_id=offer.name(), - status=uint32(TradeStatus.PENDING_ACCEPT.value), - valid_times=ConditionValidTimes(), + return TakeOfferResponse( + [STD_UTX], + [STD_TX], + offer, + TradeRecord( + confirmed_at_index=uint32(0), + accepted_at_time=uint64(123456789), + created_at_time=uint64(12345678), + is_my_offer=False, + sent=uint32(1), + sent_to=[("aaaaa", uint8(1), None)], + offer=bytes(offer), + taken_offer=None, + coins_of_interest=offer.get_involved_coins(), + trade_id=offer.name(), + status=uint32(TradeStatus.PENDING_ACCEPT.value), + valid_times=ConditionValidTimes(), + ), ) inst_rpc_client = TakeOfferRpcClient() # pylint: disable=no-value-for-parameter @@ -917,9 +935,9 @@ async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> Tra async def cancel_offer( self, trade_id: bytes32, tx_config: TXConfig, fee: uint64 = uint64(0), secure: bool = True - ) -> None: + ) -> CancelOfferResponse: self.add_to_log("cancel_offer", (trade_id, tx_config, fee, secure)) - return None + return CancelOfferResponse([STD_UTX], [STD_TX]) inst_rpc_client = CancelOfferRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index e600ca03cd81..acd37df5c7f8 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -459,9 +459,11 @@ async def test_absorb_self( assert len(await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 - tr: TransactionRecord = await client.send_transaction( - 1, uint64(100), encode_puzzle_hash(status.p2_singleton_puzzle_hash, "txch"), DEFAULT_TX_CONFIG - ) + tr: TransactionRecord = ( + await client.send_transaction( + 1, uint64(100), encode_puzzle_hash(status.p2_singleton_puzzle_hash, "txch"), DEFAULT_TX_CONFIG + ) + ).transaction await full_node_api.wait_transaction_records_entered_mempool(records=[tr]) await full_node_api.farm_blocks_to_puzzlehash(count=2, farm_to=our_ph, guarantee_transaction_blocks=True) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 67cc4ab24543..230f85d0ddb5 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -910,7 +910,7 @@ async def test_cat_change_detection( cat_amount_0 = uint64(100) cat_amount_1 = uint64(5) - tx = await client_0.send_transaction(1, cat_amount_0, addr, DEFAULT_TX_CONFIG) + tx = (await client_0.send_transaction(1, cat_amount_0, addr, DEFAULT_TX_CONFIG)).transaction spend_bundle = tx.spend_bundle assert spend_bundle is not None diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 3de945758de4..5976bf15dc74 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -217,12 +217,16 @@ async def test_cat_trades( ) # Mint some VCs that can spend the CR-CATs - vc_record_maker, _ = await client_maker.vc_mint( - did_id_maker, wallet_environments.tx_config, target_address=await wallet_maker.get_new_puzzlehash() - ) - vc_record_taker, _ = await client_taker.vc_mint( - did_id_taker, wallet_environments.tx_config, target_address=await wallet_taker.get_new_puzzlehash() - ) + vc_record_maker = ( + await client_maker.vc_mint( + did_id_maker, wallet_environments.tx_config, target_address=await wallet_maker.get_new_puzzlehash() + ) + ).vc_record + vc_record_taker = ( + await client_taker.vc_mint( + did_id_taker, wallet_environments.tx_config, target_address=await wallet_taker.get_new_puzzlehash() + ) + ).vc_record await wallet_environments.process_pending_states( [ # Balance checking for this scenario is covered in tests/wallet/vc_wallet/test_vc_lifecycle diff --git a/tests/wallet/dao_wallet/test_dao_wallets.py b/tests/wallet/dao_wallet/test_dao_wallets.py index 58c2bc3b9bb3..24d38a7177b8 100644 --- a/tests/wallet/dao_wallet/test_dao_wallets.py +++ b/tests/wallet/dao_wallet/test_dao_wallets.py @@ -1352,7 +1352,6 @@ async def test_dao_rpc_api( "fee": fee, } ) - assert create_proposal["success"] txs = [TransactionRecord.from_json_dict(create_proposal["tx"])] await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -1442,7 +1441,6 @@ async def test_dao_rpc_api( "fee": fee, } ) - assert mint_proposal["success"] txs = [TransactionRecord.from_json_dict(mint_proposal["tx"])] await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -1541,7 +1539,6 @@ async def test_dao_rpc_api( "fee": fee, } ) - assert update_proposal["success"] txs = [TransactionRecord.from_json_dict(update_proposal["tx"])] await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -1723,10 +1720,8 @@ async def test_dao_rpc_client( filter_amount=filter_amount, name="DAO WALLET 0", ) - assert dao_wallet_dict_0["success"] - dao_id_0 = dao_wallet_dict_0["wallet_id"] - treasury_id_hex = dao_wallet_dict_0["treasury_id"] - cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0["cat_wallet_id"]] + dao_id_0 = dao_wallet_dict_0.wallet_id + cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) @@ -1737,8 +1732,8 @@ async def test_dao_rpc_client( # Create a new standard cat for treasury funds new_cat_amt = uint64(100000) - free_coins_res = await client_0.create_new_cat_and_wallet(new_cat_amt, test=True) - new_cat_wallet_id = free_coins_res["wallet_id"] + new_cat_res = await client_0.create_new_cat_and_wallet(new_cat_amt, test=True) + new_cat_wallet_id = new_cat_res["wallet_id"] new_cat_wallet = wallet_node_0.wallet_state_manager.wallets[new_cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() @@ -1750,22 +1745,17 @@ async def test_dao_rpc_client( dao_wallet_dict_1 = await client_1.create_new_dao_wallet( mode="existing", tx_config=DEFAULT_TX_CONFIG, - treasury_id=treasury_id_hex, + treasury_id=dao_wallet_dict_0.treasury_id, filter_amount=filter_amount, name="DAO WALLET 1", ) - assert dao_wallet_dict_1["success"] - dao_id_1 = dao_wallet_dict_1["wallet_id"] - cat_wallet_1 = wallet_node_1.wallet_state_manager.wallets[dao_wallet_dict_1["cat_wallet_id"]] + dao_id_1 = dao_wallet_dict_1.wallet_id + cat_wallet_1 = wallet_node_1.wallet_state_manager.wallets[dao_wallet_dict_1.cat_wallet_id] # fund treasury xch_funds = uint64(10000000000) - funding_tx = await client_0.dao_add_funds_to_treasury(dao_id_0, 1, xch_funds, DEFAULT_TX_CONFIG) - cat_funding_tx = await client_0.dao_add_funds_to_treasury( - dao_id_0, new_cat_wallet_id, new_cat_amt, DEFAULT_TX_CONFIG - ) - assert funding_tx["success"] - assert cat_funding_tx["success"] + await client_0.dao_add_funds_to_treasury(dao_id_0, 1, xch_funds, DEFAULT_TX_CONFIG) + await client_0.dao_add_funds_to_treasury(dao_id_0, new_cat_wallet_id, new_cat_amt, DEFAULT_TX_CONFIG) txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -1791,7 +1781,7 @@ async def test_dao_rpc_client( # send cats to wallet 1 await client_0.cat_spend( - wallet_id=dao_wallet_dict_0["cat_wallet_id"], + wallet_id=dao_wallet_dict_0.cat_wallet_id, tx_config=DEFAULT_TX_CONFIG, amount=cat_amt, inner_address=encode_puzzle_hash(ph_1, "xch"), @@ -1807,10 +1797,8 @@ async def test_dao_rpc_client( await time_out_assert(20, cat_wallet_1.get_confirmed_balance, cat_amt) # send cats to lockup - lockup_0 = await client_0.dao_send_to_lockup(dao_id_0, cat_amt, DEFAULT_TX_CONFIG) - lockup_1 = await client_1.dao_send_to_lockup(dao_id_1, cat_amt, DEFAULT_TX_CONFIG) - assert lockup_0["success"] - assert lockup_1["success"] + await client_0.dao_send_to_lockup(dao_id_0, cat_amt, DEFAULT_TX_CONFIG) + await client_1.dao_send_to_lockup(dao_id_1, cat_amt, DEFAULT_TX_CONFIG) txs_0 = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() txs_1 = await wallet_1.wallet_state_manager.tx_store.get_all_unconfirmed() @@ -1824,7 +1812,7 @@ async def test_dao_rpc_client( {"puzzle_hash": ph_0.hex(), "amount": 1000}, {"puzzle_hash": ph_0.hex(), "amount": 10000, "asset_id": new_cat_asset_id.hex()}, ] - proposal = await client_0.dao_create_proposal( + await client_0.dao_create_proposal( wallet_id=dao_id_0, proposal_type="spend", tx_config=DEFAULT_TX_CONFIG, @@ -1832,7 +1820,6 @@ async def test_dao_rpc_client( vote_amount=cat_amt, fee=fee, ) - assert proposal["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -1844,7 +1831,7 @@ async def test_dao_rpc_client( proposal_id_hex = props["proposals"][0]["proposal_id"] # create an update proposal - update_proposal = await client_1.dao_create_proposal( + await client_1.dao_create_proposal( wallet_id=dao_id_1, proposal_type="update", tx_config=DEFAULT_TX_CONFIG, @@ -1852,7 +1839,6 @@ async def test_dao_rpc_client( new_dao_rules={"proposal_timelock": uint64(10)}, fee=fee, ) - assert update_proposal["success"] txs = await wallet_1.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) @@ -1860,7 +1846,7 @@ async def test_dao_rpc_client( # create a mint proposal mint_addr = await client_1.get_next_address(wallet_id=wallet_1.id(), new_address=False) - mint_proposal = await client_1.dao_create_proposal( + await client_1.dao_create_proposal( wallet_id=dao_id_1, proposal_type="mint", tx_config=DEFAULT_TX_CONFIG, @@ -1869,14 +1855,13 @@ async def test_dao_rpc_client( cat_target_address=mint_addr, fee=fee, ) - assert mint_proposal["success"] txs = await wallet_1.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) # vote spend - vote = await client_1.dao_vote_on_proposal( + await client_1.dao_vote_on_proposal( wallet_id=dao_id_1, proposal_id=proposal_id_hex, vote_amount=cat_amt, @@ -1884,7 +1869,6 @@ async def test_dao_rpc_client( is_yes_vote=True, fee=fee, ) - assert vote["success"] txs = await wallet_1.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_1, timeout=60) @@ -1928,7 +1912,7 @@ async def test_dao_rpc_client( close = await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False, fee=fee ) - tx = TransactionRecord.from_json_dict(close["tx"]) + tx = close.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -1963,8 +1947,7 @@ async def test_dao_rpc_client( close = await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False, fee=fee ) - assert close["success"] - tx = TransactionRecord.from_json_dict(close["tx"]) + tx = close.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -1977,7 +1960,7 @@ async def test_dao_rpc_client( await rpc_state( 20, client_1.get_wallet_balance, - [dao_wallet_dict_1["cat_wallet_id"]], + [dao_wallet_dict_1.cat_wallet_id], lambda x: x["confirmed_wallet_balance"], 100, ) @@ -1990,8 +1973,7 @@ async def test_dao_rpc_client( close = await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False, fee=fee ) - assert close["success"] - tx = TransactionRecord.from_json_dict(close["tx"]) + tx = close.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2010,18 +1992,16 @@ async def test_dao_rpc_client( free_coins_res = await client_0.dao_free_coins_from_finished_proposals( wallet_id=dao_id_0, tx_config=DEFAULT_TX_CONFIG ) - assert free_coins_res["success"] - free_coins_tx = TransactionRecord.from_json_dict(free_coins_res["tx"]) + free_coins_tx = free_coins_res.tx await full_node_api.wait_transaction_records_entered_mempool(records=[free_coins_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) - bal = await client_0.get_wallet_balance(dao_wallet_dict_0["dao_cat_wallet_id"]) + bal = await client_0.get_wallet_balance(dao_wallet_dict_0.dao_cat_wallet_id) assert bal["confirmed_wallet_balance"] == cat_amt exit = await client_0.dao_exit_lockup(dao_id_0, tx_config=DEFAULT_TX_CONFIG) - assert exit["success"] - exit_tx = TransactionRecord.from_json_dict(exit["tx"]) + exit_tx = exit.tx await full_node_api.wait_transaction_records_entered_mempool(records=[exit_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2029,14 +2009,14 @@ async def test_dao_rpc_client( await rpc_state( 20, client_0.get_wallet_balance, - [dao_wallet_dict_0["cat_wallet_id"]], + [dao_wallet_dict_0.cat_wallet_id], lambda x: x["confirmed_wallet_balance"], cat_amt, ) # coverage tests for filter amount and get treasury id treasury_id_resp = await client_0.dao_get_treasury_id(wallet_id=dao_id_0) - assert treasury_id_resp["treasury_id"] == treasury_id_hex + assert treasury_id_resp["treasury_id"] == "0x" + dao_wallet_dict_0.treasury_id.hex() filter_amount_resp = await client_0.dao_adjust_filter_level(wallet_id=dao_id_0, filter_level=30) assert filter_amount_resp["dao_info"]["filter_below_vote_amount"] == 30 @@ -2131,10 +2111,9 @@ async def test_dao_complex_spends( filter_amount=filter_amount, name="DAO WALLET 0", ) - assert dao_wallet_dict_0["success"] - dao_id_0 = dao_wallet_dict_0["wallet_id"] - treasury_id_hex = dao_wallet_dict_0["treasury_id"] - cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0["cat_wallet_id"]] + dao_id_0 = dao_wallet_dict_0.wallet_id + treasury_id = dao_wallet_dict_0.treasury_id + cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) @@ -2168,26 +2147,22 @@ async def test_dao_complex_spends( dao_wallet_dict_1 = await client_1.create_new_dao_wallet( mode="existing", tx_config=DEFAULT_TX_CONFIG, - treasury_id=treasury_id_hex, + treasury_id=treasury_id, filter_amount=filter_amount, name="DAO WALLET 1", ) - assert dao_wallet_dict_1["success"] - dao_id_1 = dao_wallet_dict_1["wallet_id"] + dao_id_1 = dao_wallet_dict_1.wallet_id # fund treasury so there are multiple coins for each asset xch_funds = uint64(10000000000) for _ in range(4): - funding_tx = await client_0.dao_add_funds_to_treasury(dao_id_0, 1, uint64(xch_funds / 4), DEFAULT_TX_CONFIG) - cat_funding_tx = await client_0.dao_add_funds_to_treasury( + await client_0.dao_add_funds_to_treasury(dao_id_0, 1, uint64(xch_funds / 4), DEFAULT_TX_CONFIG) + await client_0.dao_add_funds_to_treasury( dao_id_0, new_cat_wallet_id, uint64(new_cat_amt / 4), DEFAULT_TX_CONFIG ) - cat_funding_tx_2 = await client_0.dao_add_funds_to_treasury( + await client_0.dao_add_funds_to_treasury( dao_id_0, new_cat_wallet_id_2, uint64(new_cat_amt / 4), DEFAULT_TX_CONFIG ) - assert funding_tx["success"] - assert cat_funding_tx["success"] - assert cat_funding_tx_2["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2218,8 +2193,7 @@ async def test_dao_complex_spends( await client_1.create_wallet_for_existing_cat(new_cat_asset_id_2) # send cats to lockup - lockup_0 = await client_0.dao_send_to_lockup(dao_id_0, cat_amt, DEFAULT_TX_CONFIG) - assert lockup_0["success"] + await client_0.dao_send_to_lockup(dao_id_0, cat_amt, DEFAULT_TX_CONFIG) txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) @@ -2233,14 +2207,13 @@ async def test_dao_complex_spends( {"puzzle_hash": ph_0.hex(), "amount": xch_funds / 4}, {"puzzle_hash": ph_1.hex(), "amount": xch_funds / 4}, ] - proposal = await client_0.dao_create_proposal( + await client_0.dao_create_proposal( wallet_id=dao_id_0, proposal_type="spend", tx_config=DEFAULT_TX_CONFIG, additions=additions, vote_amount=cat_amt, ) - assert proposal["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2249,10 +2222,9 @@ async def test_dao_complex_spends( props = await client_1.dao_get_proposals(dao_id_1) proposal_id_hex = props["proposals"][-1]["proposal_id"] - close = await client_0.dao_close_proposal( + await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False ) - assert close["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2283,14 +2255,13 @@ async def test_dao_complex_spends( {"puzzle_hash": ph_0.hex(), "amount": cat_spend_amt, "asset_id": new_cat_asset_id.hex()}, {"puzzle_hash": ph_0.hex(), "amount": cat_spend_amt, "asset_id": new_cat_asset_id_2.hex()}, ] - proposal = await client_0.dao_create_proposal( + await client_0.dao_create_proposal( wallet_id=dao_id_0, proposal_type="spend", tx_config=DEFAULT_TX_CONFIG, additions=additions, vote_amount=cat_amt, ) - assert proposal["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2299,10 +2270,9 @@ async def test_dao_complex_spends( props = await client_1.dao_get_proposals(dao_id_1) proposal_id_hex = props["proposals"][-1]["proposal_id"] - close = await client_0.dao_close_proposal( + await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False ) - assert close["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2349,14 +2319,13 @@ async def test_dao_complex_spends( {"puzzle_hash": ph_0.hex(), "amount": xch_funds / 4}, {"puzzle_hash": ph_1.hex(), "amount": xch_funds / 4}, ] - proposal = await client_0.dao_create_proposal( + await client_0.dao_create_proposal( wallet_id=dao_id_0, proposal_type="spend", tx_config=DEFAULT_TX_CONFIG, additions=additions, vote_amount=cat_amt, ) - assert proposal["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2365,13 +2334,12 @@ async def test_dao_complex_spends( props = await client_0.dao_get_proposals(dao_id_0) proposal_id_hex = props["proposals"][-1]["proposal_id"] - close = await client_0.dao_close_proposal( + await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False, ) - assert close["success"] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) @@ -2748,11 +2716,9 @@ async def test_dao_cat_exits( filter_amount=filter_amount, name="DAO WALLET 0", ) - assert dao_wallet_dict_0["success"] - dao_id_0 = dao_wallet_dict_0["wallet_id"] - # treasury_id_hex = dao_wallet_dict_0["treasury_id"] - cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0["cat_wallet_id"]] - dao_cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0["dao_cat_wallet_id"]] + dao_id_0 = dao_wallet_dict_0.wallet_id + cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.cat_wallet_id] + dao_cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.dao_cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() for tx in txs: await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) @@ -2765,8 +2731,7 @@ async def test_dao_cat_exits( # fund treasury xch_funds = uint64(10000000000) funding_tx = await client_0.dao_add_funds_to_treasury(dao_id_0, 1, xch_funds, DEFAULT_TX_CONFIG) - assert funding_tx["success"] - tx = TransactionRecord.from_json_dict(funding_tx["tx"]) + tx = funding_tx.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2775,8 +2740,7 @@ async def test_dao_cat_exits( # send cats to lockup lockup_0 = await client_0.dao_send_to_lockup(dao_id_0, cat_amt, DEFAULT_TX_CONFIG) - assert lockup_0["success"] - txs = [TransactionRecord.from_json_dict(x) for x in lockup_0["txs"]] + txs = lockup_0.txs await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2796,8 +2760,7 @@ async def test_dao_cat_exits( vote_amount=cat_amt, fee=fee, ) - assert proposal["success"] - tx = TransactionRecord.from_json_dict(proposal["tx"]) + tx = proposal.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2823,8 +2786,7 @@ async def test_dao_cat_exits( close = await client_0.dao_close_proposal( wallet_id=dao_id_0, proposal_id=proposal_id_hex, tx_config=DEFAULT_TX_CONFIG, self_destruct=False, fee=fee ) - assert close["success"] - tx = TransactionRecord.from_json_dict(close["tx"]) + tx = close.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2834,8 +2796,7 @@ async def test_dao_cat_exits( # free locked cats from finished proposal res = await client_0.dao_free_coins_from_finished_proposals(wallet_id=dao_id_0, tx_config=DEFAULT_TX_CONFIG) - assert res["success"] - tx = TransactionRecord.from_json_dict(res["tx"]) + tx = res.tx await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) @@ -2844,7 +2805,7 @@ async def test_dao_cat_exits( assert dao_cat_wallet_0.dao_cat_info.locked_coins[0].active_votes == [] exit = await client_0.dao_exit_lockup(dao_id_0, DEFAULT_TX_CONFIG) - exit_tx = TransactionRecord.from_json_dict(exit["tx"]) + exit_tx = exit.tx await full_node_api.wait_transaction_records_entered_mempool(records=[exit_tx], timeout=60) await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index 5ba098f76f23..ade3ca6a0828 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -991,7 +991,6 @@ async def test_message_spend(self, self_hostname, two_wallet_nodes, trusted): response = await api_0.did_message_spend( {"wallet_id": did_wallet_1.wallet_id, "coin_announcements": ["0abc"], "puzzle_announcements": ["0def"]} ) - assert "spend_bundle" in response spend = response["spend_bundle"].coin_spends[0] conditions = conditions_dict_for_solution( spend.puzzle_reveal.to_program(), diff --git a/tests/wallet/nft_wallet/test_nft_bulk_mint.py b/tests/wallet/nft_wallet/test_nft_bulk_mint.py index e8ffdeeced56..c2fd8ebf1607 100644 --- a/tests/wallet/nft_wallet/test_nft_bulk_mint.py +++ b/tests/wallet/nft_wallet/test_nft_bulk_mint.py @@ -1,7 +1,7 @@ from __future__ import annotations import random -from typing import Any, Dict +from typing import Any import pytest @@ -282,7 +282,7 @@ async def test_nft_mint_from_did_rpc( nft_ids = set() for i in range(0, n, chunk): await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_maker, timeout=20) - resp: Dict[str, Any] = await client.nft_mint_bulk( + resp = await client.nft_mint_bulk( wallet_id=nft_wallet_maker["wallet_id"], metadata_list=metadata_list[i : i + chunk], target_list=target_list[i : i + chunk], @@ -298,19 +298,18 @@ async def test_nft_mint_from_did_rpc( fee=fee, tx_config=DEFAULT_TX_CONFIG, ) - assert resp["success"] - sb: SpendBundle = SpendBundle.from_json_dict(resp["spend_bundle"]) + sb: SpendBundle = resp.spend_bundle did_lineage_parent = [cn for cn in sb.removals() if cn.name() == did_coin.name()][0].parent_coin_info.hex() did_coin = [cn for cn in sb.additions() if (cn.parent_coin_info == did_coin.name()) and (cn.amount == 1)][0] spends.append(sb) xch_adds = [c for c in sb.additions() if c.puzzle_hash == funding_coin.puzzle_hash] assert len(xch_adds) == 1 next_coin = xch_adds[0] - for nft_id in resp["nft_id_list"]: + for nft_id in resp.nft_id_list: nft_ids.add(decode_puzzle_hash(nft_id)) for sb in spends: - resp = await client_node.push_tx(sb) - assert resp["success"] + push_resp = await client_node.push_tx(sb) + assert push_resp["success"] await full_node_api.process_spend_bundles([sb]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) @@ -476,7 +475,7 @@ async def test_nft_mint_from_did_rpc_no_royalties( for i in range(0, n, chunk): await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_maker, timeout=20) - resp: Dict[str, Any] = await client.nft_mint_bulk( + resp = await client.nft_mint_bulk( wallet_id=nft_wallet_maker["wallet_id"], metadata_list=metadata_list[i : i + chunk], target_list=target_list[i : i + chunk], @@ -491,8 +490,7 @@ async def test_nft_mint_from_did_rpc_no_royalties( mint_from_did=True, tx_config=DEFAULT_TX_CONFIG, ) - assert resp["success"] - sb: SpendBundle = SpendBundle.from_json_dict(resp["spend_bundle"]) + sb: SpendBundle = resp.spend_bundle did_lineage_parent = [cn for cn in sb.removals() if cn.name() == did_coin.name()][0].parent_coin_info.hex() did_coin = [cn for cn in sb.additions() if (cn.parent_coin_info == did_coin.name()) and (cn.amount == 1)][0] spends.append(sb) @@ -501,8 +499,8 @@ async def test_nft_mint_from_did_rpc_no_royalties( next_coin = xch_adds[0] for sb in spends: - resp = await client_node.push_tx(sb) - assert resp["success"] + push_resp = await client_node.push_tx(sb) + assert push_resp["success"] await full_node_api.process_spend_bundles([sb]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) @@ -883,7 +881,7 @@ async def test_nft_mint_from_xch_rpc( for i in range(0, n, chunk): await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_maker, timeout=20) - resp: Dict[str, Any] = await client.nft_mint_bulk( + resp = await client.nft_mint_bulk( wallet_id=nft_wallet_maker["wallet_id"], metadata_list=metadata_list[i : i + chunk], target_list=target_list[i : i + chunk], @@ -897,16 +895,15 @@ async def test_nft_mint_from_xch_rpc( fee=fee, tx_config=DEFAULT_TX_CONFIG, ) - assert resp["success"] - sb: SpendBundle = SpendBundle.from_json_dict(resp["spend_bundle"]) + sb: SpendBundle = resp.spend_bundle spends.append(sb) xch_adds = [c for c in sb.additions() if c.puzzle_hash == funding_coin.puzzle_hash] assert len(xch_adds) == 1 next_coin = xch_adds[0] for sb in spends: - resp = await client_node.push_tx(sb) - assert resp["success"] + push_resp = await client_node.push_tx(sb) + assert push_resp["success"] await full_node_api.process_spend_bundles([sb]) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token)) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index d77ea79b9e5a..12a13e889960 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -310,19 +310,21 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment # Tests sending a basic transaction extra_conditions = (Remark(Program.to(("test", None))),) non_existent_coin = Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)) - tx_no_push = await client.send_transaction( - 1, - tx_amount, - addr, - memos=["this is a basic tx"], - tx_config=DEFAULT_TX_CONFIG.override( - excluded_coin_amounts=[uint64(250000000000)], - excluded_coin_ids=[non_existent_coin.name()], - reuse_puzhash=True, - ), - extra_conditions=extra_conditions, - push=False, - ) + tx_no_push = ( + await client.send_transaction( + 1, + tx_amount, + addr, + memos=["this is a basic tx"], + tx_config=DEFAULT_TX_CONFIG.override( + excluded_coin_amounts=[uint64(250000000000)], + excluded_coin_ids=[non_existent_coin.name()], + reuse_puzhash=True, + ), + extra_conditions=extra_conditions, + push=False, + ) + ).transaction response = await client.fetch( "send_transaction", { @@ -377,11 +379,13 @@ async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironmen outputs = await create_tx_outputs(wallet, [(1234321, None)]) - tx = await client.create_signed_transaction( - outputs, - tx_config=DEFAULT_TX_CONFIG, - fee=uint64(100), - ) + tx = ( + await client.create_signed_transactions( + outputs, + tx_config=DEFAULT_TX_CONFIG, + fee=uint64(100), + ) + ).signed_tx await client.push_transactions([tx]) resp = await client.fetch("push_transactions", {"transactions": [tx.to_json_dict_convenience(wallet_node.config)]}) @@ -547,17 +551,19 @@ async def test_create_signed_transaction( ) assert len(selected_coin) == 1 - tx = await wallet_1_rpc.create_signed_transaction( - outputs, - coins=selected_coin, - fee=amount_fee, - wallet_id=wallet_id, - # shouldn't actually block it - tx_config=DEFAULT_TX_CONFIG.override( - excluded_coin_amounts=[uint64(selected_coin[0].amount)] if selected_coin is not None else [], - ), - push=True, - ) + tx = ( + await wallet_1_rpc.create_signed_transactions( + outputs, + coins=selected_coin, + fee=amount_fee, + wallet_id=wallet_id, + # shouldn't actually block it + tx_config=DEFAULT_TX_CONFIG.override( + excluded_coin_amounts=[uint64(selected_coin[0].amount)] if selected_coin is not None else [], + ), + push=True, + ) + ).signed_tx change_expected = not selected_coin or selected_coin[0].amount - amount_total > 0 assert_tx_amounts(tx, outputs, amount_fee=amount_fee, change_expected=change_expected, is_cat=is_cat) @@ -625,9 +631,11 @@ async def test_create_signed_transaction_with_coin_announcement(wallet_rpc_envir ), ] outputs = await create_tx_outputs(wallet_2, [(signed_tx_amount, None)]) - tx_res: TransactionRecord = await client.create_signed_transaction( - outputs, tx_config=DEFAULT_TX_CONFIG, extra_conditions=(*tx_coin_announcements,) - ) + tx_res: TransactionRecord = ( + await client.create_signed_transactions( + outputs, tx_config=DEFAULT_TX_CONFIG, extra_conditions=(*tx_coin_announcements,) + ) + ).signed_tx assert_tx_amounts(tx_res, outputs, amount_fee=uint64(0), change_expected=True) await assert_push_tx_error(client_node, tx_res) @@ -656,9 +664,11 @@ async def test_create_signed_transaction_with_puzzle_announcement(wallet_rpc_env ), ] outputs = await create_tx_outputs(wallet_2, [(signed_tx_amount, None)]) - tx_res = await client.create_signed_transaction( - outputs, tx_config=DEFAULT_TX_CONFIG, extra_conditions=(*tx_puzzle_announcements,) - ) + tx_res = ( + await client.create_signed_transactions( + outputs, tx_config=DEFAULT_TX_CONFIG, extra_conditions=(*tx_puzzle_announcements,) + ) + ).signed_tx assert_tx_amounts(tx_res, outputs, amount_fee=uint64(0), change_expected=True) await assert_push_tx_error(client_node, tx_res) @@ -680,12 +690,14 @@ async def it_does_not_include_the_excluded_coins() -> None: assert len(selected_coins) == 1 outputs = await create_tx_outputs(wallet_1, [(uint64(250000000000), None)]) - tx = await wallet_1_rpc.create_signed_transaction( - outputs, - DEFAULT_TX_CONFIG.override( - excluded_coin_ids=[c.name() for c in selected_coins], - ), - ) + tx = ( + await wallet_1_rpc.create_signed_transactions( + outputs, + DEFAULT_TX_CONFIG.override( + excluded_coin_ids=[c.name() for c in selected_coins], + ), + ) + ).signed_tx assert len(tx.removals) == 1 assert tx.removals[0] != selected_coins[0] @@ -700,7 +712,7 @@ async def it_throws_an_error_when_all_spendable_coins_are_excluded() -> None: outputs = await create_tx_outputs(wallet_1, [(uint64(1750000000000), None)]) with pytest.raises(ValueError): - await wallet_1_rpc.create_signed_transaction( + await wallet_1_rpc.create_signed_transactions( outputs, DEFAULT_TX_CONFIG.override( excluded_coin_ids=[c.name() for c in selected_coins], @@ -730,26 +742,30 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron wallet_1_puzhash = await wallet_1.get_new_puzzlehash() await full_node_api.wait_for_wallet_synced(wallet_node=wallet_1_node, timeout=20) wallet_2_puzhash = await wallet_2.get_new_puzzlehash() - tx = await wallet_1_rpc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=encode_puzzle_hash(wallet_2_puzhash, "txch"), - tx_config=DEFAULT_TX_CONFIG, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], - ) + tx = ( + await wallet_1_rpc.send_transaction( + wallet_id=1, + amount=uint64(500), + address=encode_puzzle_hash(wallet_2_puzhash, "txch"), + tx_config=DEFAULT_TX_CONFIG, + fee=uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + ).transaction clawback_coin_id_1 = tx.additions[0].name() assert tx.spend_bundle is not None await farm_transaction(full_node_api, wallet_1_node, tx.spend_bundle) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_2_node, timeout=20) - tx = await wallet_2_rpc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=encode_puzzle_hash(wallet_1_puzhash, "txch"), - tx_config=DEFAULT_TX_CONFIG, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], - ) + tx = ( + await wallet_2_rpc.send_transaction( + wallet_id=1, + amount=uint64(500), + address=encode_puzzle_hash(wallet_1_puzhash, "txch"), + tx_config=DEFAULT_TX_CONFIG, + fee=uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + ).transaction assert tx.spend_bundle is not None clawback_coin_id_2 = tx.additions[0].name() await farm_transaction(full_node_api, wallet_2_node, tx.spend_bundle) @@ -792,8 +808,10 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron resp = await wallet_1_rpc.spend_clawback_coins([fake_coin.name()], 100) assert resp["transaction_ids"] == [] # Test coin puzzle hash doesn't match the puzzle - tx = (await wallet_1.wallet_state_manager.tx_store.get_farming_rewards())[0] - await wallet_1.wallet_state_manager.tx_store.add_transaction_record(dataclasses.replace(tx, name=fake_coin.name())) + farmed_tx = (await wallet_1.wallet_state_manager.tx_store.get_farming_rewards())[0] + await wallet_1.wallet_state_manager.tx_store.add_transaction_record( + dataclasses.replace(farmed_tx, name=fake_coin.name()) + ) await wallet_1_node.wallet_state_manager.coin_store.add_coin_record( dataclasses.replace(coin_record, coin=fake_coin) ) @@ -805,10 +823,10 @@ async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnviron assert resp["success"] assert len(resp["transaction_ids"]) == 2 for _tx in resp["transactions"]: - tx = TransactionRecord.from_json_dict_convenience(_tx) - if tx.spend_bundle is not None: + clawback_tx = TransactionRecord.from_json_dict_convenience(_tx) + if clawback_tx.spend_bundle is not None: await time_out_assert_not_none( - 10, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name() + 10, full_node_api.full_node.mempool_manager.get_spendbundle, clawback_tx.spend_bundle.name() ) await farm_transaction_block(full_node_api, wallet_2_node) await time_out_assert(20, get_confirmed_balance, generated_funds + 300, wallet_2_rpc, 1) @@ -837,13 +855,15 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir amount_outputs = sum(output["amount"] for output in outputs) amount_fee = uint64(amount_outputs + 1) - send_tx_res: TransactionRecord = await client.send_transaction_multi( - 1, - outputs, - DEFAULT_TX_CONFIG, - coins=removals, - fee=amount_fee, - ) + send_tx_res: TransactionRecord = ( + await client.send_transaction_multi( + 1, + outputs, + DEFAULT_TX_CONFIG, + coins=removals, + fee=amount_fee, + ) + ).transaction spend_bundle = send_tx_res.spend_bundle assert spend_bundle is not None assert send_tx_res is not None @@ -1078,8 +1098,8 @@ async def test_cat_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): ["the cat memo"], ) tx_res = await client.cat_spend(cat_0_id, DEFAULT_TX_CONFIG, uint64(4), addr_1, uint64(0), ["the cat memo"]) - assert tx_res.wallet_id == cat_0_id - spend_bundle = tx_res.spend_bundle + assert tx_res.transaction.wallet_id == cat_0_id + spend_bundle = tx_res.transaction.spend_bundle assert spend_bundle is not None assert uncurry_puzzle(spend_bundle.coin_spends[0].puzzle_reveal.to_program()).mod == CAT_MOD await farm_transaction(full_node_api, wallet_node, spend_bundle) @@ -1088,8 +1108,8 @@ async def test_cat_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): # Test CAT spend with a fee tx_res = await client.cat_spend(cat_0_id, DEFAULT_TX_CONFIG, uint64(1), addr_1, uint64(5_000_000), ["the cat memo"]) - assert tx_res.wallet_id == cat_0_id - spend_bundle = tx_res.spend_bundle + assert tx_res.transaction.wallet_id == cat_0_id + spend_bundle = tx_res.transaction.spend_bundle assert spend_bundle is not None assert uncurry_puzzle(spend_bundle.coin_spends[0].puzzle_reveal.to_program()).mod == CAT_MOD await farm_transaction(full_node_api, wallet_node, spend_bundle) @@ -1101,10 +1121,10 @@ async def test_cat_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): tx_res = await client.cat_spend( cat_0_id, DEFAULT_TX_CONFIG, uint64(1), addr_1, uint64(5_000_000), ["the cat memo"], removals=removals ) - assert tx_res.wallet_id == cat_0_id - spend_bundle = tx_res.spend_bundle + assert tx_res.transaction.wallet_id == cat_0_id + spend_bundle = tx_res.transaction.spend_bundle assert spend_bundle is not None - assert removals[0] in tx_res.removals + assert removals[0] in tx_res.transaction.removals assert uncurry_puzzle(spend_bundle.coin_spends[0].puzzle_reveal.to_program()).mod == CAT_MOD await farm_transaction(full_node_api, wallet_node, spend_bundle) @@ -1153,9 +1173,11 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) await wallet_2_rpc.create_wallet_for_existing_cat(cat_asset_id) wallet_2_address = await wallet_2_rpc.get_next_address(cat_wallet_id, False) adds = [{"puzzle_hash": decode_puzzle_hash(wallet_2_address), "amount": uint64(4), "memos": ["the cat memo"]}] - tx_res = await wallet_1_rpc.send_transaction_multi( - cat_wallet_id, additions=adds, tx_config=DEFAULT_TX_CONFIG, fee=uint64(0) - ) + tx_res = ( + await wallet_1_rpc.send_transaction_multi( + cat_wallet_id, additions=adds, tx_config=DEFAULT_TX_CONFIG, fee=uint64(0) + ) + ).transaction spend_bundle = tx_res.spend_bundle assert spend_bundle is not None await farm_transaction(full_node_api, wallet_node, spend_bundle) @@ -1168,22 +1190,21 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) with pytest.raises(ValueError): await wallet_1_rpc.get_coin_records_by_names([a.name() for a in spend_bundle.additions() if a.amount == 4]) # Create an offer of 5 chia for one CAT - offer, trade_record = await wallet_1_rpc.create_offer_for_ids( + await wallet_1_rpc.create_offer_for_ids( {uint32(1): -5, cat_asset_id.hex(): 1}, DEFAULT_TX_CONFIG, validate_only=True ) all_offers = await wallet_1_rpc.get_all_offers() assert len(all_offers) == 0 - assert offer is None driver_dict: Dict[str, Any] = {cat_asset_id.hex(): {"type": "CAT", "tail": "0x" + cat_asset_id.hex()}} - offer, trade_record = await wallet_1_rpc.create_offer_for_ids( + create_res = await wallet_1_rpc.create_offer_for_ids( {uint32(1): -5, cat_asset_id.hex(): 1}, DEFAULT_TX_CONFIG, driver_dict=driver_dict, fee=uint64(1), ) - assert offer is not None + offer = create_res.offer id, summary = await wallet_1_rpc.get_offer_summary(offer) assert id == offer.name() @@ -1211,7 +1232,7 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) assert TradeStatus(all_offers[0].status) == TradeStatus.PENDING_ACCEPT assert all_offers[0].offer == bytes(offer) - trade_record = await wallet_2_rpc.take_offer(offer, DEFAULT_TX_CONFIG, fee=uint64(1)) + trade_record = (await wallet_2_rpc.take_offer(offer, DEFAULT_TX_CONFIG, fee=uint64(1))).trade_record assert TradeStatus(trade_record.status) == TradeStatus.PENDING_CONFIRM await wallet_1_rpc.cancel_offer(offer.name(), DEFAULT_TX_CONFIG, secure=False) @@ -1225,11 +1246,12 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) trade_record = await wallet_1_rpc.get_offer(offer.name()) assert TradeStatus(trade_record.status) == TradeStatus.PENDING_CANCEL - new_offer, new_trade_record = await wallet_1_rpc.create_offer_for_ids( + create_res = await wallet_1_rpc.create_offer_for_ids( {uint32(1): -5, cat_wallet_id: 1}, DEFAULT_TX_CONFIG, fee=uint64(1) ) all_offers = await wallet_1_rpc.get_all_offers() assert len(all_offers) == 2 + new_trade_record = create_res.trade_record await farm_transaction_block(full_node_api, wallet_node) @@ -1373,7 +1395,7 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) # Spend half of it back to the same wallet get some spent coins in the wallet - tx = await client.send_transaction(1, uint64(generated_funds / 2), address, DEFAULT_TX_CONFIG) + tx = (await client.send_transaction(1, uint64(generated_funds / 2), address, DEFAULT_TX_CONFIG)).transaction assert tx.spend_bundle is not None await time_out_assert(20, tx_in_mempool, True, client, tx.name) await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) @@ -1464,8 +1486,8 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) await farm_transaction_block(full_node_api, wallet_1_node) # Update recovery list - res = await wallet_1_rpc.update_did_recovery_list(did_wallet_id_0, [did_id_0], 1, DEFAULT_TX_CONFIG) - assert res["success"] + update_res = await wallet_1_rpc.update_did_recovery_list(did_wallet_id_0, [did_id_0], 1, DEFAULT_TX_CONFIG) + assert len(update_res.transactions) > 0 res = await wallet_1_rpc.get_did_recovery_list(did_wallet_id_0) assert res["num_required"] == 1 assert res["recovery_list"][0] == did_id_0 @@ -1476,8 +1498,7 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): # Update metadata with pytest.raises(ValueError, match="wallet id 1 is of type Wallet but type DIDWallet is required"): await wallet_1_rpc.update_did_metadata(wallet_1_id, {"Twitter": "Https://test"}, DEFAULT_TX_CONFIG) - res = await wallet_1_rpc.update_did_metadata(did_wallet_id_0, {"Twitter": "Https://test"}, DEFAULT_TX_CONFIG) - assert res["success"] + await wallet_1_rpc.update_did_metadata(did_wallet_id_0, {"Twitter": "Https://test"}, DEFAULT_TX_CONFIG) await farm_transaction_block(full_node_api, wallet_1_node) @@ -1489,8 +1510,7 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): # Transfer DID addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch") - res = await wallet_1_rpc.did_transfer_did(did_wallet_id_0, addr, 0, True, DEFAULT_TX_CONFIG) - assert res["success"] + await wallet_1_rpc.did_transfer_did(did_wallet_id_0, addr, 0, True, DEFAULT_TX_CONFIG) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) await farm_transaction_block(full_node_api, wallet_1_node) @@ -1516,9 +1536,7 @@ async def num_wallets() -> int: assert metadata["Twitter"] == "Https://test" last_did_coin = await did_wallet_2.get_coin() - SpendBundle.from_json_dict( - (await wallet_2_rpc.did_message_spend(did_wallet_2.id(), DEFAULT_TX_CONFIG, push=True))["spend_bundle"] - ) + await wallet_2_rpc.did_message_spend(did_wallet_2.id(), DEFAULT_TX_CONFIG, push=True) await wallet_2_node.wallet_state_manager.add_interested_coin_ids([last_did_coin.name()]) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) @@ -1528,13 +1546,7 @@ async def num_wallets() -> int: assert next_did_coin.parent_coin_info == last_did_coin.name() last_did_coin = next_did_coin - SpendBundle.from_json_dict( - ( - await wallet_2_rpc.did_message_spend( - did_wallet_2.id(), DEFAULT_TX_CONFIG.override(reuse_puzhash=True), push=True - ) - )["spend_bundle"], - ) + await wallet_2_rpc.did_message_spend(did_wallet_2.id(), DEFAULT_TX_CONFIG.override(reuse_puzhash=True), push=True) await wallet_2_node.wallet_state_manager.add_interested_coin_ids([last_did_coin.name()]) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) @@ -1560,7 +1572,7 @@ async def test_nft_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): res = await wallet_1_rpc.create_new_nft_wallet(None) nft_wallet_id = res["wallet_id"] - res = await wallet_1_rpc.mint_nft( + mint_res = await wallet_1_rpc.mint_nft( nft_wallet_id, None, None, @@ -1568,9 +1580,8 @@ async def test_nft_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): ["https://www.chia.net/img/branding/chia-logo.svg"], DEFAULT_TX_CONFIG, ) - assert res["success"] - spend_bundle = SpendBundle.from_json_dict(json_dict=res["spend_bundle"]) + spend_bundle = mint_res.spend_bundle await farm_transaction(full_node_api, wallet_1_node, spend_bundle) @@ -1596,8 +1607,7 @@ async def have_nfts(): assert nft_info["nft_coin_id"][2:] == (await nft_wallet.get_current_nfts())[0].coin.name().hex() addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch") - res = await wallet_1_rpc.transfer_nft(nft_wallet_id, nft_id, addr, 0, DEFAULT_TX_CONFIG) - assert res["success"] + await wallet_1_rpc.transfer_nft(nft_wallet_id, nft_id, addr, 0, DEFAULT_TX_CONFIG) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) await farm_transaction_block(full_node_api, wallet_1_node) await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 0) @@ -1661,7 +1671,7 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn addr = encode_puzzle_hash(ph, "txch") tx_amount = uint64(15600000) await env.full_node.api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20) - created_tx = await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG) + created_tx = (await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG)).transaction await time_out_assert(20, tx_in_mempool, True, client, created_tx.name) assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 1 @@ -1791,7 +1801,7 @@ async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment for tx_amount in tx_amounts: funds -= tx_amount # create coins for tests - tx = await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG) + tx = (await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG)).transaction spend_bundle = tx.spend_bundle assert spend_bundle is not None for coin in spend_bundle.additions(): @@ -2354,14 +2364,16 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes wallet_node: WalletNode = env.wallet_1.node wallet_node_2: WalletNode = env.wallet_2.node # Test Clawback resync - tx = await wc.send_transaction( - wallet_id=1, - amount=uint64(500), - address=address, - tx_config=DEFAULT_TX_CONFIG, - fee=uint64(0), - puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], - ) + tx = ( + await wc.send_transaction( + wallet_id=1, + amount=uint64(500), + address=address, + tx_config=DEFAULT_TX_CONFIG, + fee=uint64(0), + puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}], + ) + ).transaction clawback_coin_id = tx.additions[0].name() assert tx.spend_bundle is not None await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) @@ -2509,7 +2521,7 @@ async def test_cat_spend_run_tail(wallet_rpc_environment: WalletRpcTestEnvironme ) tx_amount = uint64(100) - tx = await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG) + tx = (await client.send_transaction(1, tx_amount, addr, DEFAULT_TX_CONFIG)).transaction transaction_id = tx.name spend_bundle = tx.spend_bundle assert spend_bundle is not None @@ -2549,13 +2561,15 @@ async def test_cat_spend_run_tail(wallet_rpc_environment: WalletRpcTestEnvironme await time_out_assert(20, get_confirmed_balance, tx_amount, client, cat_wallet_id) # Attempt to melt it fully - tx = await client.cat_spend( - cat_wallet_id, - amount=uint64(0), - tx_config=DEFAULT_TX_CONFIG, - inner_address=encode_puzzle_hash(our_ph, "txch"), - cat_discrepancy=(tx_amount * -1, Program.to(None), Program.to(None)), - ) + tx = ( + await client.cat_spend( + cat_wallet_id, + amount=uint64(0), + tx_config=DEFAULT_TX_CONFIG, + inner_address=encode_puzzle_hash(our_ph, "txch"), + cat_discrepancy=(tx_amount * -1, Program.to(None), Program.to(None)), + ) + ).transaction transaction_id = tx.name spend_bundle = tx.spend_bundle assert spend_bundle is not None diff --git a/tests/wallet/vc_wallet/test_vc_wallet.py b/tests/wallet/vc_wallet/test_vc_wallet.py index ee6d8f888356..5eb82aba3ac5 100644 --- a/tests/wallet/vc_wallet/test_vc_wallet.py +++ b/tests/wallet/vc_wallet/test_vc_wallet.py @@ -52,16 +52,18 @@ async def mint_cr_cat( CAT_AMOUNT_0 = uint64(100) await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node_0, timeout=20) - tx = await client_0.create_signed_transaction( - [ - { - "puzzle_hash": cat_puzzle.get_tree_hash(), - "amount": CAT_AMOUNT_0, - } - ], - DEFAULT_TX_CONFIG, - wallet_id=1, - ) + tx = ( + await client_0.create_signed_transactions( + [ + { + "puzzle_hash": cat_puzzle.get_tree_hash(), + "amount": CAT_AMOUNT_0, + } + ], + DEFAULT_TX_CONFIG, + wallet_id=1, + ) + ).signed_tx spend_bundle = tx.spend_bundle assert spend_bundle is not None @@ -152,9 +154,11 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: ) # Mint a VC - vc_record, _ = await client_0.vc_mint( - did_id, wallet_environments.tx_config, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) - ) + vc_record = ( + await client_0.vc_mint( + did_id, wallet_environments.tx_config, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) + ) + ).vc_record await wallet_environments.process_pending_states( [ @@ -335,14 +339,16 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: assert await wallet_node_0.wallet_state_manager.get_wallet_for_asset_id(cr_cat_wallet_0.get_asset_id()) is not None wallet_1_ph = await wallet_1.get_new_puzzlehash() wallet_1_addr = encode_puzzle_hash(wallet_1_ph, "txch") - tx = await client_0.cat_spend( - cr_cat_wallet_0.id(), - wallet_environments.tx_config, - uint64(90), - wallet_1_addr, - uint64(2000000000), - memos=["hey"], - ) + tx = ( + await client_0.cat_spend( + cr_cat_wallet_0.id(), + wallet_environments.tx_config, + uint64(90), + wallet_1_addr, + uint64(2000000000), + memos=["hey"], + ) + ).transaction [tx] = await wallet_node_0.wallet_state_manager.add_pending_transactions([tx]) await wallet_environments.process_pending_states( [ @@ -506,14 +512,16 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None: ) # Test melting a CRCAT - tx = await client_1.cat_spend( - env_1.dealias_wallet_id("crcat"), - wallet_environments.tx_config, - uint64(20), - wallet_1_addr, - uint64(0), - cat_discrepancy=(-50, Program.to(None), Program.to(None)), - ) + tx = ( + await client_1.cat_spend( + env_1.dealias_wallet_id("crcat"), + wallet_environments.tx_config, + uint64(20), + wallet_1_addr, + uint64(0), + cat_discrepancy=(-50, Program.to(None), Program.to(None)), + ) + ).transaction [tx] = await wallet_node_1.wallet_state_manager.add_pending_transactions([tx]) await wallet_environments.process_pending_states( [ @@ -637,9 +645,11 @@ async def test_self_revoke(wallet_environments: WalletTestFramework) -> None: ) did_id: bytes32 = bytes32.from_hexstr(did_wallet.get_my_DID()) - vc_record, _ = await client_0.vc_mint( - did_id, wallet_environments.tx_config, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) - ) + vc_record = ( + await client_0.vc_mint( + did_id, wallet_environments.tx_config, target_address=await wallet_0.get_new_puzzlehash(), fee=uint64(200) + ) + ).vc_record await wallet_environments.process_pending_states( [ WalletStateTransition( From 8a76ae93a969e7572d04aa472966641fdd5192c5 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jan 2024 10:48:23 -0800 Subject: [PATCH 099/274] Introduce @tx_out_cmd decorator --- chia/cmds/cmds_util.py | 27 ++++++++++++++++++++ chia/cmds/coins.py | 2 ++ chia/cmds/dao.py | 10 ++++++++ chia/cmds/data.py | 4 +++ chia/cmds/plotnft.py | 4 +++ chia/cmds/wallet.py | 26 +++++++++++++++++-- chia/cmds/wallet_funcs.py | 34 +++++++++++++++---------- tests/cmds/wallet/test_tx_decorators.py | 27 ++++++++++++++++++++ tests/cmds/wallet/test_wallet.py | 17 ++++++++++--- 9 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 tests/cmds/wallet/test_tx_decorators.py diff --git a/chia/cmds/cmds_util.py b/chia/cmds/cmds_util.py index 82b6c402f4ab..8e40be7f86d1 100644 --- a/chia/cmds/cmds_util.py +++ b/chia/cmds/cmds_util.py @@ -309,6 +309,33 @@ def timelock_args(func: Callable[..., None]) -> Callable[..., None]: ) +@streamable +@dataclasses.dataclass(frozen=True) +class TransactionBundle(Streamable): + txs: List[TransactionRecord] + + +def tx_out_cmd(func: Callable[..., List[TransactionRecord]]) -> Callable[..., None]: + def original_cmd(transaction_file: Optional[str] = None, **kwargs: Any) -> None: + txs: List[TransactionRecord] = func(**kwargs) + if transaction_file is not None: + print(f"Writing transactions to file {transaction_file}:") + with open(Path(transaction_file), "wb") as file: + file.write(bytes(TransactionBundle(txs))) + + return click.option( + "--push/--no-push", help="Push the transaction to the network", type=bool, is_flag=True, default=True + )( + click.option( + "--transaction-file", + help="A file to write relevant transactions to", + type=str, + required=False, + default=None, + )(original_cmd) + ) + + @streamable @dataclasses.dataclass(frozen=True) class CMDCoinSelectionConfigLoader(Streamable): diff --git a/chia/cmds/coins.py b/chia/cmds/coins.py index d3d4f1e4d8dd..9eba3617a5dc 100644 --- a/chia/cmds/coins.py +++ b/chia/cmds/coins.py @@ -85,6 +85,7 @@ def list_cmd( ) +# MARK: tx_endpoint @coins_cmd.command("combine", help="Combine dust coins") @click.option( "-p", @@ -182,6 +183,7 @@ def combine_cmd( ) +# MARK: tx_endpoint @coins_cmd.command("split", help="Split up larger coins") @click.option( "-p", diff --git a/chia/cmds/dao.py b/chia/cmds/dao.py index b54d488be2d4..103b522bc328 100644 --- a/chia/cmds/dao.py +++ b/chia/cmds/dao.py @@ -66,6 +66,7 @@ def dao_add_cmd( # CREATE +# MARK: tx_endpoint @dao_cmd.command("create", short_help="Create a new DAO wallet and treasury", no_args_is_help=True) @click.option( "-wp", @@ -232,6 +233,7 @@ def dao_get_id_cmd( asyncio.run(get_treasury_id(extra_params, wallet_rpc_port, fingerprint)) +# MARK: tx_endpoint @dao_cmd.command("add_funds", short_help="Send funds to a DAO treasury", no_args_is_help=True) @click.option( "-wp", @@ -415,6 +417,7 @@ def dao_show_proposal_cmd( # VOTE +# MARK: tx_endpoint @dao_cmd.command("vote", short_help="Vote on a DAO proposal", no_args_is_help=True) @click.option( "-wp", @@ -492,6 +495,7 @@ def dao_vote_cmd( # CLOSE PROPOSALS +# MARK: tx_endpoint @dao_cmd.command("close_proposal", short_help="Close a DAO proposal", no_args_is_help=True) @click.option( "-wp", @@ -559,6 +563,7 @@ def dao_close_proposal_cmd( # LOCKUP COINS +# MARK: tx_endpoint @dao_cmd.command("lockup_coins", short_help="Lock DAO CATs for voting", no_args_is_help=True) @click.option( "-wp", @@ -613,6 +618,7 @@ def dao_lockup_coins_cmd( asyncio.run(lockup_coins(extra_params, wallet_rpc_port, fingerprint)) +# MARK: tx_endpoint @dao_cmd.command("release_coins", short_help="Release closed proposals from DAO CATs", no_args_is_help=True) @click.option( "-wp", @@ -658,6 +664,7 @@ def dao_release_coins_cmd( asyncio.run(release_coins(extra_params, wallet_rpc_port, fingerprint)) +# MARK: tx_endpoint @dao_cmd.command("exit_lockup", short_help="Release DAO CATs from voting mode", no_args_is_help=True) @click.option( "-wp", @@ -713,6 +720,7 @@ def dao_proposal(ctx: click.Context) -> None: pass +# MARK: tx_endpoint @dao_proposal.command("spend", short_help="Create a proposal to spend DAO funds", no_args_is_help=True) @click.option( "-wp", @@ -807,6 +815,7 @@ def dao_create_spend_proposal_cmd( asyncio.run(create_spend_proposal(extra_params, wallet_rpc_port, fingerprint)) +# MARK: tx_endpoint @dao_proposal.command("update", short_help="Create a proposal to change the DAO rules", no_args_is_help=True) @click.option( "-wp", @@ -916,6 +925,7 @@ def dao_create_update_proposal_cmd( asyncio.run(create_update_proposal(extra_params, wallet_rpc_port, fingerprint)) +# MARK: tx_endpoint @dao_proposal.command("mint", short_help="Create a proposal to mint new DAO CATs", no_args_is_help=True) @click.option( "-wp", diff --git a/chia/cmds/data.py b/chia/cmds/data.py index a28899b5433f..04828481179c 100644 --- a/chia/cmds/data.py +++ b/chia/cmds/data.py @@ -106,6 +106,7 @@ def create_fee_option() -> Callable[[FC], FC]: ) +# MARK: tx_endpoint @data_cmd.command("create_data_store", help="Create a new data store") @create_rpc_port_option() @create_fee_option() @@ -140,6 +141,7 @@ def get_value( run(get_value_cmd(data_rpc_port, id, key_string, root_hash, fingerprint=fingerprint)) +# MARK: tx_endpoint @data_cmd.command("update_data_store", help="Update a store by providing the changelist operations") @create_data_store_id_option() @create_changelist_option() @@ -340,6 +342,7 @@ def add_missing_files( ) +# MARK: tx_endpoint @data_cmd.command("add_mirror", help="Publish mirror urls on chain") @click.option("-i", "--id", help="Store id", type=str, required=True) @click.option( @@ -378,6 +381,7 @@ def add_mirror( ) +# MARK: tx_endpoint @data_cmd.command("delete_mirror", help="Delete an owned mirror by its coin id") @click.option("-c", "--coin_id", help="Coin id", type=str, required=True) @create_fee_option() diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py index 04744c4e9007..7d9e1c733033 100644 --- a/chia/cmds/plotnft.py +++ b/chia/cmds/plotnft.py @@ -53,6 +53,7 @@ def get_login_link_cmd(launcher_id: str) -> None: asyncio.run(get_login_link(launcher_id)) +# MARK: tx_endpoint @plotnft_cmd.command("create", help="Create a plot NFT") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @options.create_fingerprint() @@ -97,6 +98,7 @@ def create_cmd( asyncio.run(create(wallet_rpc_port, fingerprint, pool_url, valid_initial_states[state], Decimal(fee), dont_prompt)) +# MARK: tx_endpoint @plotnft_cmd.command("join", help="Join a plot NFT to a Pool") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @@ -138,6 +140,7 @@ def join_cmd( ) +# MARK: tx_endpoint @plotnft_cmd.command("leave", help="Leave a pool and return to self-farming") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @@ -193,6 +196,7 @@ def inspect(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None: asyncio.run(inspect_cmd(wallet_rpc_port, fingerprint, id)) +# MARK: tx_endpoint @plotnft_cmd.command("claim", help="Claim rewards from a plot NFT") @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @options.create_fingerprint() diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 372c9269a767..8a8f230de3ab 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -8,8 +8,10 @@ from chia.cmds import options from chia.cmds.check_wallet_db import help_text as check_help_text +from chia.cmds.cmds_util import tx_out_cmd from chia.cmds.coins import coins_cmd from chia.cmds.plotnft import validate_fee +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.address_type import AddressType from chia.wallet.util.wallet_types import WalletType @@ -131,6 +133,7 @@ def get_transactions_cmd( ) +# MARK: tx_endpoint @wallet_cmd.command("send", help="Send chia to another wallet") @click.option( "-wp", @@ -191,6 +194,7 @@ def get_transactions_cmd( type=int, default=0, ) +@tx_out_cmd def send_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -205,10 +209,11 @@ def send_cmd( coins_to_exclude: Sequence[str], reuse: bool, clawback_time: int, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import send - asyncio.run( + return asyncio.run( send( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -223,6 +228,7 @@ def send_cmd( excluded_coin_ids=coins_to_exclude, reuse_puzhash=True if reuse else None, clawback_time_lock=clawback_time, + push=push, ) ) @@ -275,6 +281,7 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id: int, fingerprint: int, n asyncio.run(get_address(wallet_rpc_port, fingerprint, id, new_address)) +# MARK: tx_endpoint @wallet_cmd.command( "clawback", help="Claim or revert a Clawback transaction." @@ -421,6 +428,7 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str asyncio.run(add_token(wallet_rpc_port, fingerprint, asset_id, token_name)) +# MARK: tx_endpoint @wallet_cmd.command("make_offer", help="Create an offer of XCH/CATs/NFTs for XCH/CATs/NFTs") @click.option( "-wp", @@ -531,6 +539,7 @@ def get_offers_cmd( ) +# MARK: tx_endpoint @wallet_cmd.command("take_offer", help="Examine or take an offer") @click.argument("path_or_hex", type=str, nargs=1, required=True) @click.option( @@ -564,6 +573,7 @@ def take_offer_cmd( asyncio.run(take_offer(wallet_rpc_port, fingerprint, Decimal(fee), path_or_hex, examine_only)) # reuse is not used +# MARK: tx_endpoint @wallet_cmd.command("cancel_offer", help="Cancel an existing offer") @click.option( "-wp", @@ -603,6 +613,7 @@ def did_cmd() -> None: pass +# MARK: tx_endpoint @did_cmd.command("create", help="Create DID wallet") @click.option( "-wp", @@ -713,6 +724,7 @@ def did_get_details_cmd(wallet_rpc_port: Optional[int], fingerprint: int, coin_i asyncio.run(get_did_info(wallet_rpc_port, fingerprint, coin_id, latest)) +# MARK: tx_endpoint @did_cmd.command("update_metadata", help="Update the metadata of a DID") @click.option( "-wp", @@ -786,6 +798,7 @@ def did_find_lost_cmd( ) +# MARK: tx_endpoint @did_cmd.command("message_spend", help="Generate a DID spend bundle for announcements") @click.option( "-wp", @@ -843,6 +856,7 @@ def did_message_spend_cmd( asyncio.run(did_message_spend(wallet_rpc_port, fingerprint, id, puzzle_list, coin_list)) +# MARK: tx_endpoint @did_cmd.command("transfer", help="Transfer a DID") @click.option( "-wp", @@ -945,6 +959,7 @@ def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: s ) +# MARK: tx_endpoint @nft_cmd.command("mint", help="Mint an NFT") @click.option( "-wp", @@ -1043,6 +1058,7 @@ def nft_mint_cmd( ) +# MARK: tx_endpoint @nft_cmd.command("add_uri", help="Add an URI to an NFT") @click.option( "-wp", @@ -1100,6 +1116,7 @@ def nft_add_uri_cmd( ) +# MARK: tx_endpoint @nft_cmd.command("transfer", help="Transfer an NFT") @click.option( "-wp", @@ -1249,6 +1266,7 @@ def notification_cmd() -> None: pass +# MARK: tx_endpoint @notification_cmd.command("send", help="Send a notification to the owner of an address") @click.option( "-wp", @@ -1334,6 +1352,7 @@ def vcs_cmd() -> None: # pragma: no cover pass +# MARK: tx_endpoint @vcs_cmd.command("mint", short_help="Mint a VC") @click.option( "-wp", @@ -1384,6 +1403,7 @@ def get_vcs_cmd( asyncio.run(get_vcs(wallet_rpc_port, fingerprint, start, count)) +# MARK: tx_endpoint @vcs_cmd.command("update_proofs", short_help="Update a VC's proofs if you have the provider DID") @click.option( "-wp", @@ -1477,6 +1497,7 @@ def get_proofs_for_root_cmd( asyncio.run(get_proofs_for_root(wallet_rpc_port, fingerprint, proof_hash)) +# MARK: tx_endpoint @vcs_cmd.command("revoke", short_help="Revoke any VC if you have the proper DID and the VCs parent coin") @click.option( "-wp", @@ -1522,6 +1543,7 @@ def revoke_vc_cmd( asyncio.run(revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, Decimal(fee), reuse_puzhash)) +# MARK: tx_endpoint @vcs_cmd.command("approve_r_cats", help="Claim any R-CATs that are currently pending VC approval") @click.option( "-wp", diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index adaac12045fc..f82e5dcc02d9 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -274,7 +274,8 @@ async def send( excluded_coin_ids: Sequence[str], reuse_puzhash: Optional[bool], clawback_time_lock: int, -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if memo is None: memos = None @@ -286,19 +287,19 @@ async def send( f"A transaction of amount {amount} and fee {fee} is unusual.\n" f"Pass in --override if you are sure you mean to do this." ) - return + return [] if amount == 0: print("You can not send an empty transaction") - return + return [] if clawback_time_lock < 0: print("Clawback time lock seconds cannot be negative.") - return + return [] try: typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(typ) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return + return [] final_fee: uint64 = uint64(int(fee * units["chia"])) # fees are always in XCH mojos final_amount: uint64 = uint64(int(amount * mojo_per_unit)) @@ -321,6 +322,7 @@ async def send( ] if clawback_time_lock > 0 else None, + push=push, ) elif typ in {WalletType.CAT, WalletType.CRCAT}: print("Submitting transaction...") @@ -336,24 +338,28 @@ async def send( address, final_fee, memos, + push=push, ) else: print("Only standard wallet and CAT wallets are supported") - return + return [] tx_id = res.transaction.name - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(wallet_id, tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id)) - return None + if push: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(wallet_id, tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, tx_id)) + return res.transactions print("Transaction not yet submitted to nodes") print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + return res.transactions + async def get_address(wallet_rpc_port: Optional[int], fp: Optional[int], wallet_id: int, new_address: bool) -> None: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): diff --git a/tests/cmds/wallet/test_tx_decorators.py b/tests/cmds/wallet/test_tx_decorators.py new file mode 100644 index 000000000000..ed2b4248a7de --- /dev/null +++ b/tests/cmds/wallet/test_tx_decorators.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Any, List + +import click +from click.testing import CliRunner + +from chia.cmds.cmds_util import TransactionBundle, tx_out_cmd +from chia.wallet.transaction_record import TransactionRecord +from tests.cmds.wallet.test_consts import STD_TX + + +def test_tx_out_cmd() -> None: + @click.command() + @tx_out_cmd + def test_cmd(**kwargs: Any) -> List[TransactionRecord]: + with open("./temp.push", "w") as file: + file.write(str(kwargs["push"])) + return [STD_TX, STD_TX] + + runner: CliRunner = CliRunner() + with runner.isolated_filesystem(): + runner.invoke(test_cmd, ["--transaction-file", "./temp.transaction"]) + with open("./temp.transaction", "rb") as file: + assert TransactionBundle.from_bytes(file.read()) == TransactionBundle([STD_TX, STD_TX]) + with open("./temp.push") as file2: + assert file2.read() == "True" diff --git a/tests/cmds/wallet/test_wallet.py b/tests/cmds/wallet/test_wallet.py index bec318f75aba..e06cc336666d 100644 --- a/tests/cmds/wallet/test_wallet.py +++ b/tests/cmds/wallet/test_wallet.py @@ -5,7 +5,9 @@ import pkg_resources from chia_rs import Coin, G2Element +from click.testing import CliRunner +from chia.cmds.cmds_util import TransactionBundle from chia.rpc.wallet_request_types import ( CancelOfferResponse, CATSpendResponse, @@ -404,9 +406,18 @@ async def cat_spend( "Transaction submitted to nodes: [{'peer_id': 'aaaaa'", f"-f 789101 -tx 0x{get_bytes32(2).hex()}", ] - - run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG], assert_list) - run_cli_command_and_assert(capsys, root_dir, command_args + [CAT_FINGERPRINT_ARG], cat_assert_list) + with CliRunner().isolated_filesystem(): + run_cli_command_and_assert( + capsys, root_dir, command_args + [FINGERPRINT_ARG] + ["--transaction-file=temp"], assert_list + ) + run_cli_command_and_assert( + capsys, root_dir, command_args + [CAT_FINGERPRINT_ARG] + ["--transaction-file=temp2"], cat_assert_list + ) + + with open("temp", "rb") as file: + assert TransactionBundle.from_bytes(file.read()) == TransactionBundle([STD_TX]) + with open("temp2", "rb") as file: + assert TransactionBundle.from_bytes(file.read()) == TransactionBundle([STD_TX]) # these are various things that should be in the output expected_calls: logType = { From 6ebde50ce98df8772c6198da8f4f34d6e31cf3ab Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jan 2024 12:02:25 -0800 Subject: [PATCH 100/274] Make execute_signing_instructions RPC --- chia/rpc/wallet_rpc_api.py | 15 +++++++++++++++ chia/rpc/wallet_rpc_client.py | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index d74c60f653cb..f87be945f864 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -23,6 +23,8 @@ from chia.rpc.wallet_request_types import ( ApplySignatures, ApplySignaturesResponse, + ExecuteSigningInstructions, + ExecuteSigningInstructionsResponse, GatherSigningInfo, GatherSigningInfoResponse, SubmitTransactions, @@ -297,6 +299,8 @@ def get_routes(self) -> Dict[str, Endpoint]: "/gather_signing_info": self.gather_signing_info, "/apply_signatures": self.apply_signatures, "/submit_transactions": self.submit_transactions, + # Not technically Signer Protocol but related + "/execute_signing_instructions": self.execute_signing_instructions, # VAULT "/vault_create": self.vault_create, } @@ -4554,6 +4558,17 @@ async def submit_transactions( await self.service.wallet_state_manager.submit_transactions(request.signed_transactions) ) + @marshal + async def execute_signing_instructions( + self, + request: ExecuteSigningInstructions, + ) -> ExecuteSigningInstructionsResponse: + return ExecuteSigningInstructionsResponse( + await self.service.wallet_state_manager.execute_signing_instructions( + request.signing_instructions, request.partial_allowed + ) + ) + ########################################################################################## # VAULT ########################################################################################## diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index e09fe2c36d61..4d00fe762796 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -25,6 +25,8 @@ DIDTransferDIDResponse, DIDUpdateMetadataResponse, DIDUpdateRecoveryIDsResponse, + ExecuteSigningInstructions, + ExecuteSigningInstructionsResponse, GatherSigningInfo, GatherSigningInfoResponse, NFTAddURIResponse, @@ -1665,6 +1667,14 @@ async def submit_transactions( ) ) + async def execute_signing_instructions( + self, + args: ExecuteSigningInstructions, + ) -> ExecuteSigningInstructionsResponse: + return ExecuteSigningInstructionsResponse.from_json_dict( + await self.fetch("execute_signing_instructions", args.to_json_dict()) + ) + async def vault_create( self, secp_pk: bytes, From 5e4286aaac0b380526e98c673edc134f6629db7b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jan 2024 10:26:47 -0800 Subject: [PATCH 101/274] Add transport layer support --- chia/rpc/util.py | 23 +++- chia/wallet/util/blind_signer_tl.py | 152 ++++++++++++++++++++++ chia/wallet/util/clvm_streamable.py | 119 +++++++++++++++-- tests/wallet/test_signer_protocol.py | 184 ++++++++++++++++++++++++++- 4 files changed, 461 insertions(+), 17 deletions(-) create mode 100644 chia/wallet/util/blind_signer_tl.py diff --git a/chia/rpc/util.py b/chia/rpc/util.py index e9ef03b463ff..2934280d3fac 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -17,7 +17,8 @@ from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.clvm_streamable import clvm_serialization_mode +from chia.wallet.util.blind_signer_tl import BLIND_SIGNER_TRANSPORT +from chia.wallet.util.clvm_streamable import TransportLayer, clvm_serialization_mode from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import TXConfig, TXConfigLoader @@ -30,6 +31,9 @@ MarshallableRpcEndpoint = Callable[..., Awaitable[Streamable]] +ALL_TRANSPORT_LAYERS: Dict[str, TransportLayer] = {"chip-TBD": BLIND_SIGNER_TRANSPORT} + + def marshal(func: MarshallableRpcEndpoint) -> RpcEndpoint: hints = get_type_hints(func) request_hint = hints["request"] @@ -43,7 +47,12 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: o *args, **kwargs, ) - with clvm_serialization_mode(not request.get("full_jsonify", False)): + compression: Optional[TransportLayer] = ( + None + if "compression" not in request or request["compression"] is None + else ALL_TRANSPORT_LAYERS[request["compression"]] + ) + with clvm_serialization_mode(not request.get("full_jsonify", False), compression): return response_obj.to_json_dict() return rpc_endpoint @@ -133,10 +142,16 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s ] unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs(tx_records) - if request.get("jsonify_unsigned_txs", False): + if request.get("full_jsonify", False): response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] else: - response["unsigned_transactions"] = [bytes(tx.as_program()).hex() for tx in unsigned_txs] + compression: Optional[TransportLayer] = ( + None + if "compression" not in request or request["compression"] is None + else ALL_TRANSPORT_LAYERS[request["compression"]] + ) + with clvm_serialization_mode(True, compression): + response["unsigned_transactions"] = [bytes(tx.as_program()).hex() for tx in unsigned_txs] new_txs: List[TransactionRecord] = [] if request.get("sign", self.service.config.get("auto_sign_txs", True)): diff --git a/chia/wallet/util/blind_signer_tl.py b/chia/wallet/util/blind_signer_tl.py new file mode 100644 index 000000000000..2cb9d6e21794 --- /dev/null +++ b/chia/wallet/util/blind_signer_tl.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from dataclasses import field +from typing import List + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 +from chia.wallet.signer_protocol import ( + KeyHints, + PathHint, + SigningInstructions, + SigningResponse, + SigningTarget, + SumHint, + TransactionInfo, + UnsignedTransaction, +) +from chia.wallet.util.clvm_streamable import ClvmStreamable, TransportLayer, TransportLayerMapping + +# Pylint doesn't understand that these classes are in fact dataclasses +# pylint: disable=invalid-field-call + + +class BSTLSigningTarget(ClvmStreamable): + fingerprint: bytes = field(metadata=dict(key="f")) + message: bytes = field(metadata=dict(key="m")) + hook: bytes32 = field(metadata=dict(key="h")) + + @staticmethod + def from_wallet_api(_from: SigningTarget) -> BSTLSigningTarget: + return BSTLSigningTarget(**_from.__dict__) + + @staticmethod + def to_wallet_api(_from: BSTLSigningTarget) -> SigningTarget: + return SigningTarget(**_from.__dict__) + + +class BSTLSumHint(ClvmStreamable): + fingerprints: List[bytes] = field(metadata=dict(key="f")) + synthetic_offset: bytes = field(metadata=dict(key="o")) + final_pubkey: bytes = field(metadata=dict(key="p")) + + @staticmethod + def from_wallet_api(_from: SumHint) -> BSTLSumHint: + return BSTLSumHint(**_from.__dict__) + + @staticmethod + def to_wallet_api(_from: BSTLSumHint) -> SumHint: + return SumHint(**_from.__dict__) + + +class BSTLPathHint(ClvmStreamable): + root_fingerprint: bytes = field(metadata=dict(key="f")) + path: List[uint64] = field(metadata=dict(key="p")) + + @staticmethod + def from_wallet_api(_from: PathHint) -> BSTLPathHint: + return BSTLPathHint(**_from.__dict__) + + @staticmethod + def to_wallet_api(_from: BSTLPathHint) -> PathHint: + return PathHint(**_from.__dict__) + + +class BSTLSigningInstructions(ClvmStreamable): + sum_hints: List[BSTLSumHint] = field(metadata=dict(key="s")) + path_hints: List[BSTLPathHint] = field(metadata=dict(key="p")) + targets: List[BSTLSigningTarget] = field(metadata=dict(key="t")) + + @staticmethod + def from_wallet_api(_from: SigningInstructions) -> BSTLSigningInstructions: + return BSTLSigningInstructions( + [BSTLSumHint(**sum_hint.__dict__) for sum_hint in _from.key_hints.sum_hints], + [BSTLPathHint(**path_hint.__dict__) for path_hint in _from.key_hints.path_hints], + [BSTLSigningTarget(**signing_target.__dict__) for signing_target in _from.targets], + ) + + @staticmethod + def to_wallet_api(_from: BSTLSigningInstructions) -> SigningInstructions: + return SigningInstructions( + KeyHints( + [SumHint(**sum_hint.__dict__) for sum_hint in _from.sum_hints], + [PathHint(**path_hint.__dict__) for path_hint in _from.path_hints], + ), + [SigningTarget(**signing_target.__dict__) for signing_target in _from.targets], + ) + + +class BSTLUnsignedTransaction(ClvmStreamable): + sum_hints: List[BSTLSumHint] = field(metadata=dict(key="s")) + path_hints: List[BSTLPathHint] = field(metadata=dict(key="p")) + targets: List[BSTLSigningTarget] = field(metadata=dict(key="t")) + + @staticmethod + def from_wallet_api(_from: UnsignedTransaction) -> BSTLUnsignedTransaction: + return BSTLUnsignedTransaction( + [BSTLSumHint(**sum_hint.__dict__) for sum_hint in _from.signing_instructions.key_hints.sum_hints], + [BSTLPathHint(**path_hint.__dict__) for path_hint in _from.signing_instructions.key_hints.path_hints], + [BSTLSigningTarget(**signing_target.__dict__) for signing_target in _from.signing_instructions.targets], + ) + + @staticmethod + def to_wallet_api(_from: BSTLUnsignedTransaction) -> UnsignedTransaction: + return UnsignedTransaction( + TransactionInfo([]), + SigningInstructions( + KeyHints( + [SumHint(**sum_hint.__dict__) for sum_hint in _from.sum_hints], + [PathHint(**path_hint.__dict__) for path_hint in _from.path_hints], + ), + [SigningTarget(**signing_target.__dict__) for signing_target in _from.targets], + ), + ) + + +class BSTLSigningResponse(ClvmStreamable): + signature: bytes = field(metadata=dict(key="s")) + hook: bytes32 = field(metadata=dict(key="h")) + + @staticmethod + def from_wallet_api(_from: SigningResponse) -> BSTLSigningResponse: + return BSTLSigningResponse(**_from.__dict__) + + @staticmethod + def to_wallet_api(_from: BSTLSigningResponse) -> SigningResponse: + return SigningResponse(**_from.__dict__) + + +BLIND_SIGNER_TRANSPORT = TransportLayer( + [ + TransportLayerMapping( + SigningTarget, BSTLSigningTarget, BSTLSigningTarget.from_wallet_api, BSTLSigningTarget.to_wallet_api + ), + TransportLayerMapping(SumHint, BSTLSumHint, BSTLSumHint.from_wallet_api, BSTLSumHint.to_wallet_api), + TransportLayerMapping(PathHint, BSTLPathHint, BSTLPathHint.from_wallet_api, BSTLPathHint.to_wallet_api), + TransportLayerMapping( + SigningInstructions, + BSTLSigningInstructions, + BSTLSigningInstructions.from_wallet_api, + BSTLSigningInstructions.to_wallet_api, + ), + TransportLayerMapping( + SigningResponse, BSTLSigningResponse, BSTLSigningResponse.from_wallet_api, BSTLSigningResponse.to_wallet_api + ), + TransportLayerMapping( + UnsignedTransaction, + BSTLUnsignedTransaction, + BSTLUnsignedTransaction.from_wallet_api, + BSTLUnsignedTransaction.to_wallet_api, + ), + ] +) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 664a8e291e08..a44051d3ea50 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from dataclasses import dataclass, fields from io import BytesIO -from typing import Any, BinaryIO, Callable, Dict, Iterator, Type, TypeVar +from typing import Any, BinaryIO, Callable, Dict, Generic, Iterator, List, Optional, Type, TypeVar, Union from hsms.clvm_serde import from_program_for_type, to_program_for_type from typing_extensions import dataclass_transform @@ -18,6 +18,7 @@ @dataclass class ClvmSerializationConfig: use: bool = False + transport_layer: Optional[TransportLayer] = None class _ClvmSerializationMode: @@ -33,9 +34,9 @@ def set_config(cls, config: ClvmSerializationConfig) -> None: @contextmanager -def clvm_serialization_mode(use: bool) -> Iterator[None]: +def clvm_serialization_mode(use: bool, transport_layer: Optional[TransportLayer] = None) -> Iterator[None]: old_config = _ClvmSerializationMode.get_config() - _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use)) + _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use, transport_layer=transport_layer)) yield _ClvmSerializationMode.set_config(old_config) @@ -59,6 +60,7 @@ def __init__(cls: ClvmStreamableMeta, *args: Any) -> None: _T_ClvmStreamable = TypeVar("_T_ClvmStreamable", bound="ClvmStreamable") +_T_TLClvmStreamable = TypeVar("_T_TLClvmStreamable", bound="ClvmStreamable") class ClvmStreamable(Streamable, metaclass=ClvmStreamableMeta): @@ -70,41 +72,136 @@ def from_program(cls: Type[_T_ClvmStreamable], prog: Program) -> _T_ClvmStreamab raise NotImplementedError() # pragma: no cover def stream(self, f: BinaryIO) -> None: + transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer + if transport_layer is not None: + new_self = transport_layer.serialize_for_transport(self) + else: + new_self = self + if _ClvmSerializationMode.get_config().use: - f.write(bytes(self.as_program())) + f.write(bytes(new_self.as_program())) else: super().stream(f) @classmethod def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: assert isinstance(f, BytesIO) + transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer + if transport_layer is not None: + cls_mapping: Optional[ + TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable] + ] = transport_layer.get_mapping(cls) + if cls_mapping is not None: + new_cls: Type[Union[_T_ClvmStreamable, ClvmStreamable]] = cls_mapping.to_type + else: + new_cls = cls + else: + new_cls = cls + try: - result = cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) + result = new_cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) f.read() - return result + if transport_layer is not None and cls_mapping is not None: + deserialized_result: _T_ClvmStreamable = cls_mapping.deserialize_function(result) + return deserialized_result + else: + assert isinstance(result, cls) + return result except Exception: return super().parse(f) def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], Dict[str, Any]]) -> Any: + transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer + if transport_layer is not None: + new_self = transport_layer.serialize_for_transport(self) + else: + new_self = self + if _ClvmSerializationMode.get_config().use: return bytes(self).hex() else: new_dict = {} - for field in fields(self): - new_dict[field.name] = default_recurse_jsonify(getattr(self, field.name)) + for field in fields(new_self): + new_dict[field.name] = default_recurse_jsonify(getattr(new_self, field.name)) return new_dict @classmethod def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStreamable: + transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer + if transport_layer is not None: + cls_mapping: Optional[ + TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable] + ] = transport_layer.get_mapping(cls) + if cls_mapping is not None: + new_cls: Type[Union[_T_ClvmStreamable, ClvmStreamable]] = cls_mapping.to_type + else: + new_cls = cls + else: + new_cls = cls + if isinstance(json_dict, str): try: byts = hexstr_to_bytes(json_dict) except ValueError as e: - raise ConversionError(json_dict, cls, e) + raise ConversionError(json_dict, new_cls, e) try: - return cls.from_program(Program.from_bytes(byts)) + result = new_cls.from_program(Program.from_bytes(byts)) + if transport_layer is not None and cls_mapping is not None: + deserialized_result: _T_ClvmStreamable = cls_mapping.deserialize_function(result) + return deserialized_result + else: + assert isinstance(result, cls) + return result except Exception as e: - raise ConversionError(json_dict, cls, e) + raise ConversionError(json_dict, new_cls, e) else: return super().from_json_dict(json_dict) + + +@dataclass(frozen=True) +class TransportLayerMapping(Generic[_T_ClvmStreamable, _T_TLClvmStreamable]): + from_type: Type[_T_ClvmStreamable] + to_type: Type[_T_TLClvmStreamable] + serialize_function: Callable[[_T_ClvmStreamable], _T_TLClvmStreamable] + deserialize_function: Callable[[_T_TLClvmStreamable], _T_ClvmStreamable] + + +@dataclass(frozen=True) +class TransportLayer: + type_mappings: List[TransportLayerMapping[Any, Any]] + + def get_mapping( + self, _type: Type[_T_ClvmStreamable] + ) -> Optional[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]]: + mappings: List[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ + m for m in self.type_mappings if m.from_type == _type + ] + if len(mappings) == 1: + return mappings[0] + elif len(mappings) == 0: + return None + else: + raise RuntimeError("Malformed TransportLayer") + + def serialize_for_transport(self, instance: _T_ClvmStreamable) -> ClvmStreamable: + mappings: List[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ + m for m in self.type_mappings if m.from_type == instance.__class__ + ] + if len(mappings) == 1: + return mappings[0].serialize_function(instance) + elif len(mappings) == 0: + return instance + else: + raise RuntimeError("Malformed TransportLayer") + + def deserialize_from_transport(self, instance: _T_ClvmStreamable) -> ClvmStreamable: + mappings: List[TransportLayerMapping[ClvmStreamable, _T_ClvmStreamable]] = [ + m for m in self.type_mappings if m.to_type == instance.__class__ + ] + if len(mappings) == 1: + return mappings[0].deserialize_function(instance) + elif len(mappings) == 0: + return instance + else: + raise RuntimeError("Malformed TransportLayer") diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 61c45ee4df79..12b0d79ffddd 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -9,7 +9,12 @@ import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey -from chia.rpc.wallet_request_types import ApplySignatures, GatherSigningInfo, SubmitTransactions +from chia.rpc.wallet_request_types import ( + ApplySignatures, + GatherSigningInfo, + GatherSigningInfoResponse, + SubmitTransactions, +) from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.types.blockchain_format.coin import Coin as ConsensusCoin from chia.types.blockchain_format.program import Program @@ -25,6 +30,7 @@ calculate_synthetic_offset, ) from chia.wallet.signer_protocol import ( + Coin, KeyHints, PathHint, SignedTransaction, @@ -36,7 +42,23 @@ TransactionInfo, UnsignedTransaction, ) -from chia.wallet.util.clvm_streamable import ClvmSerializationConfig, _ClvmSerializationMode, clvm_serialization_mode +from chia.wallet.util.blind_signer_tl import ( + BLIND_SIGNER_TRANSPORT, + BSTLPathHint, + BSTLSigningInstructions, + BSTLSigningResponse, + BSTLSigningTarget, + BSTLSumHint, + BSTLUnsignedTransaction, +) +from chia.wallet.util.clvm_streamable import ( + ClvmSerializationConfig, + ClvmStreamable, + TransportLayer, + TransportLayerMapping, + _ClvmSerializationMode, + clvm_serialization_mode, +) from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager @@ -184,6 +206,153 @@ async def get_and_check_config(use: bool, wait_before: int, wait_after: int) -> await get_and_check_config(False, 3, 5) +class FooSpend(ClvmStreamable): + coin: Coin + blah: Program + blah_also: Program = dataclasses.field(metadata=dict(key="solution")) # pylint: disable=invalid-field-call + + @staticmethod + def from_wallet_api(_from: Spend) -> FooSpend: + return FooSpend( + _from.coin, + _from.puzzle, + _from.solution, + ) + + @staticmethod + def to_wallet_api(_from: FooSpend) -> Spend: + return Spend( + _from.coin, + _from.blah, + _from.blah_also, + ) + + +def test_transport_layer() -> None: + FOO_TRANSPORT = TransportLayer( + [ + TransportLayerMapping( + Spend, + FooSpend, + FooSpend.from_wallet_api, + FooSpend.to_wallet_api, + ) + ] + ) + + spend = Spend( + Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)), + Program.to(1), + Program.to([]), + ) + + with clvm_serialization_mode(True): + spend_bytes = bytes(spend) + + spend_program = Program.from_bytes(spend_bytes) + assert spend_program.at("ff") == Program.to("coin") + assert spend_program.at("rff") == Program.to("puzzle") + assert spend_program.at("rrff") == Program.to("solution") + + with clvm_serialization_mode(True, FOO_TRANSPORT): + foo_spend_bytes = bytes(spend) + assert foo_spend_bytes.hex() == spend.to_json_dict() # type: ignore[comparison-overlap] + assert spend == Spend.from_bytes(foo_spend_bytes) + assert spend == Spend.from_json_dict(foo_spend_bytes.hex()) + + # Deserialization should only work now if using the transport layer + with pytest.raises(Exception): + Spend.from_bytes(foo_spend_bytes) + with pytest.raises(Exception): + Spend.from_json_dict(foo_spend_bytes.hex()) + + assert foo_spend_bytes != spend_bytes + foo_spend_program = Program.from_bytes(foo_spend_bytes) + assert foo_spend_program.at("ff") == Program.to("coin") + assert foo_spend_program.at("rff") == Program.to("blah") + assert foo_spend_program.at("rrff") == Program.to("solution") + + +def test_blind_signer_transport_layer() -> None: + sum_hints: List[SumHint] = [ + SumHint([b"a", b"b", b"c"], b"offset", b"final"), + SumHint([b"c", b"b", b"a"], b"offset2", b"final"), + ] + path_hints: List[PathHint] = [ + PathHint(b"root1", [uint64(1), uint64(2), uint64(3)]), + PathHint(b"root2", [uint64(4), uint64(5), uint64(6)]), + ] + signing_targets: List[SigningTarget] = [ + SigningTarget(b"pubkey", b"message", bytes32([0] * 32)), + SigningTarget(b"pubkey2", b"message2", bytes32([1] * 32)), + ] + + instructions: SigningInstructions = SigningInstructions( + KeyHints(sum_hints, path_hints), + signing_targets, + ) + transaction: UnsignedTransaction = UnsignedTransaction( + TransactionInfo([]), + instructions, + ) + signing_response: SigningResponse = SigningResponse( + b"signature", + bytes32([1] * 32), + ) + + bstl_sum_hints: List[BSTLSumHint] = [ + BSTLSumHint([b"a", b"b", b"c"], b"offset", b"final"), + BSTLSumHint([b"c", b"b", b"a"], b"offset2", b"final"), + ] + bstl_path_hints: List[BSTLPathHint] = [ + BSTLPathHint(b"root1", [uint64(1), uint64(2), uint64(3)]), + BSTLPathHint(b"root2", [uint64(4), uint64(5), uint64(6)]), + ] + bstl_signing_targets: List[BSTLSigningTarget] = [ + BSTLSigningTarget(b"pubkey", b"message", bytes32([0] * 32)), + BSTLSigningTarget(b"pubkey2", b"message2", bytes32([1] * 32)), + ] + + BSTLSigningInstructions( + bstl_sum_hints, + bstl_path_hints, + bstl_signing_targets, + ) + bstl_transaction: BSTLUnsignedTransaction = BSTLUnsignedTransaction( + bstl_sum_hints, + bstl_path_hints, + bstl_signing_targets, + ) + bstl_signing_response: BSTLSigningResponse = BSTLSigningResponse( + b"signature", + bytes32([1] * 32), + ) + with clvm_serialization_mode(True, None): + bstl_transaction_bytes = bytes(bstl_transaction) + bstl_signing_response_bytes = bytes(bstl_signing_response) + + with clvm_serialization_mode(True, BLIND_SIGNER_TRANSPORT): + transaction_bytes = bytes(transaction) + signing_response_bytes = bytes(signing_response) + assert transaction_bytes == bstl_transaction_bytes == bytes(bstl_transaction) + assert signing_response_bytes == bstl_signing_response_bytes == bytes(bstl_signing_response) + + # Deserialization should only work now if using the transport layer + with pytest.raises(Exception): + UnsignedTransaction.from_bytes(transaction_bytes) + with pytest.raises(Exception): + SigningResponse.from_bytes(signing_response_bytes) + + assert BSTLUnsignedTransaction.from_bytes(transaction_bytes) == bstl_transaction + assert BSTLSigningResponse.from_bytes(signing_response_bytes) == bstl_signing_response + with clvm_serialization_mode(True, BLIND_SIGNER_TRANSPORT): + assert UnsignedTransaction.from_bytes(transaction_bytes) == transaction + assert SigningResponse.from_bytes(signing_response_bytes) == signing_response + + assert Program.from_bytes(transaction_bytes).at("ff") == Program.to("s") + assert Program.from_bytes(signing_response_bytes).at("ff") == Program.to("s") + + @pytest.mark.parametrize( "wallet_environments", [ @@ -349,3 +518,14 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ), ] ) + + # And test that we can get compressed versions if we want + request = GatherSigningInfo( + [Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)] + ).to_json_dict() + response_dict = await wallet_rpc.fetch("gather_signing_info", {"compression": "chip-TBD", **request}) + with pytest.raises(Exception): + GatherSigningInfoResponse.from_json_dict(response_dict) + with clvm_serialization_mode(True, transport_layer=BLIND_SIGNER_TRANSPORT): + response: GatherSigningInfoResponse = GatherSigningInfoResponse.from_json_dict(response_dict) + assert response.signing_instructions == not_our_utx.signing_instructions From 6dafcd0d1c127a1365b5edfaeb85bfc1728b8a98 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jan 2024 10:29:36 -0800 Subject: [PATCH 102/274] Add signer commands --- chia/cmds/chia.py | 2 + chia/cmds/cmd_classes.py | 164 ++++++++++++ chia/cmds/signer.py | 267 +++++++++++++++++++ chia/rpc/wallet_rpc_client.py | 5 +- tests/cmds/test_cmd_framework.py | 208 +++++++++++++++ tests/wallet/test_signer_protocol.py | 366 ++++++++++++++++++++++++++- 6 files changed, 1008 insertions(+), 4 deletions(-) create mode 100644 chia/cmds/cmd_classes.py create mode 100644 chia/cmds/signer.py create mode 100644 tests/cmds/test_cmd_framework.py diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 71b6fce10cbb..cf67f4f843a1 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -134,6 +134,8 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: def main() -> None: + import chia.cmds.signer # noqa + cli() # pylint: disable=no-value-for-parameter diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py new file mode 100644 index 000000000000..91751ad12523 --- /dev/null +++ b/chia/cmds/cmd_classes.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import asyncio +import inspect +import sys +from contextlib import asynccontextmanager +from dataclasses import MISSING, Field, dataclass, field, fields +from typing import Any, AsyncIterator, Callable, Dict, Iterable, Optional, Protocol, Type, Union + +import click +from typing_extensions import dataclass_transform + +from chia.cmds.cmds_util import get_wallet_client +from chia.rpc.wallet_rpc_client import WalletRpcClient + +SyncCmd = Callable[..., None] + + +class SyncChiaCommand(Protocol): + def run(self) -> None: + ... + + +class AsyncChiaCommand(Protocol): + async def run(self) -> None: + ... + + +ChiaCommand = Union[SyncChiaCommand, AsyncChiaCommand] + + +def option(*param_decls: str, **kwargs: Any) -> Any: + if sys.version_info < (3, 10): # versions < 3.10 don't know about kw_only and they complain about lacks of defaults + # Can't get coverage on this because we only test on one version + default_default = None # pragma: no cover + else: + default_default = MISSING + + return field( # pylint: disable=invalid-field-call + metadata=dict( + is_command_option=True, + param_decls=tuple(param_decls), + **kwargs, + ), + default=kwargs["default"] if "default" in kwargs else default_default, + ) + + +def _apply_options(cmd: SyncCmd, _fields: Iterable[Field[Any]]) -> SyncCmd: + wrapped_cmd = cmd + for _field in _fields: + if "is_command_option" not in _field.metadata or not _field.metadata["is_command_option"]: + continue + wrapped_cmd = click.option( + *_field.metadata["param_decls"], + **{k: v for k, v in _field.metadata.items() if k not in ("param_decls", "is_command_option")}, + )(wrapped_cmd) + + return wrapped_cmd + + +@dataclass_transform() +def chia_command(cmd: click.Group, name: str, help: str) -> Callable[[Type[ChiaCommand]], Type[ChiaCommand]]: + def _chia_command(cls: Type[ChiaCommand]) -> Type[ChiaCommand]: + # The type ignores here are largely due to the fact that the class information is not preserved after being + # passed through the dataclass wrapper. Not sure what to do about this right now. + if sys.version_info < (3, 10): # pragma: no cover + # stuff below 3.10 doesn't know about kw_only + wrapped_cls: Type[ChiaCommand] = dataclass( # type: ignore[assignment] + frozen=True, + )(cls) + else: + wrapped_cls: Type[ChiaCommand] = dataclass( # type: ignore[assignment] + frozen=True, + kw_only=True, + )(cls) + cls_fields = fields(wrapped_cls) # type: ignore[arg-type] + if inspect.iscoroutinefunction(cls.run): + + async def async_base_cmd(*args: Any, **kwargs: Any) -> None: + await wrapped_cls(*args, **kwargs).run() # type: ignore[misc] + + def base_cmd(*args: Any, **kwargs: Any) -> None: + coro = async_base_cmd(*args, **kwargs) + assert coro is not None + asyncio.run(coro) + + else: + + def base_cmd(*args: Any, **kwargs: Any) -> None: + wrapped_cls(**kwargs).run() + + marshalled_cmd = base_cmd + if issubclass(wrapped_cls, NeedsContext): + + def strip_click_context(func: SyncCmd) -> SyncCmd: + def _inner(ctx: click.Context, **kwargs: Any) -> None: + context: Dict[str, Any] = ctx.obj if ctx.obj is not None else {} + func(context=context, **kwargs) + + return _inner + + marshalled_cmd = click.pass_context(strip_click_context(marshalled_cmd)) + marshalled_cmd = _apply_options(marshalled_cmd, cls_fields) + cmd.command(name, help=help)(marshalled_cmd) + return wrapped_cls + + return _chia_command + + +@dataclass_transform() +def command_helper(cls: Type[Any]) -> Type[Any]: + if sys.version_info < (3, 10): # stuff below 3.10 doesn't support kw_only + return dataclass(frozen=True)(cls) # pragma: no cover + else: + return dataclass(frozen=True, kw_only=True)(cls) + + +@command_helper +class NeedsContext: + context: Dict[str, Any] = field(default_factory=dict) # pylint: disable=invalid-field-call + + +@dataclass(frozen=True) +class WalletClientInfo: + client: WalletRpcClient + fingerprint: int + config: Dict[str, Any] + + +@command_helper +class NeedsWalletRPC(NeedsContext): + client_info: Optional[WalletClientInfo] = None + wallet_rpc_port: Optional[int] = option( + "-wp", + "--wallet-rpc_port", + help=( + "Set the port where the Wallet is hosting the RPC interface." + "See the rpc_port under wallet in config.yaml." + ), + type=int, + default=None, + ) + fingerprint: Optional[int] = option( + "-f", + "--fingerprint", + help="Fingerprint of the wallet to use", + type=int, + default=None, + ) + + @asynccontextmanager + async def wallet_rpc(self, **kwargs: Any) -> AsyncIterator[WalletClientInfo]: + if self.client_info is not None: + yield self.client_info + else: + if "root_path" not in kwargs: + kwargs["root_path"] = self.context["root_path"] # pylint: disable=unsubscriptable-object + async with get_wallet_client(self.wallet_rpc_port, self.fingerprint, **kwargs) as ( + wallet_client, + fp, + config, + ): + yield WalletClientInfo(wallet_client, fp, config) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py new file mode 100644 index 000000000000..f1ce1fc160cc --- /dev/null +++ b/chia/cmds/signer.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import itertools +import os +import time +from dataclasses import replace +from functools import cached_property +from pathlib import Path +from threading import Thread +from typing import List, Optional, Sequence, Type, TypeVar + +import click +from chia_rs import AugSchemeMPL, G2Element +from hsms.util.byte_chunks import create_chunks_for_blob, optimal_chunk_size_for_max_chunk_size +from segno import QRCode, make_qr + +from chia.cmds.cmd_classes import NeedsWalletRPC, chia_command, command_helper, option +from chia.cmds.cmds_util import TransactionBundle +from chia.cmds.wallet import wallet_cmd +from chia.rpc.util import ALL_TRANSPORT_LAYERS +from chia.rpc.wallet_request_types import ApplySignatures, ExecuteSigningInstructions, GatherSigningInfo +from chia.types.spend_bundle import SpendBundle +from chia.wallet.signer_protocol import SignedTransaction, SigningInstructions, SigningResponse, Spend +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.clvm_streamable import ClvmStreamable, clvm_serialization_mode + + +@wallet_cmd.group("signer", help="Get information for an external signer") +def signer_cmd() -> None: + pass + + +@command_helper +class QrCodeDisplay: + qr_density: int = option( + "--qr-density", + "-d", + type=int, + help="The maximum number of bytes contained in a single qr code", + default=100, + show_default=True, + ) + rotation_speed: int = option( + "--rotation-speed", + "-w", + type=int, + help="How many seconds delay between switching QR codes when there are multiple", + default=2, + show_default=True, + ) + + def display_qr_codes(self, blobs: List[bytes]) -> None: + chunk_sizes: List[int] = [optimal_chunk_size_for_max_chunk_size(len(blob), self.qr_density) for blob in blobs] + chunks: List[List[bytes]] = [ + create_chunks_for_blob(blob, chunk_size) for blob, chunk_size in zip(blobs, chunk_sizes) + ] + qr_codes: List[List[QRCode]] = [[make_qr(chunk) for chunk in chks] for chks in chunks] + + for i, qr_code_list in enumerate(qr_codes): + confirmation: Optional[str] = None + + def _display_qr(index: int, code_list: List[QRCode]) -> None: + for qr_code in itertools.cycle(code_list): + os.system("clear") + qr_code.terminal(compact=True) + print(f"Displaying QR Codes ({index+1}/{len(qr_codes)})") + print("") + for _ in range(0, self.rotation_speed * 100): + time.sleep(0.01) + if confirmation is not None: + return + + t = Thread(target=_display_qr, args=(i, qr_code_list)) + t.start() + try: + confirmation = input("") + finally: + confirmation = "" + t.join() + + +@command_helper +class TransactionsIn: + transaction_file_in: str = option( + "--transaction-file-in", + "-i", + type=str, + help="Transaction file to use as input", + required=True, + ) + + @cached_property + def transaction_bundle(self) -> TransactionBundle: + with open(Path(self.transaction_file_in), "rb") as file: + return TransactionBundle.from_bytes(file.read()) + + +@command_helper +class TransactionsOut: + transaction_file_out: str = option( + "--transaction-file-out", + "-o", + type=str, + help="Transaction filename to use as output", + required=True, + ) + + def handle_transaction_output(self, output: List[TransactionRecord]) -> None: + with open(Path(self.transaction_file_out), "wb") as file: + file.write(bytes(TransactionBundle(output))) + + +@command_helper +class _SPCompression: + compression: str = option( + "--compression", + "-c", + type=click.Choice(["none", "chip-TBD"]), + default="none", + help="Wallet Signer Protocol CHIP to use for compression of output", + ) + + +_T_ClvmStreamable = TypeVar("_T_ClvmStreamable", bound=ClvmStreamable) + + +@command_helper +class SPIn(_SPCompression): + signer_protocol_input: Sequence[str] = option( + "--signer-protocol-input", + "-p", + type=str, + help="Signer protocol objects (signatures, signing instructions, etc.) as files to load as input", + multiple=True, + required=True, + ) + + def read_sp_input(self, typ: Type[_T_ClvmStreamable]) -> List[_T_ClvmStreamable]: + final_list: List[_T_ClvmStreamable] = [] + for filename in self.signer_protocol_input: # pylint: disable=not-an-iterable + with open(Path(filename), "rb") as file: + with clvm_serialization_mode( + True, ALL_TRANSPORT_LAYERS[self.compression] if self.compression != "none" else None + ): + final_list.append(typ.from_bytes(file.read())) + + return final_list + + +@command_helper +class SPOut(QrCodeDisplay, _SPCompression): + output_format: str = option( + "--output-format", + "-t", + type=click.Choice(["hex", "file", "qr"]), + default="hex", + help="How to output the information to transfer to an external signer", + ) + output_file: Sequence[str] = option( + "--output-file", + "-b", + type=str, + multiple=True, + help="The file(s) to output to (if --output-format=file)", + ) + + def handle_clvm_output(self, outputs: List[ClvmStreamable]) -> None: + with clvm_serialization_mode( + True, ALL_TRANSPORT_LAYERS[self.compression] if self.compression != "none" else None + ): + if self.output_format == "hex": + for output in outputs: + print(bytes(output).hex()) + if self.output_format == "file": + if len(self.output_file) == 0: + print("--output-format=file specifed without any --output-file") + return + elif len(self.output_file) != len(outputs): + print( + "Incorrect number of file outputs specified, " + f"expected: {len(outputs)} got {len(self.output_file)}" + ) + return + else: + for filename, output in zip(self.output_file, outputs): + with open(Path(filename), "wb") as file: + file.write(bytes(output)) + if self.output_format == "qr": + self.display_qr_codes([bytes(output) for output in outputs]) + + +@chia_command( + signer_cmd, + "gather_signing_info", + "Gather the information from a transaction that a signer needs in order to create a signature", +) +class GatherSigningInfoCMD(SPOut, TransactionsIn, NeedsWalletRPC): + async def run(self) -> None: + async with self.wallet_rpc() as wallet_rpc: + spends: List[Spend] = [ + Spend.from_coin_spend(cs) + for tx in self.transaction_bundle.txs + if tx.spend_bundle is not None + for cs in tx.spend_bundle.coin_spends + ] + signing_instructions: SigningInstructions = ( + await wallet_rpc.client.gather_signing_info(GatherSigningInfo(spends=spends)) + ).signing_instructions + self.handle_clvm_output([signing_instructions]) + + +@chia_command(signer_cmd, "apply_signatures", "Apply a signer's signatures to a transaction bundle") +class ApplySignaturesCMD(TransactionsOut, SPIn, TransactionsIn, NeedsWalletRPC): + async def run(self) -> None: + async with self.wallet_rpc() as wallet_rpc: + signing_responses: List[SigningResponse] = self.read_sp_input(SigningResponse) + spends: List[Spend] = [ + Spend.from_coin_spend(cs) + for tx in self.transaction_bundle.txs + if tx.spend_bundle is not None + for cs in tx.spend_bundle.coin_spends + ] + signed_transactions: List[SignedTransaction] = ( + await wallet_rpc.client.apply_signatures( + ApplySignatures(spends=spends, signing_responses=signing_responses) + ) + ).signed_transactions + signed_spends: List[Spend] = [spend for tx in signed_transactions for spend in tx.transaction_info.spends] + final_signature: G2Element = G2Element() + for signature in [sig for tx in signed_transactions for sig in tx.signatures]: + if signature.type != "bls_12381_aug_scheme": + print("No external spot for non BLS signatures in a spend") + return + final_signature = AugSchemeMPL.aggregate([final_signature, G2Element.from_bytes(signature.signature)]) + new_spend_bundle: SpendBundle = SpendBundle( + [spend.as_coin_spend() for spend in signed_spends], final_signature + ) + new_transactions: List[TransactionRecord] = [ + replace(self.transaction_bundle.txs[0], spend_bundle=new_spend_bundle, name=new_spend_bundle.name()), + *(replace(tx, spend_bundle=None) for tx in self.transaction_bundle.txs), + ] + self.handle_transaction_output(new_transactions) + + +@chia_command(signer_cmd, "execute_signing_instructions", "Given some signing instructions, return signing responses") +class ExecuteSigningInstructionsCMD(SPOut, SPIn, NeedsWalletRPC): + async def run(self) -> None: + async with self.wallet_rpc() as wallet_rpc: + signing_instructions: List[SigningInstructions] = self.read_sp_input(SigningInstructions) + self.handle_clvm_output( + [ + signing_response + for instruction_set in signing_instructions + for signing_response in ( + await wallet_rpc.client.execute_signing_instructions( + ExecuteSigningInstructions(signing_instructions=instruction_set, partial_allowed=True) + ) + ).signing_responses + ] + ) + + +@chia_command(wallet_cmd, "push_transactions", "Push a transaction bundle to the wallet to send to the network") +class PushTransactionsCMD(TransactionsIn, NeedsWalletRPC): + async def run(self) -> None: + async with self.wallet_rpc() as wallet_rpc: + await wallet_rpc.client.push_transactions(self.transaction_bundle.txs) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 4d00fe762796..2366f46561ad 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -144,9 +144,10 @@ async def get_height_info(self) -> uint32: async def push_tx(self, spend_bundle: SpendBundle) -> Dict[str, Any]: return await self.fetch("push_tx", {"spend_bundle": bytes(spend_bundle).hex()}) - async def push_transactions(self, txs: List[TransactionRecord]) -> Dict[str, Any]: + async def push_transactions(self, txs: List[TransactionRecord], sign: bool = False) -> Dict[str, Any]: transactions = [bytes(tx).hex() for tx in txs] - return await self.fetch("push_transactions", {"transactions": transactions}) + + return await self.fetch("push_transactions", {"transactions": transactions, "sign": sign}) async def farm_block(self, address: str) -> Dict[str, Any]: return await self.fetch("farm_block", {"address": address}) diff --git a/tests/cmds/test_cmd_framework.py b/tests/cmds/test_cmd_framework.py new file mode 100644 index 000000000000..ed68099494c4 --- /dev/null +++ b/tests/cmds/test_cmd_framework.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +from dataclasses import asdict +from typing import Any, TypeVar + +import click +import pytest +from click.testing import CliRunner + +from chia.cmds.cmd_classes import ChiaCommand, NeedsContext, NeedsWalletRPC, chia_command, option +from tests.conftest import ConsensusMode +from tests.environments.wallet import WalletTestFramework +from tests.wallet.conftest import * # noqa + +_T_Command = TypeVar("_T_Command", bound=ChiaCommand) + + +def check_click_parsing(cmd: ChiaCommand, *args: str) -> None: + @click.group() + def _cmd() -> None: + pass + + mock_type = type(cmd.__class__.__name__, (cmd.__class__,), {}) + + def new_run(self: Any) -> None: + # cmd is appropriately not recognized as a dataclass but I'm not sure how to hint that something is a dataclass + other_dict = asdict(cmd) # type: ignore[call-overload] + for k, v in asdict(self).items(): + if k == "context": + continue + assert v == other_dict[k] + + setattr(mock_type, "run", new_run) + chia_command(_cmd, "_", "")(mock_type) + + runner = CliRunner() + result = runner.invoke(_cmd, ["_", *args], catch_exceptions=False) + assert result.output == "" + + +def test_cmd_bases() -> None: + @click.group() + def cmd() -> None: + pass + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD: + def run(self) -> None: + print("syncronous") + + @chia_command(cmd, "temp_cmd_async", "blah") + class TempCMDAsync: + async def run(self) -> None: + print("asyncronous") + + runner = CliRunner() + result = runner.invoke( + cmd, + ["--help"], + catch_exceptions=False, + ) + assert result.output == ( + "Usage: cmd [OPTIONS] COMMAND [ARGS]...\n\nOptions:\n --help Show this " + "message and exit.\n\nCommands:\n temp_cmd blah\n temp_cmd_async blah\n" + ) + result = runner.invoke( + cmd, + ["temp_cmd"], + catch_exceptions=False, + ) + assert result.output == "syncronous\n" + result = runner.invoke( + cmd, + ["temp_cmd_async"], + catch_exceptions=False, + ) + assert result.output == "asyncronous\n" + + +def test_option_loading() -> None: + @click.group() + def cmd() -> None: + pass + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD: + some_option: int = option("-o", "--some-option", required=True, type=int) + + def run(self) -> None: + print(self.some_option) + + @chia_command(cmd, "temp_cmd_2", "blah") + class TempCMD2: + some_option: int = option("-o", "--some-option", required=True, type=int, default=13) + + def run(self) -> None: + print(self.some_option) + + runner = CliRunner() + result = runner.invoke( + cmd, + ["temp_cmd"], + catch_exceptions=False, + ) + assert "Missing option '-o' / '--some-option'" in result.output + result = runner.invoke( + cmd, + [ + "temp_cmd", + "-o", + "13", + ], + catch_exceptions=False, + ) + assert "13\n" == result.output + result = runner.invoke( + cmd, + [ + "temp_cmd_2", + ], + catch_exceptions=False, + ) + assert "13\n" == result.output + + assert TempCMD2() == TempCMD2(some_option=13) + + +def test_context_requirement() -> None: + @click.group() + @click.pass_context + def cmd(ctx: click.Context) -> None: + ctx.obj = {"foo": "bar"} + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(NeedsContext): + def run(self) -> None: + assert self.context["foo"] == "bar" + + runner = CliRunner() + result = runner.invoke( + cmd, + ["temp_cmd"], + catch_exceptions=False, + ) + assert result.output == "" + + +@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="doesn't matter") +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + "trusted": True, + "reuse_puzhash": False, + } + ], + indirect=True, +) +@pytest.mark.anyio +async def test_wallet_rpc_helper(wallet_environments: WalletTestFramework) -> None: + port: int = wallet_environments.environments[0].rpc_client.port + + assert wallet_environments.environments[0].node.logged_in_fingerprint is not None + fingerprint: int = wallet_environments.environments[0].node.logged_in_fingerprint + + @click.group() + def cmd() -> None: + pass + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(NeedsWalletRPC): + def run(self) -> None: + pass + + runner = CliRunner() + result = runner.invoke( + cmd, + [ + "temp_cmd", + "-wp", + str(port), + "-f", + str(fingerprint), + ], + catch_exceptions=False, + ) + assert result.output == "" + + result = runner.invoke( + cmd, + [ + "temp_cmd", + ], + catch_exceptions=False, + ) + assert result.output == "" + + expected_command = TempCMD( + context={"root_path": wallet_environments.environments[0].node.root_path}, + wallet_rpc_port=port, + fingerprint=fingerprint, + ) + check_click_parsing(expected_command, "-wp", str(port), "-f", str(fingerprint)) + + async with expected_command.wallet_rpc(consume_errors=False) as client_info: + assert await client_info.client.get_logged_in_fingerprint() == fingerprint diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 12b0d79ffddd..ac326fe094ec 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -4,11 +4,28 @@ import dataclasses import threading import time +from threading import Thread from typing import List, Optional +import click import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey - +from click.testing import CliRunner + +from chia.cmds.cmd_classes import WalletClientInfo, chia_command +from chia.cmds.cmds_util import TransactionBundle +from chia.cmds.signer import ( + ApplySignaturesCMD, + ExecuteSigningInstructionsCMD, + GatherSigningInfoCMD, + PushTransactionsCMD, + QrCodeDisplay, + SPIn, + SPOut, + TransactionsIn, + TransactionsOut, +) +from chia.rpc.util import ALL_TRANSPORT_LAYERS from chia.rpc.wallet_request_types import ( ApplySignatures, GatherSigningInfo, @@ -59,9 +76,11 @@ _ClvmSerializationMode, clvm_serialization_mode, ) -from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG +from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager +from tests.cmds.test_cmd_framework import check_click_parsing +from tests.cmds.wallet.test_consts import STD_TX from tests.environments.wallet import WalletStateTransition, WalletTestFramework @@ -529,3 +548,346 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram with clvm_serialization_mode(True, transport_layer=BLIND_SIGNER_TRANSPORT): response: GatherSigningInfoResponse = GatherSigningInfoResponse.from_json_dict(response_dict) assert response.signing_instructions == not_our_utx.signing_instructions + + +async def test_signer_commands(wallet_environments: WalletTestFramework) -> None: + wallet: Wallet = wallet_environments.environments[0].xch_wallet + wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager + wallet_rpc: WalletRpcClient = wallet_environments.environments[0].rpc_client + client_info: WalletClientInfo = WalletClientInfo( + wallet_rpc, + wallet_state_manager.root_pubkey.get_fingerprint(), + wallet_state_manager.config, + ) + + AMOUNT = uint64(1) + [tx] = await wallet.generate_signed_transaction(AMOUNT, bytes32([0] * 32), DEFAULT_TX_CONFIG) + + runner = CliRunner() + with runner.isolated_filesystem(): + with open("./temp-tb", "wb") as file: + file.write(bytes(TransactionBundle([tx]))) + + await GatherSigningInfoCMD( + client_info=client_info, + transaction_file_in="./temp-tb", + compression="chip-TBD", + output_format="file", + output_file=["./temp-si"], + ).run() + + await ExecuteSigningInstructionsCMD( + client_info=client_info, + compression="chip-TBD", + signer_protocol_input=["./temp-si"], + output_format="file", + output_file=["./temp-sr"], + ).run() + + await ApplySignaturesCMD( + client_info=client_info, + transaction_file_in="./temp-tb", + compression="chip-TBD", + signer_protocol_input=["./temp-sr"], + transaction_file_out="./temp-stb", + ).run() + + await PushTransactionsCMD( + client_info=client_info, + transaction_file_in="./temp-stb", + ).run() + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "unconfirmed_wallet_balance": -1 * AMOUNT, + "<=#spendable_balance": -1 * AMOUNT, + "<=#max_send_amount": -1 * AMOUNT, + "pending_change": sum(c.amount for c in tx.removals) - AMOUNT, + "pending_coin_removal_count": 1, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -1 * AMOUNT, + "pending_change": -1 * (sum(c.amount for c in tx.removals) - AMOUNT), + "pending_coin_removal_count": -1, + "set_remainder": True, + }, + }, + ), + ] + ) + + +def test_signer_command_default_parsing() -> None: + check_click_parsing( + GatherSigningInfoCMD( + client_info=None, + wallet_rpc_port=None, + fingerprint=None, + transaction_file_in="in", + compression="none", + output_format="hex", + output_file=tuple(), + ), + "-i", + "in", + ) + + check_click_parsing( + ExecuteSigningInstructionsCMD( + client_info=None, + wallet_rpc_port=None, + fingerprint=None, + compression="none", + signer_protocol_input=("sp-in",), + output_format="hex", + output_file=tuple(), + ), + "-p", + "sp-in", + ) + + check_click_parsing( + ApplySignaturesCMD( + client_info=None, + wallet_rpc_port=None, + fingerprint=None, + transaction_file_in="in", + compression="none", + signer_protocol_input=("sp-in",), + transaction_file_out="out", + ), + "-i", + "in", + "-o", + "out", + "-p", + "sp-in", + ) + + check_click_parsing( + PushTransactionsCMD( + client_info=None, + transaction_file_in="in", + ), + "-i", + "in", + ) + + +def test_transactions_in() -> None: + @click.group() + def cmd() -> None: + pass + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(TransactionsIn): + def run(self) -> None: + assert self.transaction_bundle == TransactionBundle([STD_TX]) + + runner = CliRunner() + with runner.isolated_filesystem(): + with open("some file", "wb") as file: + file.write(bytes(TransactionBundle([STD_TX]))) + + result = runner.invoke(cmd, ["temp_cmd", "--transaction-file-in", "some file"], catch_exceptions=False) + assert result.output == "" + + +def test_transactions_out() -> None: + @click.group() + def cmd() -> None: + pass + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(TransactionsOut): + def run(self) -> None: + self.handle_transaction_output([STD_TX]) + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cmd, ["temp_cmd", "--transaction-file-out", "some file"], catch_exceptions=False) + assert result.output == "" + + with open("some file", "rb") as file: + file.read() == bytes(TransactionBundle([STD_TX])) + + +class FooCoin(ClvmStreamable): + amount: uint64 + + @staticmethod + def from_wallet_api(_from: Coin) -> FooCoin: + return FooCoin(_from.amount) + + @staticmethod + def to_wallet_api(_from: FooCoin) -> Coin: + return Coin( + bytes32([0] * 32), + bytes32([0] * 32), + _from.amount, + ) + + +FOO_COIN_TRANSPORT = TransportLayer( + [ + TransportLayerMapping( + Coin, + FooCoin, + FooCoin.from_wallet_api, + FooCoin.to_wallet_api, + ) + ] +) + + +def test_signer_protocol_in(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setitem(ALL_TRANSPORT_LAYERS, "chip-TBD", FOO_COIN_TRANSPORT) + + @click.group() + def cmd() -> None: + pass + + coin = Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(13)) + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(SPIn): + def run(self) -> None: + assert self.read_sp_input(Coin) == [coin, coin] + + runner = CliRunner() + with runner.isolated_filesystem(): + with open("some file", "wb") as file: + with clvm_serialization_mode(use=True): + file.write(bytes(coin)) + + with open("some file2", "wb") as file: + with clvm_serialization_mode(use=True): + file.write(bytes(coin)) + + result = runner.invoke( + cmd, + ["temp_cmd", "--signer-protocol-input", "some file", "--signer-protocol-input", "some file2"], + catch_exceptions=False, + ) + assert result.output == "" + + with runner.isolated_filesystem(): + with open("some file", "wb") as file: + with clvm_serialization_mode(use=True, transport_layer=FOO_COIN_TRANSPORT): + file.write(bytes(coin)) + + with open("some file2", "wb") as file: + with clvm_serialization_mode(use=True, transport_layer=FOO_COIN_TRANSPORT): + file.write(bytes(coin)) + + result = runner.invoke( + cmd, ["temp_cmd", "--signer-protocol-input", "some file", "--signer-protocol-input", "some file2"] + ) + assert result.exception is not None + result = runner.invoke( + cmd, + [ + "temp_cmd", + "--signer-protocol-input", + "some file", + "--signer-protocol-input", + "some file2", + "--compression", + "chip-TBD", + ], + catch_exceptions=False, + ) + assert result.output == "" + + +def test_signer_protocol_out(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setitem(ALL_TRANSPORT_LAYERS, "chip-TBD", FOO_COIN_TRANSPORT) + + @click.group() + def cmd() -> None: + pass + + coin = Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)) + with clvm_serialization_mode(use=True): + coin_bytes = bytes(coin) + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(SPOut): + def run(self) -> None: + self.handle_clvm_output([coin, coin]) + + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cmd, ["temp_cmd", "--output-format", "hex"], catch_exceptions=False) + assert result.output.strip() == coin_bytes.hex() + "\n" + coin_bytes.hex() + + result = runner.invoke(cmd, ["temp_cmd", "--output-format", "file"], catch_exceptions=False) + assert result.output == "--output-format=file specifed without any --output-file\n" + + result = runner.invoke( + cmd, ["temp_cmd", "--output-format", "file", "--output-file", "some file"], catch_exceptions=False + ) + assert "Incorrect number of file outputs specified" in result.output + + result = runner.invoke( + cmd, + ["temp_cmd", "--output-format", "file", "--output-file", "some file", "--output-file", "some file2"], + catch_exceptions=False, + ) + assert result.output == "" + + with open("some file", "rb") as file: + file.read() == coin_bytes + + with open("some file2", "rb") as file: + file.read() == coin_bytes + + result = runner.invoke(cmd, ["temp_cmd", "--output-format", "qr"], catch_exceptions=False) + assert result.output != "" # separate test for QrCodeDisplay + + result = runner.invoke( + cmd, ["temp_cmd", "--output-format", "hex", "--compression", "chip-TBD"], catch_exceptions=False + ) + assert result.output.strip() != coin_bytes.hex() + with clvm_serialization_mode(use=True, transport_layer=ALL_TRANSPORT_LAYERS["chip-TBD"]): + assert result.output.strip() == bytes(coin).hex() + "\n" + bytes(coin).hex() + + +def test_qr_code_display(monkeypatch: pytest.MonkeyPatch) -> None: + # We monkeypatch the start method to start the thread and then wait 5 seconds before returning + # This is so the thread has a change to print the QR code multiple times before the input from click is recieved + old_start = Thread.start + + def new_start(self, *args) -> None: # type: ignore[no-untyped-def] + old_start(self) + time.sleep(5) + + monkeypatch.setattr(Thread, "start", new_start) + + @click.group() + def cmd() -> None: + pass + + bytes_to_encode = b"foo bar qat qux bam bat" + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD(QrCodeDisplay): + def run(self) -> None: + self.display_qr_codes([bytes_to_encode, bytes_to_encode]) + + runner = CliRunner() + result = runner.invoke( + cmd, + ["temp_cmd", "--qr-density", str(int(len(bytes_to_encode) / 2)), "--rotation-speed", "3"], + input="\n", + catch_exceptions=False, + ) + + # Would be good to check eventually that the QR codes are valid but segno doesn't seem to provide that ATM + assert result.output.count("Displaying QR Codes (1/2)") == 2 + assert result.output.count("Displaying QR Codes (2/2)") == 2 From f8d034ddffdd610abb0772a85b23a09330f5d76c Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jan 2024 12:08:19 -0800 Subject: [PATCH 103/274] Rework wallet execute_signing_instructions --- chia/wallet/wallet.py | 143 ++++++++++----- tests/wallet/test_signer_protocol.py | 255 ++++++++++++++++++++++++--- 2 files changed, 332 insertions(+), 66 deletions(-) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 64aaecc1b1ab..2157b4224d85 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -30,7 +30,6 @@ from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE_HASH, - GROUP_ORDER, calculate_synthetic_offset, calculate_synthetic_secret_key, puzzle_for_pk, @@ -520,7 +519,7 @@ async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: return SumHint( [dr.pubkey.get_fingerprint().to_bytes(4, "big")], calculate_synthetic_offset(dr.pubkey, DEFAULT_HIDDEN_PUZZLE_HASH).to_bytes(32, "big"), - bytes(pk_parsed), + pk, ) async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: @@ -553,32 +552,43 @@ async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: root_pubkey: G1Element = self.wallet_state_manager.root_pubkey - pk_lookup: Dict[int, G1Element] = {root_pubkey.get_fingerprint(): root_pubkey} - sk_lookup: Dict[int, PrivateKey] = { - root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key() - } + pk_lookup: Dict[int, G1Element] = ( + {root_pubkey.get_fingerprint(): root_pubkey} if self.wallet_state_manager.private_key is not None else {} + ) + sk_lookup: Dict[int, PrivateKey] = ( + {root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key()} + if self.wallet_state_manager.private_key is not None + else {} + ) + aggregate_responses_at_end: bool = True + responses: List[SigningResponse] = [] - for path_hint in signing_instructions.key_hints.path_hints: - if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint(): - if not partial_allowed: - raise ValueError(f"No root pubkey for fingerprint {root_pubkey.get_fingerprint()}") + # TODO: expand path hints and sum hints recursively (a sum hint can give a new key to path hint) + # Next, expand our pubkey set with path hints + if self.wallet_state_manager.private_key is not None: + for path_hint in signing_instructions.key_hints.path_hints: + if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint(): + if not partial_allowed: + raise ValueError(f"No root pubkey for fingerprint {root_pubkey.get_fingerprint()}") + else: + continue else: - continue - else: - path = [int(step) for step in path_hint.path] - derive_child_sk = _derive_path(self.wallet_state_manager.get_master_private_key(), path) - derive_child_sk_unhardened = _derive_path_unhardened( - self.wallet_state_manager.get_master_private_key(), path - ) - derive_child_pk = derive_child_sk.get_g1() - derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1() - pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk - pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened - sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk - sk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_sk_unhardened - + path = [int(step) for step in path_hint.path] + derive_child_sk = _derive_path(self.wallet_state_manager.get_master_private_key(), path) + derive_child_sk_unhardened = _derive_path_unhardened( + self.wallet_state_manager.get_master_private_key(), path + ) + derive_child_pk = derive_child_sk.get_g1() + derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1() + pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk + pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened + sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk + sk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_sk_unhardened + + # Next, expand our pubkey set with sum hints + sum_hint_lookup: Dict[int, List[int]] = {} for sum_hint in signing_instructions.key_hints.sum_hints: - final_sk_int: int = 0 + fingerprints_we_have: List[int] = [] for fingerprint in sum_hint.fingerprints: fingerprint_as_int = int.from_bytes(fingerprint, "big") if fingerprint_as_int not in pk_lookup: @@ -588,39 +598,81 @@ async def execute_signing_instructions( f"fingerprint {int.from_bytes(fingerprint, 'big')}" ) else: - break + aggregate_responses_at_end = False else: - final_sk_int += int.from_bytes(bytes(sk_lookup[fingerprint_as_int]), "big") - else: # Only do this if we don't break - synthetic_secret_exponent = ( - final_sk_int + int.from_bytes(sum_hint.synthetic_offset, "big") - ) % GROUP_ORDER - blob = synthetic_secret_exponent.to_bytes(32, "big") - synthetic_sk = PrivateKey.from_bytes(blob) - synthetic_pk = synthetic_sk.get_g1() - pk_lookup[synthetic_pk.get_fingerprint()] = synthetic_pk - sk_lookup[synthetic_pk.get_fingerprint()] = synthetic_sk + fingerprints_we_have.append(fingerprint_as_int) + + # Add any synthetic offsets as keys we "have" + offset_sk = PrivateKey.from_bytes(sum_hint.synthetic_offset) + offset_pk = offset_sk.get_g1() + pk_lookup[offset_pk.get_fingerprint()] = offset_pk + sk_lookup[offset_pk.get_fingerprint()] = offset_sk + final_pubkey: G1Element = G1Element.from_bytes(sum_hint.final_pubkey) + final_fingerprint: int = final_pubkey.get_fingerprint() + pk_lookup[final_fingerprint] = final_pubkey + sum_hint_lookup[final_fingerprint] = [*fingerprints_we_have, offset_pk.get_fingerprint()] - responses: List[SigningResponse] = [] for target in signing_instructions.targets: pk_fingerprint: int = int.from_bytes(target.fingerprint, "big") - if pk_fingerprint not in sk_lookup: + if pk_fingerprint not in sk_lookup and pk_fingerprint not in sum_hint_lookup: if not partial_allowed: raise ValueError(f"Pubkey {pk_fingerprint} not found (or path/sum hinted to)") else: + aggregate_responses_at_end = False continue - responses.append( - SigningResponse( - bytes(AugSchemeMPL.sign(sk_lookup[pk_fingerprint], target.message)), - target.hook, + elif pk_fingerprint in sk_lookup: + responses.append( + SigningResponse( + bytes(AugSchemeMPL.sign(sk_lookup[pk_fingerprint], target.message)), + target.hook, + ) ) - ) + else: # Implicit if pk_fingerprint in sum_hint_lookup + signatures: List[G2Element] = [] + for partial_fingerprint in sum_hint_lookup[pk_fingerprint]: + signatures.append( + AugSchemeMPL.sign(sk_lookup[partial_fingerprint], target.message, pk_lookup[pk_fingerprint]) + ) + if partial_allowed: + # In multisig scenarios, we return everything as a component signature + for sig in signatures: + responses.append( + SigningResponse( + bytes(sig), + target.hook, + ) + ) + else: + # In the scenario where we are the only signer, we can collapse many responses into one + responses.append( + SigningResponse( + bytes(AugSchemeMPL.aggregate(signatures)), + target.hook, + ) + ) + + # If we have the full set of signing responses for the instructions, aggregate them as much as possible + if aggregate_responses_at_end: + new_responses: List[SigningResponse] = [] + grouped_responses: Dict[bytes32, List[SigningResponse]] = {} + for response in responses: + grouped_responses.setdefault(response.hook, []) + grouped_responses[response.hook].append(response) + for hook, group in grouped_responses.items(): + new_responses.append( + SigningResponse( + bytes(AugSchemeMPL.aggregate([G2Element.from_bytes(res.signature) for res in group])), + hook, + ) + ) + responses = new_responses return responses async def apply_signatures( self, spends: List[Spend], signing_responses: List[SigningResponse] ) -> SignedTransaction: + signing_responses_set = set(signing_responses) return SignedTransaction( TransactionInfo(spends), [ @@ -628,7 +680,10 @@ async def apply_signatures( "bls_12381_aug_scheme", bytes( AugSchemeMPL.aggregate( - [G2Element.from_bytes(signing_response.signature) for signing_response in signing_responses] + [ + G2Element.from_bytes(signing_response.signature) + for signing_response in signing_responses_set + ] ) ), ) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 61c45ee4df79..f915210c0ab7 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -16,10 +16,12 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend, make_spend from chia.types.spend_bundle import SpendBundle +from chia.util.hash import std_hash from chia.util.ints import uint64 from chia.util.streamable import ConversionError, Streamable, streamable from chia.wallet.conditions import AggSigMe from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.derive_keys import _derive_path_unhardened from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE_HASH, calculate_synthetic_offset, @@ -288,34 +290,39 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram with pytest.raises(ValueError, match=r"not found \(or path/sum hinted to\)"): await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) with pytest.raises(ValueError, match=r"No pubkey found \(or path hinted to\) for fingerprint"): - not_our_signing_instructions = dataclasses.replace( - not_our_signing_instructions, - key_hints=dataclasses.replace( - not_our_signing_instructions.key_hints, - sum_hints=[ - *not_our_signing_instructions.key_hints.sum_hints, - SumHint([bytes(not_our_pubkey)], b"", bytes(G1Element())), - ], - ), + await wallet_state_manager.execute_signing_instructions( + dataclasses.replace( + not_our_signing_instructions, + key_hints=dataclasses.replace( + not_our_signing_instructions.key_hints, + sum_hints=[ + *not_our_signing_instructions.key_hints.sum_hints, + SumHint([bytes(not_our_pubkey)], std_hash(b"sum hint only"), bytes(G1Element())), + ], + ), + ) ) - await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) with pytest.raises(ValueError, match="No root pubkey for fingerprint"): - not_our_signing_instructions = dataclasses.replace( - not_our_signing_instructions, - key_hints=dataclasses.replace( - not_our_signing_instructions.key_hints, - path_hints=[ - *not_our_signing_instructions.key_hints.path_hints, - PathHint(bytes(not_our_pubkey), [uint64(0)]), - ], - ), + await wallet_state_manager.execute_signing_instructions( + dataclasses.replace( + not_our_signing_instructions, + key_hints=dataclasses.replace( + not_our_signing_instructions.key_hints, + path_hints=[ + *not_our_signing_instructions.key_hints.path_hints, + PathHint(bytes(not_our_pubkey), [uint64(0)]), + ], + ), + ) ) - await wallet_state_manager.execute_signing_instructions(not_our_signing_instructions) signing_responses_2 = await wallet_state_manager.execute_signing_instructions( not_our_signing_instructions, partial_allowed=True ) - assert len(signing_responses_2) == 1 - assert signing_responses_2 == signing_responses + assert len(signing_responses_2) == 2 + assert ( + bytes(AugSchemeMPL.aggregate([G2Element.from_bytes(sig.signature) for sig in signing_responses_2])) + == signing_responses[0].signature + ) signed_txs: List[SignedTransaction] = ( await wallet_rpc.apply_signatures( @@ -349,3 +356,207 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram ), ] ) + + +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + "trusted": True, + "reuse_puzhash": True, + } + ], + indirect=True, +) +@pytest.mark.anyio +async def test_p2blsdohp_execute_signing_instructions(wallet_environments: WalletTestFramework) -> None: + wallet: Wallet = wallet_environments.environments[0].xch_wallet + root_sk: PrivateKey = wallet.wallet_state_manager.get_master_private_key() + root_pk: G1Element = root_sk.get_g1() + root_fingerprint: bytes = root_pk.get_fingerprint().to_bytes(4, "big") + + # Test just a path hint + test_name: bytes32 = std_hash(b"path hint only") + child_sk: PrivateKey = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)]) + signing_responses: List[SigningResponse] = await wallet.execute_signing_instructions( + SigningInstructions( + KeyHints( + [], + [PathHint(root_fingerprint, [uint64(1), uint64(2), uint64(3), uint64(4)])], + ), + [SigningTarget(child_sk.get_g1().get_fingerprint().to_bytes(4, "big"), test_name, test_name)], + ) + ) + assert signing_responses == [SigningResponse(bytes(AugSchemeMPL.sign(child_sk, test_name)), test_name)] + + # Test just a sum hint + test_name = std_hash(b"sum hint only") + other_sk: PrivateKey = PrivateKey.from_bytes(test_name) + sum_pk: G1Element = other_sk.get_g1() + root_pk + signing_instructions: SigningInstructions = SigningInstructions( + KeyHints( + [SumHint([root_fingerprint], test_name, bytes(sum_pk))], + [], + ), + [SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)], + ) + for partial_allowed in (True, False): + signing_responses = await wallet.execute_signing_instructions(signing_instructions, partial_allowed) + assert signing_responses == [ + SigningResponse( + bytes( + AugSchemeMPL.aggregate( + [ + AugSchemeMPL.sign(other_sk, test_name, sum_pk), + AugSchemeMPL.sign(root_sk, test_name, sum_pk), + ] + ) + ), + test_name, + ), + ] + # Toss in a random SigningTarget to see that the responses split up + signing_instructions = dataclasses.replace( + signing_instructions, + targets=[ + SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name), + SigningTarget(b"random fingerprint", test_name, test_name), + ], + ) + signing_responses = await wallet.execute_signing_instructions(signing_instructions, partial_allowed=True) + assert signing_responses == [ + SigningResponse( + bytes( + AugSchemeMPL.sign(root_sk, test_name, sum_pk), + ), + test_name, + ), + SigningResponse( + bytes( + AugSchemeMPL.sign(other_sk, test_name, sum_pk), + ), + test_name, + ), + ] + + # Test both path and sum hint + test_name = std_hash(b"path and sum hint") + child_sk = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)]) + other_sk = PrivateKey.from_bytes(test_name) + sum_pk = child_sk.get_g1() + other_sk.get_g1() + signing_instructions = SigningInstructions( + KeyHints( + [SumHint([child_sk.get_g1().get_fingerprint().to_bytes(4, "big")], test_name, bytes(sum_pk))], + [PathHint(root_fingerprint, [uint64(1), uint64(2), uint64(3), uint64(4)])], + ), + [SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)], + ) + for partial_allowed in (True, False): + signing_responses = await wallet.execute_signing_instructions(signing_instructions, partial_allowed) + assert signing_responses == [ + SigningResponse( + bytes( + AugSchemeMPL.aggregate( + [ + AugSchemeMPL.sign(other_sk, test_name, sum_pk), + AugSchemeMPL.sign(child_sk, test_name, sum_pk), + ] + ) + ), + test_name, + ), + ] + + # Test partial signing + test_name = std_hash(b"path and sum hint partial") + test_name_2 = std_hash(test_name) + root_sk_2 = PrivateKey.from_bytes(std_hash(b"a key we do not have")) + child_sk = _derive_path_unhardened(root_sk, [uint64(1), uint64(2), uint64(3), uint64(4)]) + child_sk_2 = _derive_path_unhardened(root_sk_2, [uint64(1), uint64(2), uint64(3), uint64(4)]) + other_sk = PrivateKey.from_bytes(test_name) + other_sk_2 = PrivateKey.from_bytes(test_name_2) + sum_pk = child_sk.get_g1() + other_sk.get_g1() + sum_pk_2 = child_sk_2.get_g1() + other_sk_2.get_g1() + signing_responses = await wallet.execute_signing_instructions( + SigningInstructions( + KeyHints( + [ + SumHint([child_sk.get_g1().get_fingerprint().to_bytes(4, "big")], test_name, bytes(sum_pk)), + SumHint([child_sk_2.get_g1().get_fingerprint().to_bytes(4, "big")], test_name_2, bytes(sum_pk_2)), + ], + [ + PathHint(root_fingerprint, [uint64(1), uint64(2), uint64(3), uint64(4)]), + PathHint( + root_sk_2.get_g1().get_fingerprint().to_bytes(4, "big"), + [uint64(1), uint64(2), uint64(3), uint64(4)], + ), + ], + ), + [ + SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name), + SigningTarget(sum_pk_2.get_fingerprint().to_bytes(4, "big"), test_name_2, test_name_2), + ], + ), + partial_allowed=True, + ) + assert signing_responses == [ + SigningResponse( + bytes( + AugSchemeMPL.sign(child_sk, test_name, sum_pk), + ), + test_name, + ), + SigningResponse( + bytes(AugSchemeMPL.sign(other_sk, test_name, sum_pk)), + test_name, + ), + SigningResponse(bytes(AugSchemeMPL.sign(other_sk_2, test_name_2, sum_pk_2)), test_name_2), + ] + + # Test errors + unknown_path_hint = SigningInstructions( + KeyHints( + [], + [PathHint(b"unknown fingerprint", [uint64(1), uint64(2), uint64(3), uint64(4)])], + ), + [], + ) + unknown_sum_hint = SigningInstructions( + KeyHints( + [SumHint([b"unknown fingerprint"], b"", bytes(G1Element()))], + [], + ), + [], + ) + unknown_target = SigningInstructions( + KeyHints( + [], + [], + ), + [SigningTarget(b"unknown fingerprint", b"", std_hash(b"some hook"))], + ) + with pytest.raises(ValueError, match="No root pubkey for fingerprint"): + await wallet.execute_signing_instructions(unknown_path_hint) + with pytest.raises(ValueError, match="No pubkey found"): + await wallet.execute_signing_instructions(unknown_sum_hint) + with pytest.raises(ValueError, match="not found"): + await wallet.execute_signing_instructions(unknown_target) + + # Test no private key partial sign sum hint + wallet.wallet_state_manager.private_key = None + test_name = std_hash(b"sum hint partial no private key") + other_sk = PrivateKey.from_bytes(test_name) + sum_pk = other_sk.get_g1() + root_pk + signing_responses = await wallet.execute_signing_instructions( + SigningInstructions( + KeyHints( + [SumHint([root_fingerprint], test_name, bytes(sum_pk))], + [], + ), + [SigningTarget(sum_pk.get_fingerprint().to_bytes(4, "big"), test_name, test_name)], + ), + partial_allowed=True, + ) + assert signing_responses == [SigningResponse(bytes(AugSchemeMPL.sign(other_sk, test_name, sum_pk)), test_name)] From bea085d265798537b15d99525ee4f61d12883ee9 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jan 2024 13:09:48 -0800 Subject: [PATCH 104/274] Inadvertent merge changes --- .github/workflows/build-windows-installer.yml | 2 +- chia/consensus/blockchain.py | 6 +-- chia/consensus/multiprocess_validation.py | 45 ++++++++++++++----- chia/full_node/full_node.py | 10 +++-- chia/rpc/wallet_rpc_client.py | 16 +++---- tests/blockchain/blockchain_test_utils.py | 4 +- tests/blockchain/test_blockchain.py | 8 ++-- tests/build-job-matrix.py | 2 +- .../full_node/stores/test_full_node_store.py | 4 +- tests/core/test_db_conversion.py | 4 +- tests/core/test_db_validation.py | 4 +- 11 files changed, 67 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 34e258090386..7e41a12f7cba 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -150,7 +150,7 @@ jobs: if: steps.check_secrets.outputs.HAS_SIGNING_SECRET shell: cmd run: | - curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi + curl -X GET https://download.chia.net/dc/smtools-windows-x64.msi -o smtools-windows-x64.msi msiexec /i smtools-windows-x64.msi /quiet /qn smksp_registrar.exe list smctl.exe keypair ls diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 30a5173b0852..d8678f8b88a7 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -728,7 +728,7 @@ async def validate_unfinished_block( required_iters, error = await self.validate_unfinished_block_header(block, skip_overflow_ss_validation) if error is not None: - return PreValidationResult(uint16(error.value), None, None, False) + return PreValidationResult(uint16(error.value), None, None, False, uint32(0)) prev_height = ( -1 @@ -753,9 +753,9 @@ async def validate_unfinished_block( ) if error_code is not None: - return PreValidationResult(uint16(error_code.value), None, None, False) + return PreValidationResult(uint16(error_code.value), None, None, False, uint32(0)) - return PreValidationResult(None, required_iters, cost_result, False) + return PreValidationResult(None, required_iters, cost_result, False, uint32(0)) async def pre_validate_blocks_multiprocessing( self, diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index 8fcfc765fa77..5bf2cc24e089 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -2,6 +2,7 @@ import asyncio import logging +import time import traceback from concurrent.futures import Executor from dataclasses import dataclass @@ -45,6 +46,7 @@ class PreValidationResult(Streamable): required_iters: Optional[uint64] # Iff error is None npc_result: Optional[NPCResult] # Iff error is None and block is a transaction block validated_signature: bool + timing: uint32 # the time (in milliseconds) it took to pre-validate the block def batch_pre_validate_blocks( @@ -70,6 +72,7 @@ def batch_pre_validate_blocks( if full_blocks_pickled is not None: for i in range(len(full_blocks_pickled)): try: + validation_start = time.monotonic() block: FullBlock = FullBlock.from_bytes(full_blocks_pickled[i]) tx_additions: List[Coin] = [] removals: List[bytes32] = [] @@ -97,7 +100,12 @@ def batch_pre_validate_blocks( ) removals, tx_additions = tx_removals_and_additions(npc_result.conds) if npc_result is not None and npc_result.error is not None: - results.append(PreValidationResult(uint16(npc_result.error), None, npc_result, False)) + validation_time = time.monotonic() - validation_start + results.append( + PreValidationResult( + uint16(npc_result.error), None, npc_result, False, uint32(validation_time * 1000) + ) + ) continue header_block = get_block_header(block, tx_additions, removals) @@ -132,17 +140,28 @@ def batch_pre_validate_blocks( else: successfully_validated_signatures = True + validation_time = time.monotonic() - validation_start results.append( - PreValidationResult(error_int, required_iters, npc_result, successfully_validated_signatures) + PreValidationResult( + error_int, + required_iters, + npc_result, + successfully_validated_signatures, + uint32(validation_time * 1000), + ) ) except Exception: error_stack = traceback.format_exc() log.error(f"Exception: {error_stack}") - results.append(PreValidationResult(uint16(Err.UNKNOWN.value), None, None, False)) + validation_time = time.monotonic() - validation_start + results.append( + PreValidationResult(uint16(Err.UNKNOWN.value), None, None, False, uint32(validation_time * 1000)) + ) # In this case, we are validating header blocks elif header_blocks_pickled is not None: for i in range(len(header_blocks_pickled)): try: + validation_start = time.monotonic() header_block = HeaderBlock.from_bytes(header_blocks_pickled[i]) required_iters, error = validate_finished_header_block( constants, @@ -155,11 +174,17 @@ def batch_pre_validate_blocks( error_int = None if error is not None: error_int = uint16(error.code.value) - results.append(PreValidationResult(error_int, required_iters, None, False)) + validation_time = time.monotonic() - validation_start + results.append( + PreValidationResult(error_int, required_iters, None, False, uint32(validation_time * 1000)) + ) except Exception: + validation_time = time.monotonic() - validation_start error_stack = traceback.format_exc() log.error(f"Exception: {error_stack}") - results.append(PreValidationResult(uint16(Err.UNKNOWN.value), None, None, False)) + results.append( + PreValidationResult(uint16(Err.UNKNOWN.value), None, None, False, uint32(validation_time * 1000)) + ) return [bytes(r) for r in results] @@ -199,7 +224,7 @@ async def pre_validate_blocks_multiprocessing( if blocks[0].height > 0: curr = await block_records.get_block_record_from_db(blocks[0].prev_header_hash) if curr is None: - return [PreValidationResult(uint16(Err.INVALID_PREV_BLOCK_HASH.value), None, None, False)] + return [PreValidationResult(uint16(Err.INVALID_PREV_BLOCK_HASH.value), None, None, False, uint32(0))] num_sub_slots_to_look_for = 3 if curr.overflow else 2 header_hash = curr.header_hash while ( @@ -270,7 +295,7 @@ async def pre_validate_blocks_multiprocessing( for i, block_i in enumerate(blocks): if not block_record_was_present[i] and block_records.contains_block(block_hashes[i]): block_records.remove_block_record(block_hashes[i]) - return [PreValidationResult(uint16(Err.INVALID_POSPACE.value), None, None, False)] + return [PreValidationResult(uint16(Err.INVALID_POSPACE.value), None, None, False, uint32(0))] required_iters: uint64 = calculate_iterations_quality( constants.DIFFICULTY_CONSTANT_FACTOR, @@ -289,14 +314,14 @@ async def pre_validate_blocks_multiprocessing( None, ) except ValueError: - return [PreValidationResult(uint16(Err.INVALID_SUB_EPOCH_SUMMARY.value), None, None, False)] + return [PreValidationResult(uint16(Err.INVALID_SUB_EPOCH_SUMMARY.value), None, None, False, uint32(0))] if block_rec.sub_epoch_summary_included is not None and wp_summaries is not None: idx = int(block.height / constants.SUB_EPOCH_BLOCKS) - 1 next_ses = wp_summaries[idx] if not block_rec.sub_epoch_summary_included.get_hash() == next_ses.get_hash(): log.error("sub_epoch_summary does not match wp sub_epoch_summary list") - return [PreValidationResult(uint16(Err.INVALID_SUB_EPOCH_SUMMARY.value), None, None, False)] + return [PreValidationResult(uint16(Err.INVALID_SUB_EPOCH_SUMMARY.value), None, None, False, uint32(0))] # Makes sure to not override the valid blocks already in block_records if not block_records.contains_block(block_rec.header_hash): block_records.add_block_record(block_rec) # Temporarily add block to dict @@ -346,7 +371,7 @@ async def pre_validate_blocks_multiprocessing( except ValueError: return [ PreValidationResult( - uint16(Err.FAILED_GETTING_GENERATOR_MULTIPROCESSING.value), None, None, False + uint16(Err.FAILED_GETTING_GENERATOR_MULTIPROCESSING.value), None, None, False, uint32(0) ) ] if block_generator is not None: diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 6eaa505b0da3..2e2ff0703fce 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1288,7 +1288,8 @@ async def add_block_batch( self.log.log( logging.WARNING if pre_validate_time > 10 else logging.DEBUG, - f"Block pre-validation time: {pre_validate_end - pre_validate_start:0.2f} seconds " + f"Block pre-validation: {pre_validate_end - pre_validate_start:0.2f}s " + f"CLVM: {sum([pvr.timing/1000.0 for pvr in pre_validation_results]):0.2f}s " f"({len(blocks_to_validate)} blocks, start height: {blocks_to_validate[0].height})", ) for i, block in enumerate(blocks_to_validate): @@ -1804,9 +1805,10 @@ async def add_block( ) self.log.log( logging.WARNING if validation_time > 2 else logging.DEBUG, - f"Block validation time: {validation_time:0.2f} seconds, " - f"pre_validation time: {pre_validation_time:0.2f} seconds, " - f"post-process time: {post_process_time:0.2f} seconds, " + f"Block validation: {validation_time:0.2f}s, " + f"pre_validation: {pre_validation_time:0.2f}s, " + f"CLVM: {pre_validation_results[0].timing/1000.0:0.2f}s, " + f"post-process: {post_process_time:0.2f}s, " f"cost: {block.transactions_info.cost if block.transactions_info is not None else 'None'}" f"{percent_full_str} header_hash: {header_hash.hex()} height: {block.height}", ) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 98d6497daee4..d21f87927e95 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -436,7 +436,7 @@ async def update_did_recovery_list( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "new_list": recovery_list, "num_verifications_required": num_verification, @@ -461,7 +461,7 @@ async def did_message_spend( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = False, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "extra_conditions": conditions_to_json_dicts(extra_conditions), "push": push, @@ -480,7 +480,7 @@ async def update_did_metadata( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "metadata": metadata, "extra_conditions": conditions_to_json_dicts(extra_conditions), @@ -556,7 +556,7 @@ async def did_transfer_did( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "inner_address": address, "fee": fee, @@ -920,7 +920,7 @@ async def mint_nft( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "royalty_address": royalty_address, "target_address": target_address, @@ -955,7 +955,7 @@ async def add_uri_to_nft( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "nft_coin_id": nft_coin_id, "uri": uri, @@ -1001,7 +1001,7 @@ async def transfer_nft( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "nft_coin_id": nft_coin_id, "target_address": target_address, @@ -1035,7 +1035,7 @@ async def set_nft_did( timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, ) -> Dict[str, Any]: - request: Dict[str, Any] = { + request = { "wallet_id": wallet_id, "did_id": did_id, "nft_coin_id": nft_coin_id, diff --git a/tests/blockchain/blockchain_test_utils.py b/tests/blockchain/blockchain_test_utils.py index 19654b37ec56..ea39623dc86c 100644 --- a/tests/blockchain/blockchain_test_utils.py +++ b/tests/blockchain/blockchain_test_utils.py @@ -7,7 +7,7 @@ from chia.consensus.multiprocess_validation import PreValidationResult from chia.types.full_block import FullBlock from chia.util.errors import Err -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 async def check_block_store_invariant(bc: Blockchain): @@ -56,7 +56,7 @@ async def _validate_and_add_block( await check_block_store_invariant(blockchain) if skip_prevalidation: - results = PreValidationResult(None, uint64(1), None, False) + results = PreValidationResult(None, uint64(1), None, False, uint32(0)) else: # Do not change this, validate_signatures must be False pre_validation_results: List[PreValidationResult] = await blockchain.pre_validate_blocks_multiprocessing( diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index 63010728939f..aa3b001ced19 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -2543,7 +2543,7 @@ async def test_cost_exceeds_max(self, empty_blockchain, softfork_height, bt): height=softfork_height, constants=bt.constants, ) - err = (await b.add_block(blocks[-1], PreValidationResult(None, uint64(1), npc_result, True)))[1] + err = (await b.add_block(blocks[-1], PreValidationResult(None, uint64(1), npc_result, True, uint32(0))))[1] assert err in [Err.BLOCK_COST_EXCEEDS_MAX] results: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing( @@ -2604,7 +2604,7 @@ async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height, bt height=softfork_height, constants=bt.constants, ) - result, err, _ = await b.add_block(block_2, PreValidationResult(None, uint64(1), npc_result, False)) + result, err, _ = await b.add_block(block_2, PreValidationResult(None, uint64(1), npc_result, False, uint32(0))) assert err == Err.INVALID_BLOCK_COST # too low @@ -2629,7 +2629,7 @@ async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height, bt height=softfork_height, constants=bt.constants, ) - result, err, _ = await b.add_block(block_2, PreValidationResult(None, uint64(1), npc_result, False)) + result, err, _ = await b.add_block(block_2, PreValidationResult(None, uint64(1), npc_result, False, uint32(0))) assert err == Err.INVALID_BLOCK_COST # too high @@ -2655,7 +2655,7 @@ async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height, bt constants=bt.constants, ) - result, err, _ = await b.add_block(block_2, PreValidationResult(None, uint64(1), npc_result, False)) + result, err, _ = await b.add_block(block_2, PreValidationResult(None, uint64(1), npc_result, False, uint32(0))) assert err == Err.INVALID_BLOCK_COST # when the CLVM program exceeds cost during execution, it will fail with diff --git a/tests/build-job-matrix.py b/tests/build-job-matrix.py index de840471698b..07d0b23ee9e1 100644 --- a/tests/build-job-matrix.py +++ b/tests/build-job-matrix.py @@ -130,7 +130,7 @@ def update_config(parent: Dict[str, Any], child: Dict[str, Any]) -> Dict[str, An process_count = { "macos": {False: 0, True: 4}.get(conf["parallel"], conf["parallel"]), "ubuntu": {False: 0, True: 6}.get(conf["parallel"], conf["parallel"]), - "windows": {False: 0, True: 6}.get(conf["parallel"], conf["parallel"]), + "windows": {False: 0, True: 4}.get(conf["parallel"], conf["parallel"]), } pytest_parallel_args = {os: f" -n {count}" for os, count in process_count.items()} diff --git a/tests/core/full_node/stores/test_full_node_store.py b/tests/core/full_node/stores/test_full_node_store.py index 65fee63045ae..010e18319ad9 100644 --- a/tests/core/full_node/stores/test_full_node_store.py +++ b/tests/core/full_node/stores/test_full_node_store.py @@ -122,7 +122,9 @@ async def test_basic_store( # Add/get unfinished block for height, unf_block in enumerate(unfinished_blocks): assert store.get_unfinished_block(unf_block.partial_hash) is None - store.add_unfinished_block(uint32(height), unf_block, PreValidationResult(None, uint64(123532), None, False)) + store.add_unfinished_block( + uint32(height), unf_block, PreValidationResult(None, uint64(123532), None, False, uint32(0)) + ) assert store.get_unfinished_block(unf_block.partial_hash) == unf_block store.remove_unfinished_block(unf_block.partial_hash) assert store.get_unfinished_block(unf_block.partial_hash) is None diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index 16ca2be37a1f..1851d73aa6c8 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -15,7 +15,7 @@ from chia.simulator.block_tools import test_constants from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.db_wrapper import DBWrapper2 -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 from tests.util.temp_file import TempFile @@ -74,7 +74,7 @@ async def test_blocks(default_1000_blocks, with_hints: bool): for block in blocks: # await _validate_and_add_block(bc, block) - results = PreValidationResult(None, uint64(1), None, False) + results = PreValidationResult(None, uint64(1), None, False, uint32(0)) result, err, _ = await bc.add_block(block, results) assert err is None diff --git a/tests/core/test_db_validation.py b/tests/core/test_db_validation.py index 131ce91dbc45..0ed607afc0bc 100644 --- a/tests/core/test_db_validation.py +++ b/tests/core/test_db_validation.py @@ -18,7 +18,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock from chia.util.db_wrapper import DBWrapper2 -from chia.util.ints import uint64 +from chia.util.ints import uint32, uint64 from tests.util.temp_file import TempFile @@ -141,7 +141,7 @@ async def make_db(db_file: Path, blocks: List[FullBlock]) -> None: bc = await Blockchain.create(coin_store, block_store, test_constants, Path("."), reserved_cores=0) for block in blocks: - results = PreValidationResult(None, uint64(1), None, False) + results = PreValidationResult(None, uint64(1), None, False, uint32(0)) result, err, _ = await bc.add_block(block, results) assert err is None From 88d9b75a9122666d052f5a3ef0c64736b69f8a74 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jan 2024 14:02:32 -0800 Subject: [PATCH 105/274] Fix signer command tests --- tests/wallet/test_signer_protocol.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index ac326fe094ec..30305b594c09 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -550,6 +550,19 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram assert response.signing_instructions == not_our_utx.signing_instructions +@pytest.mark.parametrize( + "wallet_environments", + [ + { + "num_environments": 1, + "blocks_needed": [1], + "trusted": True, + "reuse_puzhash": True, + } + ], + indirect=True, +) +@pytest.mark.anyio async def test_signer_commands(wallet_environments: WalletTestFramework) -> None: wallet: Wallet = wallet_environments.environments[0].xch_wallet wallet_state_manager: WalletStateManager = wallet_environments.environments[0].wallet_state_manager @@ -865,7 +878,7 @@ def test_qr_code_display(monkeypatch: pytest.MonkeyPatch) -> None: def new_start(self, *args) -> None: # type: ignore[no-untyped-def] old_start(self) - time.sleep(5) + time.sleep(11) monkeypatch.setattr(Thread, "start", new_start) @@ -883,7 +896,7 @@ def run(self) -> None: runner = CliRunner() result = runner.invoke( cmd, - ["temp_cmd", "--qr-density", str(int(len(bytes_to_encode) / 2)), "--rotation-speed", "3"], + ["temp_cmd", "--qr-density", str(int(len(bytes_to_encode) / 2)), "--rotation-speed", "6"], input="\n", catch_exceptions=False, ) From 7add91b03f5f7d3d28157afff03c175f20195229 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 26 Jan 2024 14:21:08 -0800 Subject: [PATCH 106/274] Try a different approach: recursive flattening --- chia/cmds/cmd_classes.py | 170 +++++++++++++++++++-------- chia/cmds/signer.py | 54 ++++++--- tests/cmds/test_cmd_framework.py | 37 ++++-- tests/wallet/test_signer_protocol.py | 94 ++++++++------- 4 files changed, 237 insertions(+), 118 deletions(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index 91751ad12523..4a821a302adb 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -4,8 +4,8 @@ import inspect import sys from contextlib import asynccontextmanager -from dataclasses import MISSING, Field, dataclass, field, fields -from typing import Any, AsyncIterator, Callable, Dict, Iterable, Optional, Protocol, Type, Union +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Protocol, Type, Union, get_type_hints import click from typing_extensions import dataclass_transform @@ -46,17 +46,115 @@ def option(*param_decls: str, **kwargs: Any) -> Any: ) -def _apply_options(cmd: SyncCmd, _fields: Iterable[Field[Any]]) -> SyncCmd: - wrapped_cmd = cmd +@dataclass(frozen=True) +class _CommandParsingStage: + my_dataclass: Type[ChiaCommand] + my_option_decorators: List[Callable[[SyncCmd], SyncCmd]] + my_subclasses: Dict[str, _CommandParsingStage] + my_kwarg_names: List[str] + _needs_context: bool + + def needs_context(self) -> bool: + if self._needs_context: + return True + else: + return any([subclass.needs_context() for subclass in self.my_subclasses.values()]) + + def get_all_option_decorators(self) -> List[Callable[[SyncCmd], SyncCmd]]: + all_option_decorators: List[Callable[[SyncCmd], SyncCmd]] = self.my_option_decorators + for subclass in self.my_subclasses.values(): + all_option_decorators.extend(subclass.get_all_option_decorators()) + return all_option_decorators + + def initialize_instance(self, **kwargs: Any) -> ChiaCommand: + kwargs_to_pass: Dict[str, Any] = {} + for kwarg_name in self.my_kwarg_names: + kwargs_to_pass[kwarg_name] = kwargs[kwarg_name] + + for subclass_arg_name, subclass in self.my_subclasses.items(): + kwargs_to_pass[subclass_arg_name] = subclass.initialize_instance(**kwargs) + + return self.my_dataclass(**kwargs_to_pass) + + def apply_decorators(self, cmd: SyncCmd) -> SyncCmd: + cmd_to_return = cmd + if self.needs_context(): + + def strip_click_context(func: SyncCmd) -> SyncCmd: + def _inner(ctx: click.Context, **kwargs: Any) -> None: + context: Dict[str, Any] = ctx.obj if ctx.obj is not None else {} + func(context=context, **kwargs) + + return _inner + + cmd_to_return = click.pass_context(strip_click_context(cmd_to_return)) + + for decorator in self.get_all_option_decorators(): + cmd_to_return = decorator(cmd_to_return) + + return cmd_to_return + + +def _generate_command_parser(cls: Type[ChiaCommand]) -> _CommandParsingStage: + option_decorators: List[Callable[[SyncCmd], SyncCmd]] = [] + kwarg_names: List[str] = [] + subclasses: Dict[str, _CommandParsingStage] = {} + needs_context: bool = False + + hints = get_type_hints(cls) + _fields = fields(cls) # type: ignore[arg-type] + for _field in _fields: - if "is_command_option" not in _field.metadata or not _field.metadata["is_command_option"]: - continue - wrapped_cmd = click.option( - *_field.metadata["param_decls"], - **{k: v for k, v in _field.metadata.items() if k not in ("param_decls", "is_command_option")}, - )(wrapped_cmd) + field_name = _field.name + if isinstance(hints[field_name], type) and issubclass(hints[field_name], _CommandHelper): + subclasses[field_name] = _generate_command_parser(hints[field_name]) + else: + if field_name == "context": + if hints[field_name] != Context: + raise ValueError("only Context can be the hint for variables named 'context'") + else: + needs_context = True + kwarg_names.append(field_name) + continue + elif "is_command_option" not in _field.metadata or not _field.metadata["is_command_option"]: + continue + + kwarg_names.append(field_name) + option_decorators.append( + click.option( + *_field.metadata["param_decls"], + **{k: v for k, v in _field.metadata.items() if k not in ("param_decls", "is_command_option")}, + ) + ) + + return _CommandParsingStage( + cls, + option_decorators, + subclasses, + kwarg_names, + needs_context, + ) + - return wrapped_cmd +def _convert_class_to_function(cls: Type[ChiaCommand]) -> SyncCmd: + command_parser = _generate_command_parser(cls) + + if inspect.iscoroutinefunction(cls.run): + + async def async_base_cmd(*args: Any, **kwargs: Any) -> None: + await command_parser.initialize_instance(*args, **kwargs).run() # type: ignore[misc] + + def base_cmd(*args: Any, **kwargs: Any) -> None: + coro = async_base_cmd(*args, **kwargs) + assert coro is not None + asyncio.run(coro) + + else: + + def base_cmd(*args: Any, **kwargs: Any) -> None: + command_parser.initialize_instance(*args, **kwargs).run() + + return command_parser.apply_decorators(base_cmd) @dataclass_transform() @@ -74,51 +172,30 @@ def _chia_command(cls: Type[ChiaCommand]) -> Type[ChiaCommand]: frozen=True, kw_only=True, )(cls) - cls_fields = fields(wrapped_cls) # type: ignore[arg-type] - if inspect.iscoroutinefunction(cls.run): - - async def async_base_cmd(*args: Any, **kwargs: Any) -> None: - await wrapped_cls(*args, **kwargs).run() # type: ignore[misc] - - def base_cmd(*args: Any, **kwargs: Any) -> None: - coro = async_base_cmd(*args, **kwargs) - assert coro is not None - asyncio.run(coro) - - else: - - def base_cmd(*args: Any, **kwargs: Any) -> None: - wrapped_cls(**kwargs).run() - marshalled_cmd = base_cmd - if issubclass(wrapped_cls, NeedsContext): - - def strip_click_context(func: SyncCmd) -> SyncCmd: - def _inner(ctx: click.Context, **kwargs: Any) -> None: - context: Dict[str, Any] = ctx.obj if ctx.obj is not None else {} - func(context=context, **kwargs) - - return _inner - - marshalled_cmd = click.pass_context(strip_click_context(marshalled_cmd)) - marshalled_cmd = _apply_options(marshalled_cmd, cls_fields) - cmd.command(name, help=help)(marshalled_cmd) + cmd.command(name, help=help)(_convert_class_to_function(wrapped_cls)) return wrapped_cls return _chia_command +class _CommandHelper: + pass + + @dataclass_transform() def command_helper(cls: Type[Any]) -> Type[Any]: if sys.version_info < (3, 10): # stuff below 3.10 doesn't support kw_only - return dataclass(frozen=True)(cls) # pragma: no cover + return dataclass(frozen=True)( + type(cls.__name__, (dataclass(frozen=True)(cls), _CommandHelper), {}) + ) # pragma: no cover else: - return dataclass(frozen=True, kw_only=True)(cls) + return dataclass(frozen=True, kw_only=True)( + type(cls.__name__, (dataclass(frozen=True, kw_only=True)(cls), _CommandHelper), {}) + ) -@command_helper -class NeedsContext: - context: Dict[str, Any] = field(default_factory=dict) # pylint: disable=invalid-field-call +Context = Dict[str, Any] @dataclass(frozen=True) @@ -129,7 +206,8 @@ class WalletClientInfo: @command_helper -class NeedsWalletRPC(NeedsContext): +class NeedsWalletRPC: + context: Context = field(default_factory=dict) client_info: Optional[WalletClientInfo] = None wallet_rpc_port: Optional[int] = option( "-wp", @@ -155,7 +233,7 @@ async def wallet_rpc(self, **kwargs: Any) -> AsyncIterator[WalletClientInfo]: yield self.client_info else: if "root_path" not in kwargs: - kwargs["root_path"] = self.context["root_path"] # pylint: disable=unsubscriptable-object + kwargs["root_path"] = self.context["root_path"] async with get_wallet_client(self.wallet_rpc_port, self.fingerprint, **kwargs) as ( wallet_client, fp, diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index f1ce1fc160cc..cd363106f1aa 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -194,29 +194,38 @@ def handle_clvm_output(self, outputs: List[ClvmStreamable]) -> None: "gather_signing_info", "Gather the information from a transaction that a signer needs in order to create a signature", ) -class GatherSigningInfoCMD(SPOut, TransactionsIn, NeedsWalletRPC): +class GatherSigningInfoCMD: + sp_out: SPOut + txs_in: TransactionsIn + rpc_info: NeedsWalletRPC + async def run(self) -> None: - async with self.wallet_rpc() as wallet_rpc: + async with self.rpc_info.wallet_rpc() as wallet_rpc: spends: List[Spend] = [ Spend.from_coin_spend(cs) - for tx in self.transaction_bundle.txs + for tx in self.txs_in.transaction_bundle.txs if tx.spend_bundle is not None for cs in tx.spend_bundle.coin_spends ] signing_instructions: SigningInstructions = ( await wallet_rpc.client.gather_signing_info(GatherSigningInfo(spends=spends)) ).signing_instructions - self.handle_clvm_output([signing_instructions]) + self.sp_out.handle_clvm_output([signing_instructions]) @chia_command(signer_cmd, "apply_signatures", "Apply a signer's signatures to a transaction bundle") -class ApplySignaturesCMD(TransactionsOut, SPIn, TransactionsIn, NeedsWalletRPC): +class ApplySignaturesCMD: + txs_out: TransactionsOut + sp_in: SPIn + txs_in: TransactionsIn + rpc_info: NeedsWalletRPC + async def run(self) -> None: - async with self.wallet_rpc() as wallet_rpc: - signing_responses: List[SigningResponse] = self.read_sp_input(SigningResponse) + async with self.rpc_info.wallet_rpc() as wallet_rpc: + signing_responses: List[SigningResponse] = self.sp_in.read_sp_input(SigningResponse) spends: List[Spend] = [ Spend.from_coin_spend(cs) - for tx in self.transaction_bundle.txs + for tx in self.txs_in.transaction_bundle.txs if tx.spend_bundle is not None for cs in tx.spend_bundle.coin_spends ] @@ -236,18 +245,24 @@ async def run(self) -> None: [spend.as_coin_spend() for spend in signed_spends], final_signature ) new_transactions: List[TransactionRecord] = [ - replace(self.transaction_bundle.txs[0], spend_bundle=new_spend_bundle, name=new_spend_bundle.name()), - *(replace(tx, spend_bundle=None) for tx in self.transaction_bundle.txs), + replace( + self.txs_in.transaction_bundle.txs[0], spend_bundle=new_spend_bundle, name=new_spend_bundle.name() + ), + *(replace(tx, spend_bundle=None) for tx in self.txs_in.transaction_bundle.txs), ] - self.handle_transaction_output(new_transactions) + self.txs_out.handle_transaction_output(new_transactions) @chia_command(signer_cmd, "execute_signing_instructions", "Given some signing instructions, return signing responses") -class ExecuteSigningInstructionsCMD(SPOut, SPIn, NeedsWalletRPC): +class ExecuteSigningInstructionsCMD: + sp_out: SPOut + sp_in: SPIn + rpc_info: NeedsWalletRPC + async def run(self) -> None: - async with self.wallet_rpc() as wallet_rpc: - signing_instructions: List[SigningInstructions] = self.read_sp_input(SigningInstructions) - self.handle_clvm_output( + async with self.rpc_info.wallet_rpc() as wallet_rpc: + signing_instructions: List[SigningInstructions] = self.sp_in.read_sp_input(SigningInstructions) + self.sp_out.handle_clvm_output( [ signing_response for instruction_set in signing_instructions @@ -261,7 +276,10 @@ async def run(self) -> None: @chia_command(wallet_cmd, "push_transactions", "Push a transaction bundle to the wallet to send to the network") -class PushTransactionsCMD(TransactionsIn, NeedsWalletRPC): +class PushTransactionsCMD: + txs_in: TransactionsIn + rpc_info: NeedsWalletRPC + async def run(self) -> None: - async with self.wallet_rpc() as wallet_rpc: - await wallet_rpc.client.push_transactions(self.transaction_bundle.txs) + async with self.rpc_info.wallet_rpc() as wallet_rpc: + await wallet_rpc.client.push_transactions(self.txs_in.transaction_bundle.txs) diff --git a/tests/cmds/test_cmd_framework.py b/tests/cmds/test_cmd_framework.py index ed68099494c4..c91524ce72eb 100644 --- a/tests/cmds/test_cmd_framework.py +++ b/tests/cmds/test_cmd_framework.py @@ -1,13 +1,13 @@ from __future__ import annotations from dataclasses import asdict -from typing import Any, TypeVar +from typing import Any, Dict, TypeVar import click import pytest from click.testing import CliRunner -from chia.cmds.cmd_classes import ChiaCommand, NeedsContext, NeedsWalletRPC, chia_command, option +from chia.cmds.cmd_classes import ChiaCommand, Context, NeedsWalletRPC, chia_command, option from tests.conftest import ConsensusMode from tests.environments.wallet import WalletTestFramework from tests.wallet.conftest import * # noqa @@ -22,13 +22,18 @@ def _cmd() -> None: mock_type = type(cmd.__class__.__name__, (cmd.__class__,), {}) - def new_run(self: Any) -> None: - # cmd is appropriately not recognized as a dataclass but I'm not sure how to hint that something is a dataclass - other_dict = asdict(cmd) # type: ignore[call-overload] - for k, v in asdict(self).items(): + def dict_compare_with_ignore_context(one: Dict[str, Any], two: Dict[str, Any]) -> None: + for k, v in one.items(): if k == "context": continue - assert v == other_dict[k] + elif isinstance(v, dict): + dict_compare_with_ignore_context(v, two[k]) + else: + assert v == two[k] + + def new_run(self: Any) -> None: + # cmd is appropriately not recognized as a dataclass but I'm not sure how to hint that something is a dataclass + dict_compare_with_ignore_context(asdict(cmd), asdict(self)) # type: ignore[call-overload] setattr(mock_type, "run", new_run) chia_command(_cmd, "_", "")(mock_type) @@ -132,7 +137,9 @@ def cmd(ctx: click.Context) -> None: ctx.obj = {"foo": "bar"} @chia_command(cmd, "temp_cmd", "blah") - class TempCMD(NeedsContext): + class TempCMD: + context: Context + def run(self) -> None: assert self.context["foo"] == "bar" @@ -170,7 +177,9 @@ def cmd() -> None: pass @chia_command(cmd, "temp_cmd", "blah") - class TempCMD(NeedsWalletRPC): + class TempCMD: + rpc_info: NeedsWalletRPC + def run(self) -> None: pass @@ -198,11 +207,13 @@ def run(self) -> None: assert result.output == "" expected_command = TempCMD( - context={"root_path": wallet_environments.environments[0].node.root_path}, - wallet_rpc_port=port, - fingerprint=fingerprint, + rpc_info=NeedsWalletRPC( + context={"root_path": wallet_environments.environments[0].node.root_path}, + wallet_rpc_port=port, + fingerprint=fingerprint, + ), ) check_click_parsing(expected_command, "-wp", str(port), "-f", str(fingerprint)) - async with expected_command.wallet_rpc(consume_errors=False) as client_info: + async with expected_command.rpc_info.wallet_rpc(consume_errors=False) as client_info: assert await client_info.client.get_logged_in_fingerprint() == fingerprint diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 8c3bb1e81d3f..f2ad22b9aa30 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -12,7 +12,7 @@ from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey from click.testing import CliRunner -from chia.cmds.cmd_classes import WalletClientInfo, chia_command +from chia.cmds.cmd_classes import NeedsWalletRPC, WalletClientInfo, chia_command from chia.cmds.cmds_util import TransactionBundle from chia.cmds.signer import ( ApplySignaturesCMD, @@ -793,32 +793,41 @@ async def test_signer_commands(wallet_environments: WalletTestFramework) -> None file.write(bytes(TransactionBundle([tx]))) await GatherSigningInfoCMD( - client_info=client_info, - transaction_file_in="./temp-tb", - compression="chip-TBD", - output_format="file", - output_file=["./temp-si"], + rpc_info=NeedsWalletRPC(client_info=client_info), + sp_out=SPOut( + compression="chip-TBD", + output_format="file", + output_file=["./temp-si"], + ), + txs_in=TransactionsIn(transaction_file_in="./temp-tb"), ).run() await ExecuteSigningInstructionsCMD( - client_info=client_info, - compression="chip-TBD", - signer_protocol_input=["./temp-si"], - output_format="file", - output_file=["./temp-sr"], + rpc_info=NeedsWalletRPC(client_info=client_info), + sp_in=SPIn( + compression="chip-TBD", + signer_protocol_input=["./temp-si"], + ), + sp_out=SPOut( + compression="chip-TBD", + output_format="file", + output_file=["./temp-sr"], + ), ).run() await ApplySignaturesCMD( - client_info=client_info, - transaction_file_in="./temp-tb", - compression="chip-TBD", - signer_protocol_input=["./temp-sr"], - transaction_file_out="./temp-stb", + rpc_info=NeedsWalletRPC(client_info=client_info), + txs_in=TransactionsIn(transaction_file_in="./temp-tb"), + sp_in=SPIn( + compression="chip-TBD", + signer_protocol_input=["./temp-sr"], + ), + txs_out=TransactionsOut(transaction_file_out="./temp-stb"), ).run() await PushTransactionsCMD( - client_info=client_info, - transaction_file_in="./temp-stb", + rpc_info=NeedsWalletRPC(client_info=client_info), + txs_in=TransactionsIn(transaction_file_in="./temp-stb"), ).run() await wallet_environments.process_pending_states( @@ -849,13 +858,13 @@ async def test_signer_commands(wallet_environments: WalletTestFramework) -> None def test_signer_command_default_parsing() -> None: check_click_parsing( GatherSigningInfoCMD( - client_info=None, - wallet_rpc_port=None, - fingerprint=None, - transaction_file_in="in", - compression="none", - output_format="hex", - output_file=tuple(), + rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), + sp_out=SPOut( + compression="none", + output_format="hex", + output_file=tuple(), + ), + txs_in=TransactionsIn(transaction_file_in="in"), ), "-i", "in", @@ -863,13 +872,16 @@ def test_signer_command_default_parsing() -> None: check_click_parsing( ExecuteSigningInstructionsCMD( - client_info=None, - wallet_rpc_port=None, - fingerprint=None, - compression="none", - signer_protocol_input=("sp-in",), - output_format="hex", - output_file=tuple(), + rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), + sp_in=SPIn( + compression="none", + signer_protocol_input=("sp-in",), + ), + sp_out=SPOut( + compression="none", + output_format="hex", + output_file=tuple(), + ), ), "-p", "sp-in", @@ -877,13 +889,13 @@ def test_signer_command_default_parsing() -> None: check_click_parsing( ApplySignaturesCMD( - client_info=None, - wallet_rpc_port=None, - fingerprint=None, - transaction_file_in="in", - compression="none", - signer_protocol_input=("sp-in",), - transaction_file_out="out", + rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), + txs_in=TransactionsIn(transaction_file_in="in"), + sp_in=SPIn( + compression="none", + signer_protocol_input=("sp-in",), + ), + txs_out=TransactionsOut(transaction_file_out="out"), ), "-i", "in", @@ -895,8 +907,8 @@ def test_signer_command_default_parsing() -> None: check_click_parsing( PushTransactionsCMD( - client_info=None, - transaction_file_in="in", + rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), + txs_in=TransactionsIn(transaction_file_in="in"), ), "-i", "in", From c4ead01a2020181e36570c366b5a96f139b0aab8 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 29 Jan 2024 07:10:33 -0800 Subject: [PATCH 107/274] Bump hsms --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6933195a6af3..a86d649a6d5a 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ "zstd==1.5.5.1", "packaging==23.2", "psutil==5.9.4", - "hsms==0.3.0", + "hsms==0.3.1", "ecdsa==0.18.0", # For SECP ] From 675344688f98a828c8f674adb71f6be7bb731d02 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 29 Jan 2024 07:59:06 -0800 Subject: [PATCH 108/274] Add comments --- chia/wallet/signer_protocol.py | 3 +++ chia/wallet/util/clvm_streamable.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/chia/wallet/signer_protocol.py b/chia/wallet/signer_protocol.py index 59f237b6f1f7..bc559481d21e 100644 --- a/chia/wallet/signer_protocol.py +++ b/chia/wallet/signer_protocol.py @@ -10,6 +10,9 @@ from chia.util.ints import uint64 from chia.wallet.util.clvm_streamable import ClvmStreamable +# This file contains the base types for communication between a wallet and an offline transaction signer. +# These types should be compliant with CHIP-TBD + class Coin(ClvmStreamable): parent_coin_id: bytes32 diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 664a8e291e08..fe692cfe7670 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -15,6 +15,8 @@ from chia.util.streamable import ConversionError, Streamable, streamable +# This class is meant to be a context var shared by multiple calls to the methods on ClvmStreamable objects +# It is ideally thread/coroutine safe meaning when code flow is non-linear, changes in one branch do not affect others @dataclass class ClvmSerializationConfig: use: bool = False @@ -42,6 +44,25 @@ def clvm_serialization_mode(use: bool) -> Iterator[None]: @dataclass_transform() class ClvmStreamableMeta(type): + """ + We use a metaclass to define custom behavior during class initialization. We define logic such that classes that + inherit from ClvmStreamable (which uses this metaclass) behave as if they had been defined like this: + + @streamable + @dataclass(frozen=True) + class ChildClass(Streamable): + # custom streamable functions + hsms clvm_serde as/from_program methods + ... + + To streamline the process above and prevent mistakes/inconsistencies, we use the metaclass. + + TODO: Metaclasses are generally considered bad practice and we should probably pivot from this approach. + What is unclear, however, is how to keep the existing simple ergonomics and still hint that every class that this + logic has been applied to has all of the proper properties. Manadatory inheritance from a class that uses this + metaclass makes this simple because you simply need to check that something is ClvmStreamable to have those + guarantees. Perhaps in the future a decorator can be used to something like the effect of this metaclass. + """ + def __init__(cls: ClvmStreamableMeta, *args: Any) -> None: if cls.__name__ == "ClvmStreamable": return @@ -62,6 +83,12 @@ def __init__(cls: ClvmStreamableMeta, *args: Any) -> None: class ClvmStreamable(Streamable, metaclass=ClvmStreamableMeta): + """ + Classes that inherit from this base class gain access to clvm serialization from hsms clvm_serde library. + Children also gain the ability to serialize differently under the clvm_serialization_mode context manager above. + If not called under the context manager, they will serialize according to the Streamable protocol. + """ + def as_program(self) -> Program: raise NotImplementedError() # pragma: no cover @@ -78,6 +105,8 @@ def stream(self, f: BinaryIO) -> None: @classmethod def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: assert isinstance(f, BytesIO) + # This try/except is to faciliate deserializing blobs that have been serialized according to either the + # clvm_serde or streamable libraries. try: result = cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) f.read() @@ -87,6 +116,7 @@ def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], Dict[str, Any]]) -> Any: if _ClvmSerializationMode.get_config().use: + # If we are using clvm_serde, we stop JSON serialization at this point and instead return the clvm blob return bytes(self).hex() else: new_dict = {} @@ -96,6 +126,11 @@ def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], D @classmethod def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStreamable: + # If we have reached this point, the Streamable library has determined we are a responsible for deserializing + # the value at this position in the dictionary. In order to preserve the ability to parse either streamable + # or clvm_serde objects in any context, we first check whether the value to be deserialized is a string. + # If it is, we know this value was serialized according to clvm_serde and we deserialize it as a clvm blob. + # If it is not, we know it was serialized according to streamable and we deserialize as a normal JSON dict if isinstance(json_dict, str): try: byts = hexstr_to_bytes(json_dict) From afe1c37114fe6afbce95917f494c0105033eb18d Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 30 Jan 2024 17:05:02 +1300 Subject: [PATCH 109/274] create vault spends --- chia/wallet/vault/vault_drivers.py | 8 +- chia/wallet/vault/vault_info.py | 2 + chia/wallet/vault/vault_wallet.py | 206 +++++++++++++++++++++--- tests/wallet/vault/test_vault_clsp.py | 4 +- tests/wallet/vault/test_vault_wallet.py | 95 ++++++++--- 5 files changed, 270 insertions(+), 45 deletions(-) diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 3d414f061371..60a69dd259e2 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -136,8 +136,12 @@ def get_vault_full_solution(lineage_proof: LineageProof, amount: uint64, inner_s # MERKLE -def construct_vault_merkle_tree(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> MerkleTree: - return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]) +def construct_vault_merkle_tree( + secp_puzzle_hash: bytes32, recovery_puzzle_hash: Optional[bytes32] = None +) -> MerkleTree: + if recovery_puzzle_hash: + return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]) + return MerkleTree([secp_puzzle_hash]) def get_vault_proof(merkle_tree: MerkleTree, puzzle_hash: bytes32) -> Program: diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index 428d9313e689..314893679e20 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -8,6 +8,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint64 from chia.util.streamable import Streamable, streamable +from chia.wallet.lineage_proof import LineageProof @streamable @@ -20,6 +21,7 @@ class VaultInfo(Streamable): inner_puzzle_hash: bytes32 is_recoverable: bool launcher_coin_id: bytes32 + lineage_proof: LineageProof @dataclass(frozen=True) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 1aa47e06827c..440992b6ad43 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import json import logging import time @@ -12,16 +11,18 @@ from chia.protocols.wallet_protocol import CoinState 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.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64, uint128 from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import Condition, parse_timelock_info +from chia.wallet.conditions import Condition, CreateCoin, CreatePuzzleAnnouncement, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment +from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions from chia.wallet.signer_protocol import ( PathHint, SignedTransaction, @@ -36,7 +37,9 @@ from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, + construct_secp_message, construct_vault_merkle_tree, + get_p2_singleton_puzzle, get_p2_singleton_puzzle_hash, get_recovery_finish_puzzle, get_recovery_inner_puzzle, @@ -97,6 +100,181 @@ async def generate_signed_transaction( ) -> List[TransactionRecord]: raise NotImplementedError("vault wallet") + async def generate_p2_singleton_spends( + self, + primaries: List[Payment], + tx_config: TXConfig, + fee: uint64 = uint64(0), + coins: Optional[Set[Coin]] = None, + extra_conditions: Tuple[Condition, ...] = tuple(), + ) -> List[CoinSpend]: + total_amount = ( + sum(primary.amount for primary in primaries) + + fee + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + ) + total_balance = await self.get_spendable_balance() + 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 + spend_value = sum([coin.amount for coin in coins]) + change = spend_value - total_amount + assert change >= 0 + if change > 0: + change_puzzle_hash: bytes32 = next(iter(coins)).puzzle_hash + primaries.append(Payment(change_puzzle_hash, uint64(change))) + + spends: List[CoinSpend] = [] + + # 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") + + p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.vault_info.launcher_coin_id) + serialized_puzzle: SerializedProgram = SerializedProgram.from_bytes(bytes(p2_singleton_puzzle)) + + for coin in coins: + p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin.name()]) + spends.append( + CoinSpend(coin, serialized_puzzle, SerializedProgram.from_bytes(bytes(p2_singleton_solution))) + ) + + return spends + + async def generate_unsigned_vault_spend( + self, + primaries: List[Payment], + p2_spends: List[CoinSpend], + memos: Optional[List[bytes]] = None, + fee: uint64 = uint64(0), + extra_conditions: Tuple[Condition, ...] = tuple(), + ) -> Tuple[bytes, Program, Program]: + total_amount = ( + sum(primary.amount for primary in primaries) + + fee + + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + ) + coins = [spend.coin for spend in p2_spends] + spend_value = sum([coin.amount for coin in coins]) + change = spend_value - total_amount + assert change >= 0 + if change > 0: + change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.vault_info.launcher_coin_id) + primaries.append(Payment(change_puzzle_hash, uint64(change))) + # Create the vault spend + vault_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, + ) + + conditions = [primary.as_condition() for primary in primaries] + recreate_vault_condition = CreateCoin( + vault_inner_puzzle.get_tree_hash(), uint64(self.vault_info.coin.amount), memos=[self.vault_info.launcher_id] + ).to_program() + conditions.append(recreate_vault_condition) + announcements = [CreatePuzzleAnnouncement(spend.coin.name()).to_program() for spend in p2_spends] + conditions.extend(announcements) + + delegated_puzzle = puzzle_for_conditions(conditions) + delegated_solution = solution_for_conditions(conditions) + + message_to_sign = construct_secp_message( + delegated_puzzle.get_tree_hash(), + self.vault_info.coin.name(), + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ) + + return message_to_sign, delegated_puzzle, delegated_solution + + async def generate_signed_vault_spend( + self, + signed_message: bytes, + delegated_puzzle: Program, + delegated_solution: Program, + p2_spends: List[CoinSpend], + primaries: List[Payment], + fee: uint64 = uint64(0), + ) -> List[TransactionRecord]: + secp_puzzle = construct_p2_delegated_secp( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ) + vault_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, + ) + secp_solution = Program.to( + [ + delegated_puzzle, + delegated_solution, + signed_message, + self.vault_info.coin.name(), + ] + ) + if self.vault_info.is_recoverable: + recovery_puzzle_hash = get_recovery_puzzle( + secp_puzzle.get_tree_hash(), + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, + ).get_tree_hash() + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) + else: + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash()) + proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) + vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) + + full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, vault_inner_puzzle) + full_solution = get_vault_full_solution( + self.vault_info.lineage_proof, + uint64(self.vault_info.coin.amount), + vault_inner_solution, + ) + + vault_spend = CoinSpend(self.vault_info.coin, full_puzzle, full_solution) + all_spends = [*p2_spends, vault_spend] + spend_bundle = SpendBundle(all_spends, G2Element()) + + amount = uint64(sum([payment.amount for payment in primaries])) + target_puzzle_hash = primaries[0].puzzle_hash + + tx_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=target_puzzle_hash, + amount=amount, + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=spend_bundle.additions(), + removals=spend_bundle.removals(), + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=spend_bundle.name(), + valid_times=parse_timelock_info(tuple()), + ) + await self.wallet_state_manager.add_pending_transactions([tx_record], sign=False) + return [tx_record] + def puzzle_for_pk(self, pubkey: G1Element) -> Program: raise NotImplementedError("vault wallet") @@ -255,9 +433,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, inner_puzzle) assert full_puzzle.get_tree_hash() == vault_coin.puzzle_hash - # TODO: handle lineage proofs - lineage = LineageProof(self.vault_info.coin.parent_coin_info, inner_puzzle.get_tree_hash(), amount) - full_solution = get_vault_full_solution(lineage, amount, inner_solution) + full_solution = get_vault_full_solution(self.vault_info.lineage_proof, amount, inner_solution) recovery_spend = SpendBundle([CoinSpend(vault_coin, full_puzzle, full_solution)], G2Element()) # 2. Generate the Finish Recovery Spend @@ -318,7 +494,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: return [recovery_tx, finish_tx] - async def sync_singleton(self) -> None: + async def sync_vault_launcher(self) -> None: wallet_node: Any = self.wallet_state_manager.wallet_node peer = wallet_node.get_full_node_peer() assert peer is not None @@ -350,6 +526,7 @@ async def sync_singleton(self) -> None: inner_puzzle_hash = get_vault_inner_puzzle_hash( secp_pk, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock ) + lineage_proof = LineageProof(parent_state.coin.parent_coin_info, None, uint64(parent_state.coin.amount)) vault_info = VaultInfo( coin_state.coin, launcher_id, @@ -358,23 +535,8 @@ async def sync_singleton(self) -> None: inner_puzzle_hash, is_recoverable, parent_state.coin.name(), + lineage_proof, ) - - if coin_state.spent_height: - while coin_state.spent_height is not None: - coin_states = await wallet_node.fetch_children(coin_state.coin.name(), peer=peer) - odd_coin = None - for coin in coin_states: - 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.") - parent_state = coin_state - coin_state = odd_coin - - vault_info = dataclasses.replace(vault_info, coin=coin_state.coin) await self.save_info(vault_info) await self.wallet_state_manager.create_more_puzzle_hashes() diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 2d2524d43c09..0005be1f1165 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -1,10 +1,10 @@ from __future__ import annotations from hashlib import sha256 -from typing import Optional, Tuple +from typing import Optional import pytest -from chia_rs import ENABLE_SECP_OPS, G1Element, PrivateKey +from chia_rs import G1Element, PrivateKey from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 2a9fb9f431be..80aec489efcd 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -8,10 +8,11 @@ from ecdsa.util import PRNG from chia.util.ints import uint32, uint64 +from chia.wallet.payment import Payment from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault -from tests.conftest import ConsensusMode +from tests.conftest import SOFTFORK_HEIGHTS, ConsensusMode from tests.environments.wallet import WalletStateTransition, WalletTestFramework @@ -56,20 +57,27 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b ) ] ) - await env.wallet_node.keychain_proxy.add_public_key(launcher_id.hex()) + await env.node.keychain_proxy.add_public_key(launcher_id.hex()) await env.restart(vault_root.get_fingerprint()) - await wallet_environments.full_node.wait_for_wallet_synced(env.wallet_node, 20) + await wallet_environments.full_node.wait_for_wallet_synced(env.node, 20) + + +def sign_message(message: bytes) -> bytes: + seed = b"chia_secp" + SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) + signed_message: bytes = SECP_SK.sign_deterministic(message) + return signed_message @pytest.mark.parametrize( - "wallet_environments", + "wallet_environments,active_softfork_height", [ - { - "num_environments": 2, - "blocks_needed": [1, 1], - } + ( + {"num_environments": 2, "blocks_needed": [1, 1]}, + SOFTFORK_HEIGHTS[2], + ) ], - indirect=True, + indirect=["wallet_environments"], ) @pytest.mark.parametrize("setup_function", [vault_setup]) @pytest.mark.parametrize("with_recovery", [True, False]) @@ -78,6 +86,7 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b async def test_vault_creation( setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], wallet_environments: WalletTestFramework, + active_softfork_height: uint32, with_recovery: bool, ) -> None: await setup_function(wallet_environments, with_recovery) @@ -85,13 +94,12 @@ async def test_vault_creation( assert isinstance(env.xch_wallet, Vault) wallet: Vault = env.xch_wallet - await wallet.sync_singleton() + await wallet.sync_vault_launcher() assert wallet.vault_info # get a p2_singleton p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) - # launcher_id = wallet.vault_info.launcher_id if with_recovery: assert wallet.vault_info.is_recoverable @@ -102,15 +110,17 @@ async def test_vault_creation( else: assert not wallet.vault_info.is_recoverable + coins_to_create = 2 funding_amount = uint64(1000000000) funding_wallet = wallet_environments.environments[1].xch_wallet - funding_tx = await funding_wallet.generate_signed_transaction( - funding_amount, - p2_singleton_puzzle_hash, - DEFAULT_TX_CONFIG, - memos=[wallet.vault_info.pubkey], - ) - await funding_wallet.wallet_state_manager.add_pending_transactions(funding_tx) + for _ in range(coins_to_create): + funding_tx = await funding_wallet.generate_signed_transaction( + funding_amount, + p2_singleton_puzzle_hash, + DEFAULT_TX_CONFIG, + memos=[wallet.vault_info.pubkey], + ) + await funding_wallet.wallet_state_manager.add_pending_transactions(funding_tx) await wallet_environments.process_pending_states( [ @@ -132,4 +142,51 @@ async def test_vault_creation( ) recs = await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG) - assert recs + coin = recs.pop() + assert coin.amount == funding_amount + p2_ph = await funding_wallet.get_new_puzzlehash() + + payments = [ + Payment(p2_ph, uint64(500000000)), + Payment(p2_ph, uint64(510000000)), + ] + + fee = uint64(100) + + p2_spends = await wallet.generate_p2_singleton_spends(payments, DEFAULT_TX_CONFIG, fee) + message, delegated_puz, delegated_sol = await wallet.generate_unsigned_vault_spend(payments, p2_spends) + sig = sign_message(message) + + tx_records = await wallet.generate_signed_vault_spend(sig, delegated_puz, delegated_sol, p2_spends, payments, fee) + assert len(tx_records) == 1 + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + # "confirmed_wallet_balance": 0, + "set_remainder": True, + } + }, + ), + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + # "confirmed_wallet_balance": +funding_amount, + "set_remainder": True, + } + }, + ), + ], + ) From 93e12d2262eeea59ae97ccfa4ec5ee5939f20f18 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jan 2024 12:37:07 -0800 Subject: [PATCH 110/274] Test coverage --- chia/rpc/wallet_request_types.py | 6 +----- chia/rpc/wallet_rpc_client.py | 6 ++++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index b4d0121937ef..4d540db30e2c 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -5,7 +5,6 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle -from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint32 from chia.util.streamable import Streamable, streamable from chia.wallet.signer_protocol import ( @@ -145,10 +144,7 @@ class _OfferEndpointResponse(TransactionEndpointResponse): @classmethod def from_json_dict(cls: Type[_T_OfferEndpointResponse], json_dict: Dict[str, Any]) -> _T_OfferEndpointResponse: tx_endpoint: TransactionEndpointResponse = TransactionEndpointResponse.from_json_dict(json_dict) - try: - offer: Offer = Offer.from_bech32(json_dict["offer"]) - except Exception: - offer = Offer.from_bytes(hexstr_to_bytes(json_dict["offer"])) + offer: Offer = Offer.from_bech32(json_dict["offer"]) return cls( **tx_endpoint.__dict__, diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index e09fe2c36d61..96dfe3b13bd4 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -955,6 +955,7 @@ async def mint_nft( response = await self.fetch("nft_mint_nft", request) return NFTMintNFTResponse.from_json_dict(response) + # TODO: add a test for this async def add_uri_to_nft( self, wallet_id: int, @@ -966,7 +967,7 @@ async def add_uri_to_nft( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> NFTAddURIResponse: + ) -> NFTAddURIResponse: # pragma: no cover request: Dict[str, Any] = { "wallet_id": wallet_id, "nft_coin_id": nft_coin_id, @@ -1036,6 +1037,7 @@ async def list_nfts(self, wallet_id: int, num: int = 50, start_index: int = 0) - response = await self.fetch("nft_get_nfts", request) return response + # TODO: add a test for this async def set_nft_did( self, wallet_id: int, @@ -1046,7 +1048,7 @@ async def set_nft_did( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> NFTSetNFTDIDResponse: + ) -> NFTSetNFTDIDResponse: # pragma: no cover request: Dict[str, Any] = { "wallet_id": wallet_id, "did_id": did_id, From 35ca138074f2ae268a0b9ad7eb67474bd537b5b9 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jan 2024 12:52:02 -0800 Subject: [PATCH 111/274] Coverage ignores --- chia/cmds/wallet_funcs.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index f82e5dcc02d9..21993a9639e7 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -287,19 +287,19 @@ async def send( f"A transaction of amount {amount} and fee {fee} is unusual.\n" f"Pass in --override if you are sure you mean to do this." ) - return [] + return [] # pragma: no cover if amount == 0: print("You can not send an empty transaction") - return [] + return [] # pragma: no cover if clawback_time_lock < 0: print("Clawback time lock seconds cannot be negative.") - return [] + return [] # pragma: no cover try: typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(typ) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return [] + return [] # pragma: no cover final_fee: uint64 = uint64(int(fee * units["chia"])) # fees are always in XCH mojos final_amount: uint64 = uint64(int(amount * mojo_per_unit)) @@ -342,7 +342,7 @@ async def send( ) else: print("Only standard wallet and CAT wallets are supported") - return [] + return [] # pragma: no cover tx_id = res.transaction.name if push: @@ -356,9 +356,10 @@ async def send( return res.transactions print("Transaction not yet submitted to nodes") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + if push: # pragma: no cover + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") - return res.transactions + return res.transactions # pragma: no cover async def get_address(wallet_rpc_port: Optional[int], fp: Optional[int], wallet_id: int, new_address: bool) -> None: From d4d8d057b37860b4491a950e8bcf9a029ad36768 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jan 2024 13:25:48 -0800 Subject: [PATCH 112/274] Remove inadvertent time traveler --- chia/rpc/wallet_request_types.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 4d540db30e2c..8dc0396cca35 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -59,19 +59,6 @@ class SubmitTransactionsResponse(Streamable): mempool_ids: List[bytes32] -@streamable -@dataclass(frozen=True) -class ExecuteSigningInstructions(Streamable): - signing_instructions: SigningInstructions - partial_allowed: bool - - -@streamable -@dataclass(frozen=True) -class ExecuteSigningInstructionsResponse(Streamable): - signing_responses: List[SigningResponse] - - @streamable @dataclass(frozen=True) class TransactionEndpointResponse(Streamable): From c4bb86c8a54c4c2777b641aae83a30bbd3d1c9a0 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jan 2024 14:06:29 -0800 Subject: [PATCH 113/274] Bring in lost time traveler --- chia/rpc/wallet_request_types.py | 2 +- tests/wallet/test_signer_protocol.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index b4d0121937ef..7328c065c973 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -64,7 +64,7 @@ class SubmitTransactionsResponse(Streamable): @dataclass(frozen=True) class ExecuteSigningInstructions(Streamable): signing_instructions: SigningInstructions - partial_allowed: bool + partial_allowed: bool = False @streamable diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index f915210c0ab7..e9380c972659 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -9,7 +9,12 @@ import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey -from chia.rpc.wallet_request_types import ApplySignatures, GatherSigningInfo, SubmitTransactions +from chia.rpc.wallet_request_types import ( + ApplySignatures, + ExecuteSigningInstructions, + GatherSigningInfo, + SubmitTransactions, +) from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.types.blockchain_format.coin import Coin as ConsensusCoin from chia.types.blockchain_format.program import Program @@ -248,9 +253,9 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram assert utx.signing_instructions.targets[0].fingerprint == synthetic_pubkey.get_fingerprint().to_bytes(4, "big") assert utx.signing_instructions.targets[0].message == message - signing_responses: List[SigningResponse] = await wallet_state_manager.execute_signing_instructions( - utx.signing_instructions - ) + signing_responses: List[SigningResponse] = ( + await wallet_rpc.execute_signing_instructions(ExecuteSigningInstructions(utx.signing_instructions)) + ).signing_responses assert len(signing_responses) == 1 assert signing_responses[0].hook == utx.signing_instructions.targets[0].hook assert AugSchemeMPL.verify(synthetic_pubkey, message, G2Element.from_bytes(signing_responses[0].signature)) From 81d48a155de9133a78d9fa753e5b085a78a6beb2 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jan 2024 14:16:10 -0800 Subject: [PATCH 114/274] pylint --- chia/cmds/cmd_classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index 4a821a302adb..089f2501fa22 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -207,7 +207,7 @@ class WalletClientInfo: @command_helper class NeedsWalletRPC: - context: Context = field(default_factory=dict) + context: Context = field(default_factory=dict) # pylint: disable=invalid-field-call client_info: Optional[WalletClientInfo] = None wallet_rpc_port: Optional[int] = option( "-wp", @@ -233,7 +233,7 @@ async def wallet_rpc(self, **kwargs: Any) -> AsyncIterator[WalletClientInfo]: yield self.client_info else: if "root_path" not in kwargs: - kwargs["root_path"] = self.context["root_path"] + kwargs["root_path"] = self.context["root_path"] # pylint: disable=unsubscriptable-object async with get_wallet_client(self.wallet_rpc_port, self.fingerprint, **kwargs) as ( wallet_client, fp, From 6c17e902acf1ba8eee0644028f5dac71134b37f2 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 07:33:28 -0800 Subject: [PATCH 115/274] Attempt to make qr test less flaky --- chia/cmds/signer.py | 62 +++++++++++++++++----------- tests/wallet/test_signer_protocol.py | 6 +-- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index cd363106f1aa..381f4fbf912a 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -6,8 +6,8 @@ from dataclasses import replace from functools import cached_property from pathlib import Path -from threading import Thread -from typing import List, Optional, Sequence, Type, TypeVar +from threading import Event, Thread +from typing import List, Sequence, Type, TypeVar import click from chia_rs import AugSchemeMPL, G2Element @@ -25,6 +25,11 @@ from chia.wallet.util.clvm_streamable import ClvmStreamable, clvm_serialization_mode +def _clear_screen() -> None: + # Cross-platform screen clear + os.system("cls" if os.name == "nt" else "clear") + + @wallet_cmd.group("signer", help="Get information for an external signer") def signer_cmd() -> None: pass @@ -48,35 +53,37 @@ class QrCodeDisplay: default=2, show_default=True, ) + stop_event: Event = Event() + + def _display_qr(self, index: int, max_index: int, code_list: List[QRCode]) -> None: + while not self.stop_event.is_set(): + for qr_code in itertools.cycle(code_list): + _clear_screen() + qr_code.terminal(compact=True) + print(f"Displaying QR Codes ({index + 1}/{max_index})") + print("") + + for _ in range(self.rotation_speed * 100): + time.sleep(0.01) + if self.stop_event.is_set(): + return def display_qr_codes(self, blobs: List[bytes]) -> None: - chunk_sizes: List[int] = [optimal_chunk_size_for_max_chunk_size(len(blob), self.qr_density) for blob in blobs] - chunks: List[List[bytes]] = [ - create_chunks_for_blob(blob, chunk_size) for blob, chunk_size in zip(blobs, chunk_sizes) - ] - qr_codes: List[List[QRCode]] = [[make_qr(chunk) for chunk in chks] for chks in chunks] + chunk_sizes = [optimal_chunk_size_for_max_chunk_size(len(blob), self.qr_density) for blob in blobs] + chunks = [create_chunks_for_blob(blob, chunk_size) for blob, chunk_size in zip(blobs, chunk_sizes)] + qr_codes = [[make_qr(chunk) for chunk in chks] for chks in chunks] for i, qr_code_list in enumerate(qr_codes): - confirmation: Optional[str] = None - - def _display_qr(index: int, code_list: List[QRCode]) -> None: - for qr_code in itertools.cycle(code_list): - os.system("clear") - qr_code.terminal(compact=True) - print(f"Displaying QR Codes ({index+1}/{len(qr_codes)})") - print("") - for _ in range(0, self.rotation_speed * 100): - time.sleep(0.01) - if confirmation is not None: - return - - t = Thread(target=_display_qr, args=(i, qr_code_list)) + self.stop_event.clear() + t = Thread(target=self._display_qr, args=(i, len(qr_codes), qr_code_list)) t.start() + try: - confirmation = input("") + input("") finally: - confirmation = "" + self.stop_event.set() t.join() + self.stop_event.clear() @command_helper @@ -283,3 +290,12 @@ class PushTransactionsCMD: async def run(self) -> None: async with self.rpc_info.wallet_rpc() as wallet_rpc: await wallet_rpc.client.push_transactions(self.txs_in.transaction_bundle.txs) + + +# Uncomment this for testing of qr code display +# @chia_command(signer_cmd, "temp", "") +# class Temp: +# qr: QrCodeDisplay +# +# def run(self) -> None: +# self.qr.display_qr_codes([bytes([1] * 200), bytes([2] * 200)]) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index f2ad22b9aa30..590ac3e05002 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -1100,8 +1100,8 @@ def test_qr_code_display(monkeypatch: pytest.MonkeyPatch) -> None: old_start = Thread.start def new_start(self, *args) -> None: # type: ignore[no-untyped-def] - old_start(self) - time.sleep(11) + old_start(self, *args) + time.sleep(5) monkeypatch.setattr(Thread, "start", new_start) @@ -1119,7 +1119,7 @@ def run(self) -> None: runner = CliRunner() result = runner.invoke( cmd, - ["temp_cmd", "--qr-density", str(int(len(bytes_to_encode) / 2)), "--rotation-speed", "6"], + ["temp_cmd", "--qr-density", str(int(len(bytes_to_encode) / 2)), "--rotation-speed", "4"], input="\n", catch_exceptions=False, ) From 14eb69a2913974772b584d5a56a3fb4332cecb60 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 07:40:42 -0800 Subject: [PATCH 116/274] Actually test coverage ignore --- chia/rpc/wallet_rpc_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index a432b036bff1..914787ba3955 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -968,7 +968,7 @@ async def add_uri_to_nft( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> NFTAddURIResponse: + ) -> NFTAddURIResponse: # pragma: no cover request = { "wallet_id": wallet_id, "nft_coin_id": nft_coin_id, @@ -1049,7 +1049,7 @@ async def set_nft_did( extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), push: bool = True, - ) -> NFTSetNFTDIDResponse: + ) -> NFTSetNFTDIDResponse: # pragma: no cover request = { "wallet_id": wallet_id, "did_id": did_id, From 47aca8c62e9acde432b80894b0dd9454325b09a2 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 09:38:09 -0800 Subject: [PATCH 117/274] Small refactor for test coverage --- chia/wallet/util/blind_signer_tl.py | 27 ++++++++++--------- chia/wallet/util/clvm_streamable.py | 39 +++++++++++++--------------- tests/wallet/test_signer_protocol.py | 12 ++++++++- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/chia/wallet/util/blind_signer_tl.py b/chia/wallet/util/blind_signer_tl.py index 2cb9d6e21794..336e87d3dc13 100644 --- a/chia/wallet/util/blind_signer_tl.py +++ b/chia/wallet/util/blind_signer_tl.py @@ -70,19 +70,19 @@ class BSTLSigningInstructions(ClvmStreamable): @staticmethod def from_wallet_api(_from: SigningInstructions) -> BSTLSigningInstructions: return BSTLSigningInstructions( - [BSTLSumHint(**sum_hint.__dict__) for sum_hint in _from.key_hints.sum_hints], - [BSTLPathHint(**path_hint.__dict__) for path_hint in _from.key_hints.path_hints], - [BSTLSigningTarget(**signing_target.__dict__) for signing_target in _from.targets], + [BSTLSumHint.from_wallet_api(sum_hint) for sum_hint in _from.key_hints.sum_hints], + [BSTLPathHint.from_wallet_api(path_hint) for path_hint in _from.key_hints.path_hints], + [BSTLSigningTarget.from_wallet_api(signing_target) for signing_target in _from.targets], ) @staticmethod def to_wallet_api(_from: BSTLSigningInstructions) -> SigningInstructions: return SigningInstructions( KeyHints( - [SumHint(**sum_hint.__dict__) for sum_hint in _from.sum_hints], - [PathHint(**path_hint.__dict__) for path_hint in _from.path_hints], + [BSTLSumHint.to_wallet_api(sum_hint) for sum_hint in _from.sum_hints], + [BSTLPathHint.to_wallet_api(path_hint) for path_hint in _from.path_hints], ), - [SigningTarget(**signing_target.__dict__) for signing_target in _from.targets], + [BSTLSigningTarget.to_wallet_api(signing_target) for signing_target in _from.targets], ) @@ -94,9 +94,12 @@ class BSTLUnsignedTransaction(ClvmStreamable): @staticmethod def from_wallet_api(_from: UnsignedTransaction) -> BSTLUnsignedTransaction: return BSTLUnsignedTransaction( - [BSTLSumHint(**sum_hint.__dict__) for sum_hint in _from.signing_instructions.key_hints.sum_hints], - [BSTLPathHint(**path_hint.__dict__) for path_hint in _from.signing_instructions.key_hints.path_hints], - [BSTLSigningTarget(**signing_target.__dict__) for signing_target in _from.signing_instructions.targets], + [BSTLSumHint.from_wallet_api(sum_hint) for sum_hint in _from.signing_instructions.key_hints.sum_hints], + [BSTLPathHint.from_wallet_api(path_hint) for path_hint in _from.signing_instructions.key_hints.path_hints], + [ + BSTLSigningTarget.from_wallet_api(signing_target) + for signing_target in _from.signing_instructions.targets + ], ) @staticmethod @@ -105,10 +108,10 @@ def to_wallet_api(_from: BSTLUnsignedTransaction) -> UnsignedTransaction: TransactionInfo([]), SigningInstructions( KeyHints( - [SumHint(**sum_hint.__dict__) for sum_hint in _from.sum_hints], - [PathHint(**path_hint.__dict__) for path_hint in _from.path_hints], + [BSTLSumHint.to_wallet_api(sum_hint) for sum_hint in _from.sum_hints], + [BSTLPathHint.to_wallet_api(path_hint) for path_hint in _from.path_hints], ), - [SigningTarget(**signing_target.__dict__) for signing_target in _from.targets], + [BSTLSigningTarget.to_wallet_api(signing_target) for signing_target in _from.targets], ), ) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index a44051d3ea50..f3a486551aa2 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -102,7 +102,8 @@ def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: result = new_cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) f.read() if transport_layer is not None and cls_mapping is not None: - deserialized_result: _T_ClvmStreamable = cls_mapping.deserialize_function(result) + deserialized_result = transport_layer.deserialize_from_transport(result) + assert isinstance(deserialized_result, cls) return deserialized_result else: assert isinstance(result, cls) @@ -148,7 +149,8 @@ def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStrea try: result = new_cls.from_program(Program.from_bytes(byts)) if transport_layer is not None and cls_mapping is not None: - deserialized_result: _T_ClvmStreamable = cls_mapping.deserialize_function(result) + deserialized_result = transport_layer.deserialize_from_transport(result) + assert isinstance(deserialized_result, cls) return deserialized_result else: assert isinstance(result, cls) @@ -172,36 +174,31 @@ class TransportLayer: type_mappings: List[TransportLayerMapping[Any, Any]] def get_mapping( - self, _type: Type[_T_ClvmStreamable] + self, _type: Type[_T_ClvmStreamable], for_serialized_type: bool = False ) -> Optional[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]]: - mappings: List[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ - m for m in self.type_mappings if m.from_type == _type - ] + if for_serialized_type: + mappings: List[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ + m for m in self.type_mappings if m.to_type == _type + ] + else: + mappings = [m for m in self.type_mappings if m.from_type == _type] if len(mappings) == 1: return mappings[0] elif len(mappings) == 0: return None - else: + else: # pragma: no cover raise RuntimeError("Malformed TransportLayer") def serialize_for_transport(self, instance: _T_ClvmStreamable) -> ClvmStreamable: - mappings: List[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ - m for m in self.type_mappings if m.from_type == instance.__class__ - ] - if len(mappings) == 1: - return mappings[0].serialize_function(instance) - elif len(mappings) == 0: + mapping = self.get_mapping(instance.__class__) + if mapping is None: return instance else: - raise RuntimeError("Malformed TransportLayer") + return mapping.serialize_function(instance) def deserialize_from_transport(self, instance: _T_ClvmStreamable) -> ClvmStreamable: - mappings: List[TransportLayerMapping[ClvmStreamable, _T_ClvmStreamable]] = [ - m for m in self.type_mappings if m.to_type == instance.__class__ - ] - if len(mappings) == 1: - return mappings[0].deserialize_function(instance) - elif len(mappings) == 0: + mapping = self.get_mapping(instance.__class__, for_serialized_type=True) + if mapping is None: return instance else: - raise RuntimeError("Malformed TransportLayer") + return mapping.deserialize_function(instance) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 6fa6688582c5..7c14cb157a11 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -242,8 +242,9 @@ def test_transport_layer() -> None: ] ) + coin = Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)) spend = Spend( - Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)), + coin, Program.to(1), Program.to([]), ) @@ -274,6 +275,15 @@ def test_transport_layer() -> None: assert foo_spend_program.at("rff") == Program.to("blah") assert foo_spend_program.at("rrff") == Program.to("solution") + # Test that types not registered with transport layer are serialized properly + with clvm_serialization_mode(True): + coin_bytes = bytes(coin) + coin_json = coin.to_json_dict() + with clvm_serialization_mode(True, FOO_TRANSPORT): + assert coin_bytes == bytes(coin) + assert Coin.from_bytes(coin_bytes) == coin + assert Coin.from_json_dict(coin_json) == coin + def test_blind_signer_transport_layer() -> None: sum_hints: List[SumHint] = [ From edca84494211ddb74dfb4abcf28116fa292f725e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 10:08:02 -0800 Subject: [PATCH 118/274] transport -> translation --- chia/rpc/util.py | 16 ++++---- chia/wallet/util/blind_signer_tl.py | 16 ++++---- chia/wallet/util/clvm_streamable.py | 58 ++++++++++++++-------------- tests/wallet/test_signer_protocol.py | 30 +++++++------- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 2934280d3fac..f4a3abee9e48 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -17,8 +17,8 @@ from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.blind_signer_tl import BLIND_SIGNER_TRANSPORT -from chia.wallet.util.clvm_streamable import TransportLayer, clvm_serialization_mode +from chia.wallet.util.blind_signer_tl import BLIND_SIGNER_TRANSLATION +from chia.wallet.util.clvm_streamable import TranslationLayer, clvm_serialization_mode from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import TXConfig, TXConfigLoader @@ -31,7 +31,7 @@ MarshallableRpcEndpoint = Callable[..., Awaitable[Streamable]] -ALL_TRANSPORT_LAYERS: Dict[str, TransportLayer] = {"chip-TBD": BLIND_SIGNER_TRANSPORT} +ALL_TRANSLATION_LAYERS: Dict[str, TranslationLayer] = {"chip-TBD": BLIND_SIGNER_TRANSLATION} def marshal(func: MarshallableRpcEndpoint) -> RpcEndpoint: @@ -47,10 +47,10 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: o *args, **kwargs, ) - compression: Optional[TransportLayer] = ( + compression: Optional[TranslationLayer] = ( None - if "compression" not in request or request["compression"] is None - else ALL_TRANSPORT_LAYERS[request["compression"]] + if "translation" not in request or request["translation"] is None + else ALL_TRANSLATION_LAYERS[request["translation"]] ) with clvm_serialization_mode(not request.get("full_jsonify", False), compression): return response_obj.to_json_dict() @@ -145,10 +145,10 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s if request.get("full_jsonify", False): response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] else: - compression: Optional[TransportLayer] = ( + compression: Optional[TranslationLayer] = ( None if "compression" not in request or request["compression"] is None - else ALL_TRANSPORT_LAYERS[request["compression"]] + else ALL_TRANSLATION_LAYERS[request["compression"]] ) with clvm_serialization_mode(True, compression): response["unsigned_transactions"] = [bytes(tx.as_program()).hex() for tx in unsigned_txs] diff --git a/chia/wallet/util/blind_signer_tl.py b/chia/wallet/util/blind_signer_tl.py index 336e87d3dc13..d23a0997bf2b 100644 --- a/chia/wallet/util/blind_signer_tl.py +++ b/chia/wallet/util/blind_signer_tl.py @@ -15,7 +15,7 @@ TransactionInfo, UnsignedTransaction, ) -from chia.wallet.util.clvm_streamable import ClvmStreamable, TransportLayer, TransportLayerMapping +from chia.wallet.util.clvm_streamable import ClvmStreamable, TranslationLayer, TranslationLayerMapping # Pylint doesn't understand that these classes are in fact dataclasses # pylint: disable=invalid-field-call @@ -129,23 +129,23 @@ def to_wallet_api(_from: BSTLSigningResponse) -> SigningResponse: return SigningResponse(**_from.__dict__) -BLIND_SIGNER_TRANSPORT = TransportLayer( +BLIND_SIGNER_TRANSLATION = TranslationLayer( [ - TransportLayerMapping( + TranslationLayerMapping( SigningTarget, BSTLSigningTarget, BSTLSigningTarget.from_wallet_api, BSTLSigningTarget.to_wallet_api ), - TransportLayerMapping(SumHint, BSTLSumHint, BSTLSumHint.from_wallet_api, BSTLSumHint.to_wallet_api), - TransportLayerMapping(PathHint, BSTLPathHint, BSTLPathHint.from_wallet_api, BSTLPathHint.to_wallet_api), - TransportLayerMapping( + TranslationLayerMapping(SumHint, BSTLSumHint, BSTLSumHint.from_wallet_api, BSTLSumHint.to_wallet_api), + TranslationLayerMapping(PathHint, BSTLPathHint, BSTLPathHint.from_wallet_api, BSTLPathHint.to_wallet_api), + TranslationLayerMapping( SigningInstructions, BSTLSigningInstructions, BSTLSigningInstructions.from_wallet_api, BSTLSigningInstructions.to_wallet_api, ), - TransportLayerMapping( + TranslationLayerMapping( SigningResponse, BSTLSigningResponse, BSTLSigningResponse.from_wallet_api, BSTLSigningResponse.to_wallet_api ), - TransportLayerMapping( + TranslationLayerMapping( UnsignedTransaction, BSTLUnsignedTransaction, BSTLUnsignedTransaction.from_wallet_api, diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 5d5cf6781690..723e30eb39cc 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -20,7 +20,7 @@ @dataclass class ClvmSerializationConfig: use: bool = False - transport_layer: Optional[TransportLayer] = None + translation_layer: Optional[TranslationLayer] = None class _ClvmSerializationMode: @@ -36,9 +36,9 @@ def set_config(cls, config: ClvmSerializationConfig) -> None: @contextmanager -def clvm_serialization_mode(use: bool, transport_layer: Optional[TransportLayer] = None) -> Iterator[None]: +def clvm_serialization_mode(use: bool, translation_layer: Optional[TranslationLayer] = None) -> Iterator[None]: old_config = _ClvmSerializationMode.get_config() - _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use, transport_layer=transport_layer)) + _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use, translation_layer=translation_layer)) yield _ClvmSerializationMode.set_config(old_config) @@ -99,9 +99,9 @@ def from_program(cls: Type[_T_ClvmStreamable], prog: Program) -> _T_ClvmStreamab raise NotImplementedError() # pragma: no cover def stream(self, f: BinaryIO) -> None: - transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer - if transport_layer is not None: - new_self = transport_layer.serialize_for_transport(self) + translation_layer: Optional[TranslationLayer] = _ClvmSerializationMode.get_config().translation_layer + if translation_layer is not None: + new_self = translation_layer.serialize_for_translation(self) else: new_self = self @@ -113,11 +113,11 @@ def stream(self, f: BinaryIO) -> None: @classmethod def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: assert isinstance(f, BytesIO) - transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer - if transport_layer is not None: + translation_layer: Optional[TranslationLayer] = _ClvmSerializationMode.get_config().translation_layer + if translation_layer is not None: cls_mapping: Optional[ - TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable] - ] = transport_layer.get_mapping(cls) + TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable] + ] = translation_layer.get_mapping(cls) if cls_mapping is not None: new_cls: Type[Union[_T_ClvmStreamable, ClvmStreamable]] = cls_mapping.to_type else: @@ -130,8 +130,8 @@ def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: try: result = new_cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) f.read() - if transport_layer is not None and cls_mapping is not None: - deserialized_result = transport_layer.deserialize_from_transport(result) + if translation_layer is not None and cls_mapping is not None: + deserialized_result = translation_layer.deserialize_from_translation(result) assert isinstance(deserialized_result, cls) return deserialized_result else: @@ -141,9 +141,9 @@ def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: return super().parse(f) def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], Dict[str, Any]]) -> Any: - transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer - if transport_layer is not None: - new_self = transport_layer.serialize_for_transport(self) + translation_layer: Optional[TranslationLayer] = _ClvmSerializationMode.get_config().translation_layer + if translation_layer is not None: + new_self = translation_layer.serialize_for_translation(self) else: new_self = self @@ -158,11 +158,11 @@ def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], D @classmethod def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStreamable: - transport_layer: Optional[TransportLayer] = _ClvmSerializationMode.get_config().transport_layer - if transport_layer is not None: + translation_layer: Optional[TranslationLayer] = _ClvmSerializationMode.get_config().translation_layer + if translation_layer is not None: cls_mapping: Optional[ - TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable] - ] = transport_layer.get_mapping(cls) + TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable] + ] = translation_layer.get_mapping(cls) if cls_mapping is not None: new_cls: Type[Union[_T_ClvmStreamable, ClvmStreamable]] = cls_mapping.to_type else: @@ -183,8 +183,8 @@ def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStrea try: result = new_cls.from_program(Program.from_bytes(byts)) - if transport_layer is not None and cls_mapping is not None: - deserialized_result = transport_layer.deserialize_from_transport(result) + if translation_layer is not None and cls_mapping is not None: + deserialized_result = translation_layer.deserialize_from_translation(result) assert isinstance(deserialized_result, cls) return deserialized_result else: @@ -197,7 +197,7 @@ def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStrea @dataclass(frozen=True) -class TransportLayerMapping(Generic[_T_ClvmStreamable, _T_TLClvmStreamable]): +class TranslationLayerMapping(Generic[_T_ClvmStreamable, _T_TLClvmStreamable]): from_type: Type[_T_ClvmStreamable] to_type: Type[_T_TLClvmStreamable] serialize_function: Callable[[_T_ClvmStreamable], _T_TLClvmStreamable] @@ -205,14 +205,14 @@ class TransportLayerMapping(Generic[_T_ClvmStreamable, _T_TLClvmStreamable]): @dataclass(frozen=True) -class TransportLayer: - type_mappings: List[TransportLayerMapping[Any, Any]] +class TranslationLayer: + type_mappings: List[TranslationLayerMapping[Any, Any]] def get_mapping( self, _type: Type[_T_ClvmStreamable], for_serialized_type: bool = False - ) -> Optional[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]]: + ) -> Optional[TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable]]: if for_serialized_type: - mappings: List[TransportLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ + mappings: List[TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = [ m for m in self.type_mappings if m.to_type == _type ] else: @@ -222,16 +222,16 @@ def get_mapping( elif len(mappings) == 0: return None else: # pragma: no cover - raise RuntimeError("Malformed TransportLayer") + raise RuntimeError("Malformed TranslationLayer") - def serialize_for_transport(self, instance: _T_ClvmStreamable) -> ClvmStreamable: + def serialize_for_translation(self, instance: _T_ClvmStreamable) -> ClvmStreamable: mapping = self.get_mapping(instance.__class__) if mapping is None: return instance else: return mapping.serialize_function(instance) - def deserialize_from_transport(self, instance: _T_ClvmStreamable) -> ClvmStreamable: + def deserialize_from_translation(self, instance: _T_ClvmStreamable) -> ClvmStreamable: mapping = self.get_mapping(instance.__class__, for_serialized_type=True) if mapping is None: return instance diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index c7a95e1961d6..d80df662db24 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -46,7 +46,7 @@ UnsignedTransaction, ) from chia.wallet.util.blind_signer_tl import ( - BLIND_SIGNER_TRANSPORT, + BLIND_SIGNER_TRANSLATION, BSTLPathHint, BSTLSigningInstructions, BSTLSigningResponse, @@ -57,8 +57,8 @@ from chia.wallet.util.clvm_streamable import ( ClvmSerializationConfig, ClvmStreamable, - TransportLayer, - TransportLayerMapping, + TranslationLayer, + TranslationLayerMapping, _ClvmSerializationMode, clvm_serialization_mode, ) @@ -231,10 +231,10 @@ def to_wallet_api(_from: FooSpend) -> Spend: ) -def test_transport_layer() -> None: - FOO_TRANSPORT = TransportLayer( +def test_translation_layer() -> None: + FOO_TRANSLATION = TranslationLayer( [ - TransportLayerMapping( + TranslationLayerMapping( Spend, FooSpend, FooSpend.from_wallet_api, @@ -258,13 +258,13 @@ def test_transport_layer() -> None: assert spend_program.at("rff") == Program.to("puzzle") assert spend_program.at("rrff") == Program.to("solution") - with clvm_serialization_mode(True, FOO_TRANSPORT): + with clvm_serialization_mode(True, FOO_TRANSLATION): foo_spend_bytes = bytes(spend) assert foo_spend_bytes.hex() == spend.to_json_dict() # type: ignore[comparison-overlap] assert spend == Spend.from_bytes(foo_spend_bytes) assert spend == Spend.from_json_dict(foo_spend_bytes.hex()) - # Deserialization should only work now if using the transport layer + # Deserialization should only work now if using the translation layer with pytest.raises(Exception): Spend.from_bytes(foo_spend_bytes) with pytest.raises(Exception): @@ -276,17 +276,17 @@ def test_transport_layer() -> None: assert foo_spend_program.at("rff") == Program.to("blah") assert foo_spend_program.at("rrff") == Program.to("solution") - # Test that types not registered with transport layer are serialized properly + # Test that types not registered with translation layer are serialized properly with clvm_serialization_mode(True): coin_bytes = bytes(coin) coin_json = coin.to_json_dict() - with clvm_serialization_mode(True, FOO_TRANSPORT): + with clvm_serialization_mode(True, FOO_TRANSLATION): assert coin_bytes == bytes(coin) assert Coin.from_bytes(coin_bytes) == coin assert Coin.from_json_dict(coin_json) == coin -def test_blind_signer_transport_layer() -> None: +def test_blind_signer_translation_layer() -> None: sum_hints: List[SumHint] = [ SumHint([b"a", b"b", b"c"], b"offset", b"final"), SumHint([b"c", b"b", b"a"], b"offset2", b"final"), @@ -344,13 +344,13 @@ def test_blind_signer_transport_layer() -> None: bstl_transaction_bytes = bytes(bstl_transaction) bstl_signing_response_bytes = bytes(bstl_signing_response) - with clvm_serialization_mode(True, BLIND_SIGNER_TRANSPORT): + with clvm_serialization_mode(True, BLIND_SIGNER_TRANSLATION): transaction_bytes = bytes(transaction) signing_response_bytes = bytes(signing_response) assert transaction_bytes == bstl_transaction_bytes == bytes(bstl_transaction) assert signing_response_bytes == bstl_signing_response_bytes == bytes(bstl_signing_response) - # Deserialization should only work now if using the transport layer + # Deserialization should only work now if using the translation layer with pytest.raises(Exception): UnsignedTransaction.from_bytes(transaction_bytes) with pytest.raises(Exception): @@ -358,7 +358,7 @@ def test_blind_signer_transport_layer() -> None: assert BSTLUnsignedTransaction.from_bytes(transaction_bytes) == bstl_transaction assert BSTLSigningResponse.from_bytes(signing_response_bytes) == bstl_signing_response - with clvm_serialization_mode(True, BLIND_SIGNER_TRANSPORT): + with clvm_serialization_mode(True, BLIND_SIGNER_TRANSLATION): assert UnsignedTransaction.from_bytes(transaction_bytes) == transaction assert SigningResponse.from_bytes(signing_response_bytes) == signing_response @@ -544,7 +544,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram response_dict = await wallet_rpc.fetch("gather_signing_info", {"compression": "chip-TBD", **request}) with pytest.raises(Exception): GatherSigningInfoResponse.from_json_dict(response_dict) - with clvm_serialization_mode(True, transport_layer=BLIND_SIGNER_TRANSPORT): + with clvm_serialization_mode(True, translation_layer=BLIND_SIGNER_TRANSLATION): response: GatherSigningInfoResponse = GatherSigningInfoResponse.from_json_dict(response_dict) assert response.signing_instructions == not_our_utx.signing_instructions From b5eabe28c81a9f2ffd41e948c51b35b3fb294c4e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 10:33:56 -0800 Subject: [PATCH 119/274] missed one --- tests/wallet/test_signer_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index d80df662db24..dcb0ee295901 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -541,7 +541,7 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram request = GatherSigningInfo( [Spend.from_coin_spend(coin_spend), Spend.from_coin_spend(not_our_coin_spend)] ).to_json_dict() - response_dict = await wallet_rpc.fetch("gather_signing_info", {"compression": "chip-TBD", **request}) + response_dict = await wallet_rpc.fetch("gather_signing_info", {"translation": "chip-TBD", **request}) with pytest.raises(Exception): GatherSigningInfoResponse.from_json_dict(response_dict) with clvm_serialization_mode(True, translation_layer=BLIND_SIGNER_TRANSLATION): From b00e793cad306ee4c568b6278be80f580be81a9f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 10:26:40 -0800 Subject: [PATCH 120/274] transport -> translation --- chia/cmds/signer.py | 18 +++++++------- tests/wallet/test_signer_protocol.py | 36 ++++++++++++++-------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index 381f4fbf912a..0b518e5f8d8f 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -17,7 +17,7 @@ from chia.cmds.cmd_classes import NeedsWalletRPC, chia_command, command_helper, option from chia.cmds.cmds_util import TransactionBundle from chia.cmds.wallet import wallet_cmd -from chia.rpc.util import ALL_TRANSPORT_LAYERS +from chia.rpc.util import ALL_TRANSLATION_LAYERS from chia.rpc.wallet_request_types import ApplySignatures, ExecuteSigningInstructions, GatherSigningInfo from chia.types.spend_bundle import SpendBundle from chia.wallet.signer_protocol import SignedTransaction, SigningInstructions, SigningResponse, Spend @@ -118,13 +118,13 @@ def handle_transaction_output(self, output: List[TransactionRecord]) -> None: @command_helper -class _SPCompression: - compression: str = option( - "--compression", +class _SPTranslation: + translation: str = option( + "--translation", "-c", type=click.Choice(["none", "chip-TBD"]), default="none", - help="Wallet Signer Protocol CHIP to use for compression of output", + help="Wallet Signer Protocol CHIP to use for translation of output", ) @@ -132,7 +132,7 @@ class _SPCompression: @command_helper -class SPIn(_SPCompression): +class SPIn(_SPTranslation): signer_protocol_input: Sequence[str] = option( "--signer-protocol-input", "-p", @@ -147,7 +147,7 @@ def read_sp_input(self, typ: Type[_T_ClvmStreamable]) -> List[_T_ClvmStreamable] for filename in self.signer_protocol_input: # pylint: disable=not-an-iterable with open(Path(filename), "rb") as file: with clvm_serialization_mode( - True, ALL_TRANSPORT_LAYERS[self.compression] if self.compression != "none" else None + True, ALL_TRANSLATION_LAYERS[self.translation] if self.translation != "none" else None ): final_list.append(typ.from_bytes(file.read())) @@ -155,7 +155,7 @@ def read_sp_input(self, typ: Type[_T_ClvmStreamable]) -> List[_T_ClvmStreamable] @command_helper -class SPOut(QrCodeDisplay, _SPCompression): +class SPOut(QrCodeDisplay, _SPTranslation): output_format: str = option( "--output-format", "-t", @@ -173,7 +173,7 @@ class SPOut(QrCodeDisplay, _SPCompression): def handle_clvm_output(self, outputs: List[ClvmStreamable]) -> None: with clvm_serialization_mode( - True, ALL_TRANSPORT_LAYERS[self.compression] if self.compression != "none" else None + True, ALL_TRANSLATION_LAYERS[self.translation] if self.translation != "none" else None ): if self.output_format == "hex": for output in outputs: diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 9316826f28be..4ff060cddcad 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -25,7 +25,7 @@ TransactionsIn, TransactionsOut, ) -from chia.rpc.util import ALL_TRANSPORT_LAYERS +from chia.rpc.util import ALL_TRANSLATION_LAYERS from chia.rpc.wallet_request_types import ( ApplySignatures, ExecuteSigningInstructions, @@ -806,7 +806,7 @@ async def test_signer_commands(wallet_environments: WalletTestFramework) -> None await GatherSigningInfoCMD( rpc_info=NeedsWalletRPC(client_info=client_info), sp_out=SPOut( - compression="chip-TBD", + translation="chip-TBD", output_format="file", output_file=["./temp-si"], ), @@ -816,11 +816,11 @@ async def test_signer_commands(wallet_environments: WalletTestFramework) -> None await ExecuteSigningInstructionsCMD( rpc_info=NeedsWalletRPC(client_info=client_info), sp_in=SPIn( - compression="chip-TBD", + translation="chip-TBD", signer_protocol_input=["./temp-si"], ), sp_out=SPOut( - compression="chip-TBD", + translation="chip-TBD", output_format="file", output_file=["./temp-sr"], ), @@ -830,7 +830,7 @@ async def test_signer_commands(wallet_environments: WalletTestFramework) -> None rpc_info=NeedsWalletRPC(client_info=client_info), txs_in=TransactionsIn(transaction_file_in="./temp-tb"), sp_in=SPIn( - compression="chip-TBD", + translation="chip-TBD", signer_protocol_input=["./temp-sr"], ), txs_out=TransactionsOut(transaction_file_out="./temp-stb"), @@ -871,7 +871,7 @@ def test_signer_command_default_parsing() -> None: GatherSigningInfoCMD( rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), sp_out=SPOut( - compression="none", + translation="none", output_format="hex", output_file=tuple(), ), @@ -885,11 +885,11 @@ def test_signer_command_default_parsing() -> None: ExecuteSigningInstructionsCMD( rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), sp_in=SPIn( - compression="none", + translation="none", signer_protocol_input=("sp-in",), ), sp_out=SPOut( - compression="none", + translation="none", output_format="hex", output_file=tuple(), ), @@ -903,7 +903,7 @@ def test_signer_command_default_parsing() -> None: rpc_info=NeedsWalletRPC(client_info=None, wallet_rpc_port=None, fingerprint=None), txs_in=TransactionsIn(transaction_file_in="in"), sp_in=SPIn( - compression="none", + translation="none", signer_protocol_input=("sp-in",), ), txs_out=TransactionsOut(transaction_file_out="out"), @@ -980,9 +980,9 @@ def to_wallet_api(_from: FooCoin) -> Coin: ) -FOO_COIN_TRANSPORT = TransportLayer( +FOO_COIN_TRANSLATION = TranslationLayer( [ - TransportLayerMapping( + TranslationLayerMapping( Coin, FooCoin, FooCoin.from_wallet_api, @@ -993,7 +993,7 @@ def to_wallet_api(_from: FooCoin) -> Coin: def test_signer_protocol_in(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setitem(ALL_TRANSPORT_LAYERS, "chip-TBD", FOO_COIN_TRANSPORT) + monkeypatch.setitem(ALL_TRANSLATION_LAYERS, "chip-TBD", FOO_COIN_TRANSLATION) @click.group() def cmd() -> None: @@ -1025,11 +1025,11 @@ def run(self) -> None: with runner.isolated_filesystem(): with open("some file", "wb") as file: - with clvm_serialization_mode(use=True, transport_layer=FOO_COIN_TRANSPORT): + with clvm_serialization_mode(use=True, translation_layer=FOO_COIN_TRANSLATION): file.write(bytes(coin)) with open("some file2", "wb") as file: - with clvm_serialization_mode(use=True, transport_layer=FOO_COIN_TRANSPORT): + with clvm_serialization_mode(use=True, translation_layer=FOO_COIN_TRANSLATION): file.write(bytes(coin)) result = runner.invoke( @@ -1044,7 +1044,7 @@ def run(self) -> None: "some file", "--signer-protocol-input", "some file2", - "--compression", + "--translation", "chip-TBD", ], catch_exceptions=False, @@ -1053,7 +1053,7 @@ def run(self) -> None: def test_signer_protocol_out(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setitem(ALL_TRANSPORT_LAYERS, "chip-TBD", FOO_COIN_TRANSPORT) + monkeypatch.setitem(ALL_TRANSLATION_LAYERS, "chip-TBD", FOO_COIN_TRANSLATION) @click.group() def cmd() -> None: @@ -1098,10 +1098,10 @@ def run(self) -> None: assert result.output != "" # separate test for QrCodeDisplay result = runner.invoke( - cmd, ["temp_cmd", "--output-format", "hex", "--compression", "chip-TBD"], catch_exceptions=False + cmd, ["temp_cmd", "--output-format", "hex", "--translation", "chip-TBD"], catch_exceptions=False ) assert result.output.strip() != coin_bytes.hex() - with clvm_serialization_mode(use=True, transport_layer=ALL_TRANSPORT_LAYERS["chip-TBD"]): + with clvm_serialization_mode(use=True, translation_layer=ALL_TRANSLATION_LAYERS["chip-TBD"]): assert result.output.strip() == bytes(coin).hex() + "\n" + bytes(coin).hex() From 268a0b504c3da52ecc1aad90c54a98e8e868dc9c Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 13:02:23 -0800 Subject: [PATCH 121/274] Fix asdict error --- chia/cmds/signer.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index 0b518e5f8d8f..f8b165be76dc 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -53,10 +53,9 @@ class QrCodeDisplay: default=2, show_default=True, ) - stop_event: Event = Event() - def _display_qr(self, index: int, max_index: int, code_list: List[QRCode]) -> None: - while not self.stop_event.is_set(): + def _display_qr(self, index: int, max_index: int, code_list: List[QRCode], stop_event: Event) -> None: + while not stop_event.is_set(): for qr_code in itertools.cycle(code_list): _clear_screen() qr_code.terminal(compact=True) @@ -65,7 +64,7 @@ def _display_qr(self, index: int, max_index: int, code_list: List[QRCode]) -> No for _ in range(self.rotation_speed * 100): time.sleep(0.01) - if self.stop_event.is_set(): + if stop_event.is_set(): return def display_qr_codes(self, blobs: List[bytes]) -> None: @@ -74,16 +73,16 @@ def display_qr_codes(self, blobs: List[bytes]) -> None: qr_codes = [[make_qr(chunk) for chunk in chks] for chks in chunks] for i, qr_code_list in enumerate(qr_codes): - self.stop_event.clear() - t = Thread(target=self._display_qr, args=(i, len(qr_codes), qr_code_list)) + stop_event = Event() + t = Thread(target=self._display_qr, args=(i, len(qr_codes), qr_code_list, stop_event)) t.start() try: input("") finally: - self.stop_event.set() + stop_event.set() t.join() - self.stop_event.clear() + stop_event.clear() @command_helper From 10836d8b2071cbfa391ff3cb557ceea135e2b2a6 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 13:20:15 -0800 Subject: [PATCH 122/274] coverage --- tests/wallet/test_signer_protocol.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index dcb0ee295901..a1dd239ceb95 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -285,6 +285,10 @@ def test_translation_layer() -> None: assert Coin.from_bytes(coin_bytes) == coin assert Coin.from_json_dict(coin_json) == coin + # Test a TranslationLayer edge case (no mapping for serialized object) + foo_spend = FooSpend.from_wallet_api(spend) + assert foo_spend == TranslationLayer([]).deserialize_from_translation(foo_spend) + def test_blind_signer_translation_layer() -> None: sum_hints: List[SumHint] = [ From baa8962e7400213fe1a7348d70df978c66dbda0c Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 31 Jan 2024 13:53:42 -0800 Subject: [PATCH 123/274] Forgoe rotation testing for now --- tests/wallet/test_signer_protocol.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 4ff060cddcad..ca40f37d5176 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -4,7 +4,6 @@ import dataclasses import threading import time -from threading import Thread from typing import List, Optional import click @@ -1105,17 +1104,7 @@ def run(self) -> None: assert result.output.strip() == bytes(coin).hex() + "\n" + bytes(coin).hex() -def test_qr_code_display(monkeypatch: pytest.MonkeyPatch) -> None: - # We monkeypatch the start method to start the thread and then wait 5 seconds before returning - # This is so the thread has a change to print the QR code multiple times before the input from click is recieved - old_start = Thread.start - - def new_start(self, *args) -> None: # type: ignore[no-untyped-def] - old_start(self, *args) - time.sleep(5) - - monkeypatch.setattr(Thread, "start", new_start) - +def test_qr_code_display() -> None: @click.group() def cmd() -> None: pass @@ -1130,11 +1119,11 @@ def run(self) -> None: runner = CliRunner() result = runner.invoke( cmd, - ["temp_cmd", "--qr-density", str(int(len(bytes_to_encode) / 2)), "--rotation-speed", "4"], + ["temp_cmd"], input="\n", catch_exceptions=False, ) # Would be good to check eventually that the QR codes are valid but segno doesn't seem to provide that ATM - assert result.output.count("Displaying QR Codes (1/2)") == 2 - assert result.output.count("Displaying QR Codes (2/2)") == 2 + assert result.output.count("Displaying QR Codes (1/2)") == 1 + assert result.output.count("Displaying QR Codes (2/2)") == 1 From 43889e4b1ba5128e5ebdbeb953562897d5c35a94 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 1 Feb 2024 10:39:20 -0800 Subject: [PATCH 124/274] Test coverage --- chia/cmds/signer.py | 4 ++-- tests/cmds/test_cmd_framework.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index f8b165be76dc..b4d2e5916dfd 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -32,7 +32,7 @@ def _clear_screen() -> None: @wallet_cmd.group("signer", help="Get information for an external signer") def signer_cmd() -> None: - pass + pass # pragma: no cover @command_helper @@ -243,7 +243,7 @@ async def run(self) -> None: signed_spends: List[Spend] = [spend for tx in signed_transactions for spend in tx.transaction_info.spends] final_signature: G2Element = G2Element() for signature in [sig for tx in signed_transactions for sig in tx.signatures]: - if signature.type != "bls_12381_aug_scheme": + if signature.type != "bls_12381_aug_scheme": # pragma: no cover print("No external spot for non BLS signatures in a spend") return final_signature = AugSchemeMPL.aggregate([final_signature, G2Element.from_bytes(signature.signature)]) diff --git a/tests/cmds/test_cmd_framework.py b/tests/cmds/test_cmd_framework.py index c91524ce72eb..05817bd8b36d 100644 --- a/tests/cmds/test_cmd_framework.py +++ b/tests/cmds/test_cmd_framework.py @@ -151,6 +151,16 @@ def run(self) -> None: ) assert result.output == "" + # Test that other variables named context are disallowed + with pytest.raises(ValueError, match="context"): + + @chia_command(cmd, "shouldnt_work", "blah") + class BadCMD: + context: int + + def run(self) -> None: + pass + @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="doesn't matter") @pytest.mark.parametrize( From a88bda1331d1d6a10b6f7f16800854951760330e Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 2 Feb 2024 09:27:35 +1300 Subject: [PATCH 125/274] ignore farmed p2_singleton coins --- tests/wallet/vault/test_vault_wallet.py | 51 ++++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 80aec489efcd..e550a2754d15 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -7,12 +7,14 @@ from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault -from tests.conftest import SOFTFORK_HEIGHTS, ConsensusMode +from tests.conftest import ConsensusMode from tests.environments.wallet import WalletStateTransition, WalletTestFramework @@ -54,7 +56,21 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b "set_remainder": True, } }, - ) + ), + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + # "confirmed_wallet_balance": -1, + "set_remainder": True, + } + }, + ), ] ) await env.node.keychain_proxy.add_public_key(launcher_id.hex()) @@ -70,23 +86,17 @@ def sign_message(message: bytes) -> bytes: @pytest.mark.parametrize( - "wallet_environments,active_softfork_height", - [ - ( - {"num_environments": 2, "blocks_needed": [1, 1]}, - SOFTFORK_HEIGHTS[2], - ) - ], - indirect=["wallet_environments"], + "wallet_environments", + [{"num_environments": 2, "blocks_needed": [1, 1]}], + indirect=True, ) @pytest.mark.parametrize("setup_function", [vault_setup]) @pytest.mark.parametrize("with_recovery", [True, False]) @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") @pytest.mark.anyio async def test_vault_creation( - setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], wallet_environments: WalletTestFramework, - active_softfork_height: uint32, + setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], with_recovery: bool, ) -> None: await setup_function(wallet_environments, with_recovery) @@ -133,11 +143,11 @@ async def test_vault_creation( }, post_block_balance_updates={ 1: { - # "confirmed_wallet_balance": -funding_amount, + "confirmed_wallet_balance": funding_amount * 2, "set_remainder": True, } }, - ) + ), ], ) @@ -150,27 +160,32 @@ async def test_vault_creation( Payment(p2_ph, uint64(500000000)), Payment(p2_ph, uint64(510000000)), ] - + total_spend = 1010000000 fee = uint64(100) p2_spends = await wallet.generate_p2_singleton_spends(payments, DEFAULT_TX_CONFIG, fee) - message, delegated_puz, delegated_sol = await wallet.generate_unsigned_vault_spend(payments, p2_spends) + message, delegated_puz, delegated_sol = await wallet.generate_unsigned_vault_spend(payments, p2_spends, fee=fee) sig = sign_message(message) tx_records = await wallet.generate_signed_vault_spend(sig, delegated_puz, delegated_sol, p2_spends, payments, fee) assert len(tx_records) == 1 + # Farm a block so the vault balance includes farmed coins from the test setup. + # Do this after generating the tx so we can be sure to spend the funding coins + await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) + await wallet_environments.process_pending_states( [ WalletStateTransition( pre_block_balance_updates={ 1: { + ">=#confirmed_wallet_balance": 2 * funding_amount, "set_remainder": True, } }, post_block_balance_updates={ 1: { - # "confirmed_wallet_balance": 0, + "confirmed_wallet_balance": -total_spend - fee, "set_remainder": True, } }, @@ -183,7 +198,7 @@ async def test_vault_creation( }, post_block_balance_updates={ 1: { - # "confirmed_wallet_balance": +funding_amount, + "confirmed_wallet_balance": total_spend, "set_remainder": True, } }, From 79e5e0276f9748e02ce45e82f4fa6a806b52351a Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 2 Feb 2024 11:16:51 -0800 Subject: [PATCH 126/274] jsonify_unsigned_txs -> full_jsonify --- chia/rpc/util.py | 2 +- tests/wallet/rpc/test_wallet_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index bb02826b050e..c7b5857a055e 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -133,7 +133,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s ] unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs(tx_records) - if request.get("jsonify_unsigned_txs", False): + if request.get("full_jsonify", False): response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] else: response["unsigned_transactions"] = [bytes(tx.as_program()).hex() for tx in unsigned_txs] diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index e027a4296172..57e6226cea7c 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -337,7 +337,7 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment "exclude_coin_amounts": [250000000000], "exclude_coins": [non_existent_coin.to_json_dict()], "reuse_puzhash": True, - "jsonify_unsigned_txs": True, + "full_jsonify": True, "push": True, }, ) From 33e1e725ec4af882716d0457eaf1c587bddfbdfe Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 2 Feb 2024 11:18:38 -0800 Subject: [PATCH 127/274] Move a class --- chia/rpc/wallet_request_types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 0e59088c4dc3..62fc3afaa0fa 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -20,14 +20,14 @@ class GetNotifications(Streamable): @streamable @dataclass(frozen=True) -class GatherSigningInfo(Streamable): - spends: List[Spend] +class GetNotificationsResponse(Streamable): + notifications: List[Notification] @streamable @dataclass(frozen=True) -class GetNotificationsResponse(Streamable): - notifications: List[Notification] +class GatherSigningInfo(Streamable): + spends: List[Spend] @streamable From f53440f2e628d4678d5950ff1751fb21c714298f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 5 Feb 2024 08:21:48 -0800 Subject: [PATCH 128/274] Add better type checking to framework --- chia/cmds/cmd_classes.py | 89 +++++++++++++++++- tests/cmds/test_cmd_framework.py | 149 ++++++++++++++++++++++++++++++- 2 files changed, 232 insertions(+), 6 deletions(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index 089f2501fa22..c88ca708156d 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -1,17 +1,34 @@ from __future__ import annotations import asyncio +import collections import inspect import sys from contextlib import asynccontextmanager from dataclasses import MISSING, dataclass, field, fields -from typing import Any, AsyncIterator, Callable, Dict, List, Optional, Protocol, Type, Union, get_type_hints +from typing import ( + Any, + AsyncIterator, + Callable, + Dict, + List, + Optional, + Protocol, + Type, + Union, + get_args, + get_origin, + get_type_hints, +) import click from typing_extensions import dataclass_transform from chia.cmds.cmds_util import get_wallet_client from chia.rpc.wallet_rpc_client import WalletRpcClient +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.byte_types import hexstr_to_bytes +from chia.util.streamable import is_type_SpecificOptional SyncCmd = Callable[..., None] @@ -46,6 +63,30 @@ def option(*param_decls: str, **kwargs: Any) -> Any: ) +class HexString(click.ParamType): + name = "hexstring" + + def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> bytes: + if isinstance(value, bytes): # This if is due to some poor handling on click's part + return value + try: + return hexstr_to_bytes(value) + except ValueError: + self.fail(f"{value} is not a valid hex string", param, ctx) + + +class HexString32(click.ParamType): + name = "hexstring32" + + def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> bytes32: + if isinstance(value, bytes32): # This if is due to some poor handling on click's part + return value + try: + return bytes32.from_hexstr(value) + except ValueError: + self.fail(f"{value} is not a valid 32-byte hex string", param, ctx) + + @dataclass(frozen=True) class _CommandParsingStage: my_dataclass: Type[ChiaCommand] @@ -119,11 +160,55 @@ def _generate_command_parser(cls: Type[ChiaCommand]) -> _CommandParsingStage: elif "is_command_option" not in _field.metadata or not _field.metadata["is_command_option"]: continue + if "type" not in _field.metadata: + origin = get_origin(hints[field_name]) + if origin == collections.abc.Sequence: + if "multiple" not in _field.metadata or not _field.metadata["multiple"]: + raise TypeError("Can only use Sequence with multiple=True") + else: + type_arg = get_args(hints[field_name])[0] + if "default" in _field.metadata and ( + not isinstance(_field.metadata["default"], tuple) + or any(not isinstance(item, type_arg) for item in _field.metadata["default"]) + ): + raise TypeError( + f"Default {_field.metadata['default']} is not a tuple " + f"or all of its elements are not of type {type_arg}" + ) + elif "multiple" in _field.metadata: + raise TypeError("Options with multiple=True must be Sequence[T]") + elif is_type_SpecificOptional(hints[field_name]): + if "required" not in _field.metadata or _field.metadata["required"]: + raise TypeError("Optional only allowed for options with required=False") + type_arg = get_args(hints[field_name])[0] + if "default" in _field.metadata and ( + not isinstance(_field.metadata["default"], type_arg) and _field.metadata["default"] is not None + ): + raise TypeError(f"Default {_field.metadata['default']} is not type {type_arg} or None") + elif origin is not None: + raise TypeError(f"Type {origin} invalid as a click type") + else: + if hints[field_name] == bytes: + type_arg = HexString() + elif hints[field_name] == bytes32: + type_arg = HexString32() + else: + type_arg = hints[field_name] + if "default" in _field.metadata and not isinstance(_field.metadata["default"], hints[field_name]): + raise TypeError(f"Default {_field.metadata['default']} is not type {type_arg}") + else: + type_arg = _field.metadata["type"] + kwarg_names.append(field_name) option_decorators.append( click.option( *_field.metadata["param_decls"], - **{k: v for k, v in _field.metadata.items() if k not in ("param_decls", "is_command_option")}, + type=type_arg, + **{ + k: v + for k, v in _field.metadata.items() + if k not in ("param_decls", "is_command_option", "type") + }, ) ) diff --git a/tests/cmds/test_cmd_framework.py b/tests/cmds/test_cmd_framework.py index 05817bd8b36d..910e4afba5e5 100644 --- a/tests/cmds/test_cmd_framework.py +++ b/tests/cmds/test_cmd_framework.py @@ -1,19 +1,18 @@ from __future__ import annotations from dataclasses import asdict -from typing import Any, Dict, TypeVar +from typing import Any, Dict, List, Optional, Sequence import click import pytest from click.testing import CliRunner from chia.cmds.cmd_classes import ChiaCommand, Context, NeedsWalletRPC, chia_command, option +from chia.types.blockchain_format.sized_bytes import bytes32 from tests.conftest import ConsensusMode from tests.environments.wallet import WalletTestFramework from tests.wallet.conftest import * # noqa -_T_Command = TypeVar("_T_Command", bound=ChiaCommand) - def check_click_parsing(cmd: ChiaCommand, *args: str) -> None: @click.group() @@ -159,7 +158,149 @@ class BadCMD: context: int def run(self) -> None: - pass + ... + + +def test_typing() -> None: + @click.group() + def cmd() -> None: + pass + + @chia_command(cmd, "temp_cmd", "blah") + class TempCMD: + integer: int = option("--integer", default=1, required=False) + text: str = option("--text", default="1", required=False) + boolean: bool = option("--boolean", default=True, required=False) + floating_point: float = option("--floating-point", default=1.1, required=False) + blob: bytes = option("--blob", default=b"foo", required=False) + blob32: bytes32 = option("--blob32", default=bytes32([1] * 32), required=False) + choice: str = option("--choice", default="a", type=click.Choice(["a", "b"]), required=False) + + def run(self) -> None: + ... + + check_click_parsing(TempCMD()) + check_click_parsing( + TempCMD(), + "--integer", + "1", + "--text", + "1", + "--boolean", + "true", + "--floating-point", + "1.1", + "--blob", + "0x666f6f", + "--blob32", + "0x0101010101010101010101010101010101010101010101010101010101010101", + "--choice", + "a", + ) + + # Test optional + @chia_command(cmd, "temp_cmd_optional", "blah") + class TempCMDOptional: + optional: Optional[int] = option("--optional", required=False) + + def run(self) -> None: + ... + + check_click_parsing(TempCMDOptional(optional=None)) + check_click_parsing(TempCMDOptional(optional=1), "--optional", "1") + + # Test optional failure + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_optional_bad", "blah") + class TempCMDOptionalBad: + optional: Optional[int] = option("--optional") + + def run(self) -> None: + ... + + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_optional_bad", "blah") + class TempCMDOptionalBad2: + optional: Optional[int] = option("--optional", required=True) + + def run(self) -> None: + ... + + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_optional_bad", "blah") + class TempCMDOptionalBad3: + optional: Optional[int] = option("--optional", default="string") + + def run(self) -> None: + ... + + @chia_command(cmd, "temp_cmd_optional_fine", "blah") + class TempCMDOptionalBad4: + optional: Optional[int] = option("--optional", default=None, required=False) + + def run(self) -> None: + ... + + # Test multiple + @chia_command(cmd, "temp_cmd_sequence", "blah") + class TempCMDSequence: + sequence: Sequence[int] = option("--sequence", multiple=True) + + def run(self) -> None: + ... + + check_click_parsing(TempCMDSequence(sequence=tuple())) + check_click_parsing(TempCMDSequence(sequence=(1, 2)), "--sequence", "1", "--sequence", "2") + + # Test sequence failure + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_sequence_bad", "blah") + class TempCMDSequenceBad: + sequence: Sequence[int] = option("--sequence") + + def run(self) -> None: + ... + + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_sequence_bad", "blah") + class TempCMDSequenceBad2: + sequence: int = option("--sequence", multiple=True) + + def run(self) -> None: + ... + + with pytest.raises(ValueError): + + @chia_command(cmd, "temp_cmd_sequence_bad", "blah") + class TempCMDSequenceBad3: + sequence: Sequence[int] = option("--sequence", default=[1, 2, 3], multiple=True) + + def run(self) -> None: + ... + + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_sequence_bad", "blah") + class TempCMDSequenceBad4: + sequence: Sequence[int] = option("--sequence", default=(1, 2, "3"), multiple=True) + + def run(self) -> None: + ... + + # Test invalid type + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_bad_type", "blah") + class TempCMDBadType: + sequence: List[int] = option("--sequence") + + def run(self) -> None: + ... @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="doesn't matter") From 32c247bec177d2e970633d0fcf2ab437d67b8df3 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 7 Feb 2024 07:16:06 -0800 Subject: [PATCH 129/274] explicitly state kwargs on _CommandParsingStage Co-authored-by: Kyle Altendorf --- chia/cmds/cmd_classes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index c88ca708156d..cff56f22fd48 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -213,11 +213,11 @@ def _generate_command_parser(cls: Type[ChiaCommand]) -> _CommandParsingStage: ) return _CommandParsingStage( - cls, - option_decorators, - subclasses, - kwarg_names, - needs_context, + my_dataclass=cls, + my_option_decorators=option_decorators, + my_subclasses=subclasses, + my_kwarg_names=kwarg_names, + _needs_context=needs_context, ) From 9a462ee598dfc07c8a3448731a0b8b82e8783dd1 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 7 Feb 2024 07:48:11 -0800 Subject: [PATCH 130/274] Address comments by @altendky --- chia/cmds/cmd_classes.py | 69 +++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index cff56f22fd48..051641f689ce 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -55,11 +55,12 @@ def option(*param_decls: str, **kwargs: Any) -> Any: return field( # pylint: disable=invalid-field-call metadata=dict( - is_command_option=True, - param_decls=tuple(param_decls), - **kwargs, + option_args=dict( + param_decls=tuple(param_decls), + **kwargs, + ), ), - default=kwargs["default"] if "default" in kwargs else default_default, + default=kwargs.get("default", default_default), ) @@ -147,7 +148,11 @@ def _generate_command_parser(cls: Type[ChiaCommand]) -> _CommandParsingStage: for _field in _fields: field_name = _field.name - if isinstance(hints[field_name], type) and issubclass(hints[field_name], _CommandHelper): + if ( + isinstance(hints[field_name], type) + and hasattr(hints[field_name], "__command_helper__") + and hints[field_name].__command_helper__ + ): subclasses[field_name] = _generate_command_parser(hints[field_name]) else: if field_name == "context": @@ -157,34 +162,36 @@ def _generate_command_parser(cls: Type[ChiaCommand]) -> _CommandParsingStage: needs_context = True kwarg_names.append(field_name) continue - elif "is_command_option" not in _field.metadata or not _field.metadata["is_command_option"]: + + if "option_args" not in _field.metadata: continue + option_args = _field.metadata["option_args"] - if "type" not in _field.metadata: + if "type" not in option_args: origin = get_origin(hints[field_name]) if origin == collections.abc.Sequence: - if "multiple" not in _field.metadata or not _field.metadata["multiple"]: + if "multiple" not in option_args or not option_args["multiple"]: raise TypeError("Can only use Sequence with multiple=True") else: type_arg = get_args(hints[field_name])[0] - if "default" in _field.metadata and ( - not isinstance(_field.metadata["default"], tuple) - or any(not isinstance(item, type_arg) for item in _field.metadata["default"]) + if "default" in option_args and ( + not isinstance(option_args["default"], tuple) + or any(not isinstance(item, type_arg) for item in option_args["default"]) ): raise TypeError( - f"Default {_field.metadata['default']} is not a tuple " + f"Default {option_args['default']} is not a tuple " f"or all of its elements are not of type {type_arg}" ) - elif "multiple" in _field.metadata: + elif "multiple" in option_args: raise TypeError("Options with multiple=True must be Sequence[T]") elif is_type_SpecificOptional(hints[field_name]): - if "required" not in _field.metadata or _field.metadata["required"]: + if "required" not in option_args or option_args["required"]: raise TypeError("Optional only allowed for options with required=False") type_arg = get_args(hints[field_name])[0] - if "default" in _field.metadata and ( - not isinstance(_field.metadata["default"], type_arg) and _field.metadata["default"] is not None + if "default" in option_args and ( + not isinstance(option_args["default"], type_arg) and option_args["default"] is not None ): - raise TypeError(f"Default {_field.metadata['default']} is not type {type_arg} or None") + raise TypeError(f"Default {option_args['default']} is not type {type_arg} or None") elif origin is not None: raise TypeError(f"Type {origin} invalid as a click type") else: @@ -194,21 +201,17 @@ def _generate_command_parser(cls: Type[ChiaCommand]) -> _CommandParsingStage: type_arg = HexString32() else: type_arg = hints[field_name] - if "default" in _field.metadata and not isinstance(_field.metadata["default"], hints[field_name]): - raise TypeError(f"Default {_field.metadata['default']} is not type {type_arg}") + if "default" in option_args and not isinstance(option_args["default"], hints[field_name]): + raise TypeError(f"Default {option_args['default']} is not type {type_arg}") else: - type_arg = _field.metadata["type"] + type_arg = option_args["type"] kwarg_names.append(field_name) option_decorators.append( click.option( - *_field.metadata["param_decls"], + *option_args["param_decls"], type=type_arg, - **{ - k: v - for k, v in _field.metadata.items() - if k not in ("param_decls", "is_command_option", "type") - }, + **{k: v for k, v in option_args.items() if k not in ("param_decls", "is_command_option", "type")}, ) ) @@ -264,20 +267,14 @@ def _chia_command(cls: Type[ChiaCommand]) -> Type[ChiaCommand]: return _chia_command -class _CommandHelper: - pass - - @dataclass_transform() def command_helper(cls: Type[Any]) -> Type[Any]: if sys.version_info < (3, 10): # stuff below 3.10 doesn't support kw_only - return dataclass(frozen=True)( - type(cls.__name__, (dataclass(frozen=True)(cls), _CommandHelper), {}) - ) # pragma: no cover + new_cls = dataclass(frozen=True)(cls) # pragma: no cover else: - return dataclass(frozen=True, kw_only=True)( - type(cls.__name__, (dataclass(frozen=True, kw_only=True)(cls), _CommandHelper), {}) - ) + new_cls = dataclass(frozen=True, kw_only=True)(cls) + setattr(new_cls, "__command_helper__", True) + return new_cls Context = Dict[str, Any] From b5e58577e6ba27a2b7f3eeb62a1d6344f2f5ca90 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 9 Feb 2024 07:45:32 +1300 Subject: [PATCH 131/274] spend vault funds --- chia/wallet/vault/vault_drivers.py | 12 ++++ chia/wallet/vault/vault_wallet.py | 78 ++++++++++++++++++++----- chia/wallet/wallet_singleton_store.py | 33 +++++++++++ chia/wallet/wallet_state_manager.py | 26 ++++++++- tests/wallet/vault/test_vault_wallet.py | 4 +- 5 files changed, 137 insertions(+), 16 deletions(-) diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 60a69dd259e2..58bf27ce38dd 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -12,6 +12,7 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE, puzzle_hash_for_pk from chia.wallet.puzzles.singleton_top_layer_v1_1 import ( SINGLETON_LAUNCHER_HASH, + SINGLETON_MOD, SINGLETON_MOD_HASH, puzzle_for_singleton, puzzle_hash_for_singleton, @@ -118,6 +119,17 @@ def get_p2_singleton_puzzle_hash(launcher_id: bytes32) -> bytes32: return get_p2_singleton_puzzle(launcher_id).get_tree_hash() +def match_vault_puzzle(mod: Program, curried_args: Program) -> bool: + try: + if mod == SINGLETON_MOD: + if curried_args.at("rf").uncurry()[0] == P2_1_OF_N_MOD: + return True + except ValueError: + # We just pass here to prevent spamming logs with error messages when WSM checks incoming coins + pass + return False + + # SOLUTIONS def get_recovery_solution(amount: uint64, bls_pk: G1Element) -> Program: recovery_conditions = get_recovery_conditions(bls_pk, amount) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 440992b6ad43..bb6db92740ac 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -32,6 +32,7 @@ SumHint, ) from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.compute_hints import compute_spend_hints_and_additions from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend @@ -76,8 +77,13 @@ async def create( async def get_new_puzzle(self) -> Program: dr = await self.wallet_state_manager.get_unused_derivation_record(self.id()) - puzzle = construct_p2_delegated_secp( - dr.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, dr.puzzle_hash + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(dr.index).get_tree_hash() + puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, ) return puzzle @@ -169,18 +175,12 @@ async def generate_unsigned_vault_spend( if change > 0: change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.vault_info.launcher_coin_id) primaries.append(Payment(change_puzzle_hash, uint64(change))) - # Create the vault spend - vault_inner_puzzle = get_vault_inner_puzzle( - self.vault_info.pubkey, - self.wallet_state_manager.constants.GENESIS_CHALLENGE, - self.vault_info.hidden_puzzle_hash, - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, - ) conditions = [primary.as_condition() for primary in primaries] + next_puzzle_hash = await self.get_new_puzzlehash() + # TODO: should the vault inner puz create this condition? recreate_vault_condition = CreateCoin( - vault_inner_puzzle.get_tree_hash(), uint64(self.vault_info.coin.amount), memos=[self.vault_info.launcher_id] + next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] ).to_program() conditions.append(recreate_vault_condition) announcements = [CreatePuzzleAnnouncement(spend.coin.name()).to_program() for spend in p2_spends] @@ -544,11 +544,63 @@ async def sync_vault_launcher(self) -> None: p2_puzzle_hash = self.get_p2_singleton_puzzle_hash() await self.wallet_state_manager.add_interested_puzzle_hashes([p2_puzzle_hash], [self.id()]) + # add the singleton record to store + await self.wallet_state_manager.singleton_store.add_eve_record( + self.id(), + coin_state.coin, + launcher_spend, + inner_puzzle_hash, + lineage_proof, + uint32(coin_state.spent_height) if coin_state.spent_height else uint32(0), + pending=False, + custom_data=bytes(json.dumps(vault_info.to_json_dict()), "utf-8"), + ) + + async def update_vault_singleton( + self, next_inner_puzzle: Program, coin_spend: CoinSpend, coin_state: CoinState + ) -> None: + hints, _ = compute_spend_hints_and_additions(coin_spend) + inner_puzzle_hash = hints[coin_state.coin.name()].hint + assert inner_puzzle_hash + dr = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(inner_puzzle_hash) + assert dr is not None + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(dr.index).get_tree_hash() + next_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, + ) + + # get the parent state to create lineage proof + wallet_node: Any = self.wallet_state_manager.wallet_node + peer = wallet_node.get_full_node_peer() + assert peer is not None + parent_state = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] + parent_spend = await fetch_coin_spend(uint32(parent_state.spent_height), parent_state.coin, peer) + parent_puzzle = parent_spend.puzzle_reveal.to_program() + parent_inner_puzzle_hash = parent_puzzle.uncurry()[1].at("rf").get_tree_hash() + lineage_proof = LineageProof( + parent_state.coin.parent_coin_info, parent_inner_puzzle_hash, parent_state.coin.amount + ) + new_vault_info = VaultInfo( + coin_state.coin, + self.vault_info.launcher_id, + self.vault_info.pubkey, + hidden_puzzle_hash, + next_inner_puzzle.get_tree_hash(), + self.vault_info.is_recoverable, + self.vault_info.launcher_coin_id, + lineage_proof, + ) + + await self.wallet_state_manager.singleton_store.add_spend(self.id(), coin_spend) + await self.save_info(new_vault_info) + async def save_info(self, vault_info: VaultInfo) -> None: self.vault_info = vault_info current_info = self.wallet_info data_str = json.dumps(vault_info.to_json_dict()) wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) self.wallet_info = wallet_info - # TODO: push new info to user store - # await self.wallet_state_manager.user_store.update_wallet(wallet_info) diff --git a/chia/wallet/wallet_singleton_store.py b/chia/wallet/wallet_singleton_store.py index 56f9d457f2c7..8f8ed803a46d 100644 --- a/chia/wallet/wallet_singleton_store.py +++ b/chia/wallet/wallet_singleton_store.py @@ -81,6 +81,39 @@ async def save_singleton(self, record: SingletonRecord) -> None: ), ) + async def add_eve_record( + self, + wallet_id: uint32, + eve_coin: Coin, + parent_coin_spend: CoinSpend, + inner_puzzle_hash: bytes32, + lineage_proof: LineageProof, + removed_height: uint32 = uint32(0), + pending: bool = False, + custom_data: Optional[bytes] = None, + ) -> None: + pending_int = 1 if pending else False + async with self.db_wrapper.writer_maybe_transaction() as conn: + columns = ( + "coin_id, coin, singleton_id, wallet_id, parent_coin_spend, inner_puzzle_hash, " + "pending, removed_height, lineage_proof, custom_data" + ) + await conn.execute( + f"INSERT or REPLACE INTO singletons ({columns}) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + eve_coin.name().hex(), + json.dumps(eve_coin.to_json_dict()), + eve_coin.name().hex(), + wallet_id, + bytes(parent_coin_spend), + inner_puzzle_hash, + pending_int, + removed_height, + bytes(lineage_proof), + custom_data, + ), + ) + async def add_spend( self, wallet_id: uint32, diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 224d5e38ca82..faaeb55160b1 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -145,6 +145,7 @@ ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash +from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault 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 @@ -804,6 +805,14 @@ async def determine_coin_type( uncurried = uncurry_puzzle(puzzle) + from chia.wallet.vault.vault_drivers import match_vault_puzzle + + vault_check = match_vault_puzzle(uncurried.mod, uncurried.args) + if vault_check: + await self.handle_vault(puzzle, coin_spend, coin_state) + # Return None since we don't want to add the vault singleton to the normal wallet coin store + return None, None + dao_ids = [] wallets = self.wallets.values() for wallet in wallets: @@ -1078,6 +1087,21 @@ async def is_standard_wallet_tx(self, coin_state: CoinState) -> bool: wallet_identifier = await self.get_wallet_identifier_for_puzzle_hash(coin_state.coin.puzzle_hash) return wallet_identifier is not None and wallet_identifier.type == WalletType.STANDARD_WALLET + async def handle_vault( + self, + puzzle: Program, + coin_spend: CoinSpend, + coin_state: CoinState, + ) -> None: + if isinstance(self.observation_root, VaultRoot): + for wallet in self.wallets.values(): + if wallet.type() == WalletType.STANDARD_WALLET: + assert isinstance(wallet, Vault) + # make sure we've got the singleton coin + if coin_state.coin.amount % 2 == 1: + # Update the vault singleton record + await wallet.update_vault_singleton(puzzle, coin_spend, coin_state) + async def handle_dao_cat( self, curried_args: Iterator[Program], @@ -1723,6 +1747,7 @@ async def _add_coin_states( wallet_identifier = WalletIdentifier(uint32(local_record.wallet_id), local_record.wallet_type) elif coin_state.created_height is not None: wallet_identifier, coin_data = await self.determine_coin_type(peer, coin_state, fork_height) + try: dl_wallet = self.get_dl_wallet() except ValueError: @@ -2161,7 +2186,6 @@ async def get_wallet_identifier_for_coin( coin_record = await self.coin_store.get_coin_record(coin.name()) if coin_record is not None: wallet_identifier = WalletIdentifier(uint32(coin_record.wallet_id), coin_record.wallet_type) - return wallet_identifier async def get_wallet_identifier_for_hinted_coin(self, coin: Coin, hint: bytes32) -> Optional[WalletIdentifier]: diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index e550a2754d15..fb5263c03fc9 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -170,8 +170,8 @@ async def test_vault_creation( tx_records = await wallet.generate_signed_vault_spend(sig, delegated_puz, delegated_sol, p2_spends, payments, fee) assert len(tx_records) == 1 - # Farm a block so the vault balance includes farmed coins from the test setup. - # Do this after generating the tx so we can be sure to spend the funding coins + # Farm a block so the vault balance includes farmed coins from the test setup in pre-block update. + # Do this after generating the tx so we can be sure to spend the right funding coins await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) await wallet_environments.process_pending_states( From 603cf479d2b5f14c60bff700694d052c3237142a Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 9 Feb 2024 07:53:01 +1300 Subject: [PATCH 132/274] use make_spend for vault CoinSpends --- chia/wallet/vault/vault_wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index bb6db92740ac..0ef775164f1e 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -13,7 +13,7 @@ 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.coin_spend import CoinSpend, make_spend from chia.types.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64, uint128 @@ -246,7 +246,7 @@ async def generate_signed_vault_spend( vault_inner_solution, ) - vault_spend = CoinSpend(self.vault_info.coin, full_puzzle, full_solution) + vault_spend = make_spend(self.vault_info.coin, full_puzzle, full_solution) all_spends = [*p2_spends, vault_spend] spend_bundle = SpendBundle(all_spends, G2Element()) @@ -434,7 +434,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: assert full_puzzle.get_tree_hash() == vault_coin.puzzle_hash full_solution = get_vault_full_solution(self.vault_info.lineage_proof, amount, inner_solution) - recovery_spend = SpendBundle([CoinSpend(vault_coin, full_puzzle, full_solution)], G2Element()) + recovery_spend = SpendBundle([make_spend(vault_coin, full_puzzle, full_solution)], G2Element()) # 2. Generate the Finish Recovery Spend recovery_finish_puzzle = get_recovery_finish_puzzle( @@ -448,7 +448,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: lineage = LineageProof(self.vault_info.coin.name(), inner_puzzle.get_tree_hash(), amount) full_recovery_solution = get_vault_full_solution(lineage, amount, recovery_solution) finish_spend = SpendBundle( - [CoinSpend(recovery_coin, full_recovery_puzzle, full_recovery_solution)], G2Element() + [make_spend(recovery_coin, full_recovery_puzzle, full_recovery_solution)], G2Element() ) # make the tx records From 50a1fe1b7202342c7292cd11e347e29b8ed0b343 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 13 Feb 2024 18:49:04 +1300 Subject: [PATCH 133/274] generate_signed_transaction --- chia/wallet/vault/vault_wallet.py | 156 +++++++++++++++++------- tests/wallet/vault/test_vault_wallet.py | 59 ++------- 2 files changed, 122 insertions(+), 93 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 0ef775164f1e..7ced07e8b927 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -11,7 +11,6 @@ from chia.protocols.wallet_protocol import CoinState 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, make_spend from chia.types.signing_mode import SigningMode @@ -38,7 +37,6 @@ from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, - construct_secp_message, construct_vault_merkle_tree, get_p2_singleton_puzzle, get_p2_singleton_puzzle_hash, @@ -104,71 +102,105 @@ async def generate_signed_transaction( extra_conditions: Tuple[Condition, ...] = tuple(), **kwargs: Unpack[GSTOptionalArgs], ) -> List[TransactionRecord]: - raise NotImplementedError("vault wallet") + """ + Creates Un-signed transactions to be passed into signer. + """ + negative_change_allowed: bool = kwargs.get("negative_change_allowed", False) + if primaries is None: + non_change_amount: int = amount + else: + non_change_amount = amount + sum(p.amount for p in primaries) + + non_change_amount += sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) + coin_spends = await self._generate_unsigned_transaction( + amount, + puzzle_hash, + tx_config, + fee=fee, + coins=coins, + primaries_input=primaries, + memos=memos, + negative_change_allowed=negative_change_allowed, + puzzle_decorator_override=puzzle_decorator_override, + extra_conditions=extra_conditions, + ) + spend_bundle = SpendBundle(coin_spends, G2Element()) + + tx_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=puzzle_hash, + amount=uint64(non_change_amount), + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=[], + removals=[], + wallet_id=self.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=spend_bundle.name(), + valid_times=parse_timelock_info(tuple()), + ) + return [tx_record] async def generate_p2_singleton_spends( self, - primaries: List[Payment], + amount: uint64, tx_config: TXConfig, - fee: uint64 = uint64(0), coins: Optional[Set[Coin]] = None, - extra_conditions: Tuple[Condition, ...] = tuple(), ) -> List[CoinSpend]: - total_amount = ( - sum(primary.amount for primary in primaries) - + fee - + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) - ) total_balance = await self.get_spendable_balance() if coins is None: - if total_amount > total_balance: + if amount > total_balance: raise ValueError( - f"Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {total_amount} mojos" + f"Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {amount} mojos" ) coins = await self.select_coins( - uint64(total_amount), + uint64(amount), tx_config.coin_selection_config, ) assert len(coins) > 0 - spend_value = sum([coin.amount for coin in coins]) - change = spend_value - total_amount - assert change >= 0 - if change > 0: - change_puzzle_hash: bytes32 = next(iter(coins)).puzzle_hash - primaries.append(Payment(change_puzzle_hash, uint64(change))) - - spends: List[CoinSpend] = [] - - # 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") p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.vault_info.launcher_coin_id) - serialized_puzzle: SerializedProgram = SerializedProgram.from_bytes(bytes(p2_singleton_puzzle)) - for coin in coins: + spends: List[CoinSpend] = [] + for coin in list(coins): p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin.name()]) - spends.append( - CoinSpend(coin, serialized_puzzle, SerializedProgram.from_bytes(bytes(p2_singleton_solution))) - ) + spends.append(make_spend(coin, p2_singleton_puzzle, p2_singleton_solution)) return spends - async def generate_unsigned_vault_spend( + async def _generate_unsigned_transaction( self, - primaries: List[Payment], - p2_spends: List[CoinSpend], - memos: Optional[List[bytes]] = None, + 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, + memos: Optional[List[bytes]] = None, + negative_change_allowed: bool = False, + puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> Tuple[bytes, Program, Program]: + ) -> List[CoinSpend]: + primaries = [] + if primaries_input is not None: + primaries.extend(primaries_input) total_amount = ( - sum(primary.amount for primary in primaries) + amount + + sum(primary.amount for primary in primaries) + fee + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) ) - coins = [spend.coin for spend in p2_spends] + + p2_singleton_spends = await self.generate_p2_singleton_spends(uint64(total_amount), tx_config, coins=coins) + + coins = set([spend.coin for spend in p2_singleton_spends]) spend_value = sum([coin.amount for coin in coins]) change = spend_value - total_amount assert change >= 0 @@ -183,20 +215,56 @@ async def generate_unsigned_vault_spend( next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] ).to_program() conditions.append(recreate_vault_condition) - announcements = [CreatePuzzleAnnouncement(spend.coin.name()).to_program() for spend in p2_spends] + announcements = [CreatePuzzleAnnouncement(spend.coin.name()).to_program() for spend in p2_singleton_spends] conditions.extend(announcements) delegated_puzzle = puzzle_for_conditions(conditions) delegated_solution = solution_for_conditions(conditions) - message_to_sign = construct_secp_message( - delegated_puzzle.get_tree_hash(), - self.vault_info.coin.name(), + secp_puzzle = construct_p2_delegated_secp( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ) + vault_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, self.vault_info.hidden_puzzle_hash, + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, ) - return message_to_sign, delegated_puzzle, delegated_solution + secp_solution = Program.to( + [ + delegated_puzzle, + delegated_solution, + None, # Slot for signed message + self.vault_info.coin.name(), + ] + ) + if self.vault_info.is_recoverable: + recovery_puzzle_hash = get_recovery_puzzle( + secp_puzzle.get_tree_hash(), + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, + ).get_tree_hash() + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) + else: + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash()) + proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) + vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) + + full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, vault_inner_puzzle) + full_solution = get_vault_full_solution( + self.vault_info.lineage_proof, + uint64(self.vault_info.coin.amount), + vault_inner_solution, + ) + + vault_spend = make_spend(self.vault_info.coin, full_puzzle, full_solution) + all_spends = [*p2_singleton_spends, vault_spend] + + return all_spends async def generate_signed_vault_spend( self, diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index fb5263c03fc9..d05a929fe475 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -1,16 +1,15 @@ from __future__ import annotations from hashlib import sha256 -from typing import Awaitable, Callable +from typing import Awaitable, Callable, List import pytest from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG -from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault @@ -154,54 +153,16 @@ async def test_vault_creation( recs = await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG) coin = recs.pop() assert coin.amount == funding_amount - p2_ph = await funding_wallet.get_new_puzzlehash() + recipient_ph = await funding_wallet.get_new_puzzlehash() - payments = [ - Payment(p2_ph, uint64(500000000)), - Payment(p2_ph, uint64(510000000)), + primaries = [ + Payment(recipient_ph, uint64(500000000)), + Payment(recipient_ph, uint64(510000000)), ] - total_spend = 1010000000 + amount = uint64(1000000) fee = uint64(100) - p2_spends = await wallet.generate_p2_singleton_spends(payments, DEFAULT_TX_CONFIG, fee) - message, delegated_puz, delegated_sol = await wallet.generate_unsigned_vault_spend(payments, p2_spends, fee=fee) - sig = sign_message(message) - - tx_records = await wallet.generate_signed_vault_spend(sig, delegated_puz, delegated_sol, p2_spends, payments, fee) - assert len(tx_records) == 1 - - # Farm a block so the vault balance includes farmed coins from the test setup in pre-block update. - # Do this after generating the tx so we can be sure to spend the right funding coins - await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={ - 1: { - ">=#confirmed_wallet_balance": 2 * funding_amount, - "set_remainder": True, - } - }, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": -total_spend - fee, - "set_remainder": True, - } - }, - ), - WalletStateTransition( - pre_block_balance_updates={ - 1: { - "set_remainder": True, - } - }, - post_block_balance_updates={ - 1: { - "confirmed_wallet_balance": total_spend, - "set_remainder": True, - } - }, - ), - ], + unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( + amount, recipient_ph, DEFAULT_TX_CONFIG, primaries=primaries, fee=fee ) + assert len(unsigned_txs) == 1 From da9f4514a65d9d21ea07e02a9b08dc88584ee4ef Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 14 Feb 2024 16:51:30 +1300 Subject: [PATCH 134/274] move gather_signing_info to wallet --- chia/wallet/wallet.py | 26 +++++++++++++++++++++++++ chia/wallet/wallet_protocol.py | 3 +++ chia/wallet/wallet_state_manager.py | 26 ++----------------------- tests/wallet/vault/test_vault_wallet.py | 4 ++++ 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 62463efd6f68..3e93c6285b0d 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -14,6 +14,7 @@ from chia.types.coin_spend import CoinSpend, make_spend from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.types.spend_bundle import SpendBundle +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.util.streamable import Streamable @@ -50,6 +51,7 @@ SignedTransaction, SigningInstructions, SigningResponse, + SigningTarget, Spend, SumHint, TransactionInfo, @@ -709,3 +711,27 @@ def handle_own_derivation(self) -> bool: def derivation_for_index(self, index: int) -> List[DerivationRecord]: # pragma: no cover raise NotImplementedError() + + async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: + pks: List[bytes] = [] + signing_targets: List[SigningTarget] = [] + for coin_spend in coin_spends: + _coin_spend = coin_spend.as_coin_spend() + # Get AGG_SIG conditions + conditions_dict = conditions_dict_for_solution( + _coin_spend.puzzle_reveal.to_program(), + _coin_spend.solution.to_program(), + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + # Create signature + for pk_bytes, msg in pkm_pairs_for_conditions_dict( + conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + pks.append(pk_bytes) + fingerprint: bytes = G1Element.from_bytes(pk_bytes).get_fingerprint().to_bytes(4, "big") + signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) + + return SigningInstructions( + await self.wallet_state_manager.key_hints_for_pubkeys(pks), + signing_targets, + ) diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index a2ab5bf7f287..2db7a58002ef 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -179,6 +179,9 @@ def make_solution( async def get_puzzle(self, new: bool) -> Program: ... + async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: + ... + class GSTOptionalArgs(TypedDict): # DataLayerWallet diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f09e7d735fee..de2d9631c206 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -52,7 +52,6 @@ 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.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.db_synchronous import db_synchronous_on from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err @@ -114,7 +113,6 @@ SignedTransaction, SigningInstructions, SigningResponse, - SigningTarget, Spend, SumHint, TransactionInfo, @@ -2626,28 +2624,8 @@ async def key_hints_for_pubkeys(self, pks: List[bytes]) -> KeyHints: ) async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: - pks: List[bytes] = [] - signing_targets: List[SigningTarget] = [] - for coin_spend in coin_spends: - _coin_spend = coin_spend.as_coin_spend() - # Get AGG_SIG conditions - conditions_dict = conditions_dict_for_solution( - _coin_spend.puzzle_reveal.to_program(), - _coin_spend.solution.to_program(), - self.constants.MAX_BLOCK_COST_CLVM, - ) - # Create signature - for pk_bytes, msg in pkm_pairs_for_conditions_dict( - conditions_dict, _coin_spend.coin, self.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - pks.append(pk_bytes) - fingerprint: bytes = G1Element.from_bytes(pk_bytes).get_fingerprint().to_bytes(4, "big") - signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) - - return SigningInstructions( - await self.key_hints_for_pubkeys(pks), - signing_targets, - ) + signing_instructions = await self.main_wallet.gather_signing_info(coin_spends) + return signing_instructions async def gather_signing_info_for_bundles(self, bundles: List[SpendBundle]) -> List[UnsignedTransaction]: utxs: List[UnsignedTransaction] = [] diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index d05a929fe475..0916b31fee46 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -7,6 +7,7 @@ from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG +# from chia.rpc.wallet_request_types import GatherSigningInfo from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord @@ -166,3 +167,6 @@ async def test_vault_creation( amount, recipient_ph, DEFAULT_TX_CONFIG, primaries=primaries, fee=fee ) assert len(unsigned_txs) == 1 + + # gather_sig_info = GatherSigningInfo(unsigned_txs[0].spend_bundle.coin_spends) + # await env.rpc_client.gather_signing_info(gather_sig_info) From 15054d270c580f5f7b50341bcc8d80aa2759a7b9 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Sat, 24 Feb 2024 12:00:19 +1300 Subject: [PATCH 135/274] signing instructions --- chia/wallet/vault/vault_wallet.py | 67 ++++++++++++++++++++++--- tests/wallet/vault/test_vault_wallet.py | 41 +++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 7ced07e8b927..ff93847a0e44 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element +from ecdsa.keys import SigningKey from typing_extensions import Unpack from chia.protocols.wallet_protocol import CoinState @@ -15,6 +16,7 @@ from chia.types.coin_spend import CoinSpend, make_spend from chia.types.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle +from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.wallet.coin_selection import select_coins from chia.wallet.conditions import Condition, CreateCoin, CreatePuzzleAnnouncement, parse_timelock_info @@ -24,11 +26,14 @@ from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions from chia.wallet.signer_protocol import ( PathHint, + Signature, SignedTransaction, SigningInstructions, SigningResponse, + SigningTarget, Spend, SumHint, + TransactionInfo, ) from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_hints import compute_spend_hints_and_additions @@ -51,6 +56,7 @@ get_vault_inner_puzzle_hash, get_vault_inner_solution, get_vault_proof, + match_vault_puzzle, ) from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo from chia.wallet.vault.vault_root import VaultRoot @@ -200,7 +206,7 @@ async def _generate_unsigned_transaction( p2_singleton_spends = await self.generate_p2_singleton_spends(uint64(total_amount), tx_config, coins=coins) - coins = set([spend.coin for spend in p2_singleton_spends]) + coins = {spend.coin for spend in p2_singleton_spends} spend_value = sum([coin.amount for coin in coins]) change = spend_value - total_amount assert change >= 0 @@ -263,7 +269,7 @@ async def _generate_unsigned_transaction( vault_spend = make_spend(self.vault_info.coin, full_puzzle, full_solution) all_spends = [*p2_singleton_spends, vault_spend] - + return all_spends async def generate_signed_vault_spend( @@ -363,21 +369,70 @@ async def get_puzzle_hash(self, new: bool) -> bytes32: return await self.get_new_puzzlehash() return record.puzzle_hash + async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: + pk = self.vault_info.pubkey + vault_spend = [spend for spend in coin_spends if spend.as_coin_spend().coin == self.vault_info.coin][0] + inner_sol = vault_spend.solution.at("rrf") + secp_puz = inner_sol.at("rf") + secp_sol = inner_sol.at("rrf") + _, secp_args = secp_puz.uncurry() + genesis_challenge = secp_args.at("f").as_atom() + hidden_puzzle_hash = secp_args.at("rrf").as_atom() + delegated_puzzle_hash = secp_sol.at("f").get_tree_hash() + coin_id = secp_sol.at("rrrf").as_atom() + message = delegated_puzzle_hash + coin_id + genesis_challenge + hidden_puzzle_hash + fingerprint = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") + target = SigningTarget(fingerprint, message, std_hash(pk + message)) + sig_info = SigningInstructions( + await self.wallet_state_manager.key_hints_for_pubkeys([pk]), + [target], + ) + return sig_info + async def apply_signatures( self, spends: List[Spend], signing_responses: List[SigningResponse] ) -> SignedTransaction: - raise NotImplementedError("vault wallet") + signed_spends = [] + for spend in spends: + mod, curried_args = spend.puzzle.uncurry() + if match_vault_puzzle(mod, curried_args): + new_sol = spend.solution.replace(rrfrrfrrf=signing_responses[0].signature) + signed_spends.append(Spend(spend.coin, spend.puzzle, new_sol)) + else: + signed_spends.append(spend) + return SignedTransaction( + TransactionInfo(signed_spends), + [Signature("bls_12381_aug_scheme", G2Element().to_bytes())], + ) async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: - raise NotImplementedError("vault wallet") + root_pubkey = self.wallet_state_manager.observation_root + sk: SigningKey = self.wallet_state_manager.config["test_sk"] # Temporary access to private key + sk_lookup: Dict[int, SigningKey] = {root_pubkey.get_fingerprint(): sk} + responses: List[SigningResponse] = [] + + # We don't need to expand path and sum hints since vault signer always uses the same keys + # so just sign the targets + for target in signing_instructions.targets: + fingerprint: int = int.from_bytes(target.fingerprint, "big") + if fingerprint not in sk_lookup: + raise ValueError(f"Pubkey {fingerprint} not found") + responses.append( + SigningResponse( + sk_lookup[fingerprint].sign_deterministic(target.message), + target.hook, + ) + ) + + return responses async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: - raise NotImplementedError("vault wallet") + return None async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: - raise NotImplementedError("vault wallet") + return None def make_solution( self, diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 0916b31fee46..2307d10f78ac 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -7,9 +7,12 @@ from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG -# from chia.rpc.wallet_request_types import GatherSigningInfo +from chia.rpc.wallet_request_types import GatherSigningInfo +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment +from chia.wallet.signer_protocol import Spend from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.vault.vault_root import VaultRoot @@ -23,6 +26,9 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b seed = b"chia_secp" SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) SECP_PK = SECP_SK.verifying_key.to_string("compressed") + + # Temporary hack so execute_signing_instructions can access the key + env.wallet_state_manager.config["test_sk"] = SECP_SK client = env.rpc_client fingerprint = (await client.get_public_keys())[0] bls_pk = None @@ -162,11 +168,40 @@ async def test_vault_creation( ] amount = uint64(1000000) fee = uint64(100) + balance_delta = 1011000100 unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( amount, recipient_ph, DEFAULT_TX_CONFIG, primaries=primaries, fee=fee ) assert len(unsigned_txs) == 1 - # gather_sig_info = GatherSigningInfo(unsigned_txs[0].spend_bundle.coin_spends) - # await env.rpc_client.gather_signing_info(gather_sig_info) + # Farm a block so the vault balance includes farmed coins from the test setup in pre-block update. + # Do this after generating the tx so we can be sure to spend the right funding coins + await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) + + assert unsigned_txs[0].spend_bundle is not None + spends = [Spend.from_coin_spend(spend) for spend in unsigned_txs[0].spend_bundle.coin_spends] + signing_info = await env.rpc_client.gather_signing_info(GatherSigningInfo(spends)) + + signing_responses = await wallet.execute_signing_instructions(signing_info.signing_instructions) + + signed_response = await wallet.apply_signatures(spends, signing_responses) + await env.wallet_state_manager.submit_transactions([signed_response]) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -balance_delta, + "set_remainder": True, + } + }, + ), + ], + ) From 4094c4c5ad4b8b6e3c387c2e415047e3ce8f94a5 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 26 Feb 2024 09:52:55 -0800 Subject: [PATCH 136/274] Consolidate add_private_key and add_public_key --- chia/cmds/keys_funcs.py | 2 +- chia/cmds/sim_funcs.py | 2 +- chia/daemon/keychain_proxy.py | 65 ++++++++-------- chia/daemon/keychain_server.py | 55 ++++--------- chia/rpc/wallet_rpc_api.py | 2 +- chia/simulator/block_tools.py | 6 +- chia/simulator/setup_services.py | 2 +- chia/simulator/simulator_test_tools.py | 2 +- chia/util/keychain.py | 77 ++++++++++--------- tests/core/cmds/test_keys.py | 8 +- tests/core/daemon/test_daemon.py | 24 +++--- tests/core/daemon/test_keychain_proxy.py | 12 +-- tests/core/data_layer/test_data_rpc.py | 2 +- tests/core/util/test_keychain.py | 40 +++++----- .../farmer_harvester/test_farmer_harvester.py | 4 +- tests/wallet/test_wallet_node.py | 18 ++--- 16 files changed, 149 insertions(+), 172 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 53071d9024e3..ea82fa38a1f2 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -78,7 +78,7 @@ def add_private_key_seed(mnemonic: str, label: Optional[str]) -> None: """ unlock_keyring() try: - sk = Keychain().add_private_key(mnemonic, label) + sk = Keychain().add_key(mnemonic, label) fingerprint = sk.get_g1().get_fingerprint() print(f"Added private key with public key fingerprint {fingerprint}") diff --git a/chia/cmds/sim_funcs.py b/chia/cmds/sim_funcs.py index c9207fc2975d..63967f97b1e2 100644 --- a/chia/cmds/sim_funcs.py +++ b/chia/cmds/sim_funcs.py @@ -158,7 +158,7 @@ def generate_and_return_fingerprint(mnemonic: Optional[str] = None) -> int: print("Generating private key") mnemonic = generate_mnemonic() try: - sk = Keychain().add_private_key(mnemonic, None) + sk = Keychain().add_key(mnemonic, None) fingerprint: int = sk.get_g1().get_fingerprint() except KeychainFingerprintExists as e: fingerprint = e.fingerprint diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 595f03495db7..66dce13f635f 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -5,7 +5,7 @@ import ssl import traceback from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Literal, Optional, Tuple, Union, overload from aiohttp import ClientConnectorError, ClientSession from chia_rs import AugSchemeMPL, G1Element, PrivateKey @@ -170,47 +170,48 @@ def handle_error(self, response: WsRpcMessage) -> None: raise Exception(f"{err}") raise Exception(f"{error}") - async def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> PrivateKey: - """ - Forwards to Keychain.add_private_key() - """ - key: PrivateKey - if self.use_local_keychain(): - key = self.keychain.add_private_key(mnemonic, label) - else: - response, success = await self.get_response_for_request( - "add_private_key", {"mnemonic": mnemonic, "label": label} - ) - if success: - seed = mnemonic_to_seed(mnemonic) - key = AugSchemeMPL.key_gen(seed) - else: - error = response["data"].get("error", None) - if error == KEYCHAIN_ERR_KEYERROR: - error_details = response["data"].get("error_details", {}) - word = error_details.get("word", "") - raise KeyError(word) - else: - self.handle_error(response) + @overload + async def add_key(self, mnemonic_or_pk: str) -> PrivateKey: + ... - return key + @overload + async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: + ... + + @overload + async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: + ... - async def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: + @overload + async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: + ... + + @overload + async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: bool) -> Union[PrivateKey, G1Element]: + ... + + async def add_key( + self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True + ) -> Union[PrivateKey, G1Element]: """ - Forwards to Keychain.add_public_key() + Forwards to Keychain.add_key() """ - key: G1Element + key: Union[PrivateKey, G1Element] if self.use_local_keychain(): - key = self.keychain.add_public_key(pubkey, label) # pragma: no cover + key = self.keychain.add_key(mnemonic_or_pk, label, private) else: response, success = await self.get_response_for_request( - "add_public_key", {"public_key": pubkey, "label": label} + "add_key", {"mnemonic_or_pk": mnemonic_or_pk, "label": label, "private": private} ) if success: - key = G1Element.from_bytes(hexstr_to_bytes(pubkey)) + if private: + seed = mnemonic_to_seed(mnemonic_or_pk) + key = AugSchemeMPL.key_gen(seed) + else: + key = G1Element.from_bytes(hexstr_to_bytes(mnemonic_or_pk)) else: error = response["data"].get("error", None) - if error == KEYCHAIN_ERR_KEYERROR: # pragma: no cover + if error == KEYCHAIN_ERR_KEYERROR: error_details = response["data"].get("error_details", {}) word = error_details.get("word", "") raise KeyError(word) @@ -331,7 +332,7 @@ async def get_first_private_key(self) -> Optional[PrivateKey]: async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: """ - Locates and returns a private key matching the provided fingerprint + Locates and returns a public key matching the provided fingerprint """ key: Optional[PrivateKey] = None if self.use_local_keychain(): diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index b20f8b3c65dd..f183cfee552c 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -16,7 +16,7 @@ # Commands that are handled by the KeychainServer keychain_commands = [ "add_private_key", - "add_public_key", + "add_key", "check_keys", "delete_all_keys", "delete_key_by_fingerprint", @@ -175,9 +175,9 @@ def get_keychain_for_request(self, request: Dict[str, Any]) -> Keychain: async def handle_command(self, command: str, data: Dict[str, Any]) -> Dict[str, Any]: try: if command == "add_private_key": - return await self.add_private_key(data) - elif command == "add_public_key": - return await self.add_public_key(data) + return await self.add_key({"mnemonic_or_pk": data["mnemonic"], "label": data["label"], "private": True}) + elif command == "add_key": + return await self.add_key(data) elif command == "check_keys": return await self.check_keys(data) elif command == "delete_all_keys": @@ -209,22 +209,23 @@ async def handle_command(self, command: str, data: Dict[str, Any]) -> Dict[str, log.exception(e) return {"success": False, "error": str(e), "command": command} - async def add_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]: + async def add_key(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.get_keychain_for_request(request).is_keyring_locked(): return {"success": False, "error": KEYCHAIN_ERR_LOCKED} - mnemonic = request.get("mnemonic", None) + mnemonic_or_pk = request.get("mnemonic_or_pk", None) label = request.get("label", None) + private = request.get("private", True) - if mnemonic is None: + if mnemonic_or_pk is None: return { "success": False, "error": KEYCHAIN_ERR_MALFORMED_REQUEST, - "error_details": {"message": "missing mnemonic"}, + "error_details": {"message": "missing key_information"}, } try: - sk = self.get_keychain_for_request(request).add_private_key(mnemonic, label) + key = self.get_keychain_for_request(request).add_key(mnemonic_or_pk, label, private) except KeyError as e: return { "success": False, @@ -238,38 +239,12 @@ async def add_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]: "error": str(e), } - return {"success": True, "fingerprint": sk.get_g1().get_fingerprint()} - - async def add_public_key(self, request: Dict[str, Any]) -> Dict[str, Any]: - if self.get_keychain_for_request(request).is_keyring_locked(): # pragma: no cover - return {"success": False, "error": KEYCHAIN_ERR_LOCKED} - - public_key = request.get("public_key", None) - label = request.get("label", None) - - if public_key is None: # pragma: no cover - return { - "success": False, - "error": KEYCHAIN_ERR_MALFORMED_REQUEST, - "error_details": {"message": "missing public_key"}, - } - - try: - pk = self.get_keychain_for_request(request).add_public_key(public_key, label) - except KeyError as e: # pragma: no cover - return { - "success": False, - "error": KEYCHAIN_ERR_KEYERROR, - "error_details": {"message": f"The word '{e.args[0]}' is incorrect.'", "word": e.args[0]}, - } - except ValueError as e: # pragma: no cover - log.exception(e) - return { - "success": False, - "error": str(e), - } + if isinstance(key, PrivateKey): + fingerprint = key.get_g1().get_fingerprint() + else: + fingerprint = key.get_fingerprint() - return {"success": True, "fingerprint": pk.get_fingerprint()} + return {"success": True, "fingerprint": fingerprint} async def check_keys(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.get_keychain_for_request(request).is_keyring_locked(): diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index ee2d86f70108..13d37603b6b6 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -446,7 +446,7 @@ async def add_key(self, request: Dict[str, Any]) -> EndpointResult: # Adding a key from 24 word mnemonic mnemonic = request["mnemonic"] try: - sk = await self.service.keychain_proxy.add_private_key(" ".join(mnemonic)) + sk = await self.service.keychain_proxy.add_key(" ".join(mnemonic)) except KeyError as e: return { "success": False, diff --git a/chia/simulator/block_tools.py b/chia/simulator/block_tools.py index 0fd2103cb0c3..0ae4397f43f1 100644 --- a/chia/simulator/block_tools.py +++ b/chia/simulator/block_tools.py @@ -327,10 +327,8 @@ async def setup_keys(self, fingerprint: Optional[int] = None, reward_ph: Optiona await keychain_proxy.delete_all_keys() self.farmer_master_sk_entropy = std_hash(b"block_tools farmer key") # both entropies are only used here self.pool_master_sk_entropy = std_hash(b"block_tools pool key") - self.farmer_master_sk = await keychain_proxy.add_private_key( - bytes_to_mnemonic(self.farmer_master_sk_entropy) - ) - self.pool_master_sk = await keychain_proxy.add_private_key( + self.farmer_master_sk = await keychain_proxy.add_key(bytes_to_mnemonic(self.farmer_master_sk_entropy)) + self.pool_master_sk = await keychain_proxy.add_key( bytes_to_mnemonic(self.pool_master_sk_entropy), ) else: diff --git a/chia/simulator/setup_services.py b/chia/simulator/setup_services.py index 3cf9708a90cd..5f305446d0ac 100644 --- a/chia/simulator/setup_services.py +++ b/chia/simulator/setup_services.py @@ -254,7 +254,7 @@ async def setup_wallet_node( entropy = bytes32.secret() if key_seed is None: key_seed = entropy - keychain.add_private_key(bytes_to_mnemonic(key_seed)) + keychain.add_key(bytes_to_mnemonic(key_seed)) first_pk = keychain.get_first_public_key() assert first_pk is not None db_path_key_suffix = str(first_pk.get_fingerprint()) diff --git a/chia/simulator/simulator_test_tools.py b/chia/simulator/simulator_test_tools.py index adbe8537099b..58430a14ddd4 100644 --- a/chia/simulator/simulator_test_tools.py +++ b/chia/simulator/simulator_test_tools.py @@ -42,7 +42,7 @@ def mnemonic_fingerprint(keychain: Keychain) -> Tuple[str, int]: ) # add key to keychain try: - sk = keychain.add_private_key(mnemonic) + sk = keychain.add_key(mnemonic) except KeychainFingerprintExists: pass fingerprint = sk.get_g1().get_fingerprint() diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 41336b9aa827..8496d3bd03e9 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from hashlib import pbkdf2_hmac from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Literal, Optional, Tuple, Union, overload import pkg_resources from bitstring import BitArray # pyright: reportMissingImports=false @@ -332,47 +332,50 @@ def _get_free_private_key_index(self) -> int: except KeychainUserNotFound: return index - def add_private_key(self, mnemonic: str, label: Optional[str] = None) -> PrivateKey: - """ - Adds a private key to the keychain, with the given entropy and passphrase. The - keychain itself will store the public key, and the entropy bytes, - but not the passphrase. - """ - seed = mnemonic_to_seed(mnemonic) - entropy = bytes_from_mnemonic(mnemonic) - index = self._get_free_private_key_index() - key = AugSchemeMPL.key_gen(seed) - fingerprint = key.get_g1().get_fingerprint() + @overload + def add_key(self, mnemonic_or_pk: str) -> PrivateKey: + ... - if fingerprint in [pk.get_fingerprint() for pk in self.get_all_public_keys()]: - # Prevents duplicate add - raise KeychainFingerprintExists(fingerprint) + @overload + def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: + ... - # Try to set the label first, it may fail if the label is invalid or already exists. - # This can probably just be moved into `FileKeyring.set_passphrase` after the legacy keyring stuff was dropped. - if label is not None: - self.keyring_wrapper.set_label(fingerprint, label) + @overload + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: + ... - try: - self.keyring_wrapper.set_passphrase( - self.service, - get_private_key_user(self.user, index), - bytes(key.get_g1()).hex() + entropy.hex(), - ) - except Exception: - if label is not None: - self.keyring_wrapper.delete_label(fingerprint) - raise + @overload + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: + ... - return key + @overload + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: bool) -> Union[PrivateKey, G1Element]: + ... - def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: + def add_key( + self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True + ) -> Union[PrivateKey, G1Element]: """ - Adds a public key to the keychain. + Adds a key to the keychain. The keychain itself will store the public key, and the entropy bytes (if given), + but not the passphrase. """ - key = G1Element.from_bytes(hexstr_to_bytes(pubkey)) - index = self._get_free_private_key_index() - fingerprint = key.get_fingerprint() + key: Union[PrivateKey, G1Element] + if private: + seed = mnemonic_to_seed(mnemonic_or_pk) + entropy = bytes_from_mnemonic(mnemonic_or_pk) + index = self._get_free_private_key_index() + key = AugSchemeMPL.key_gen(seed) + assert isinstance(key, PrivateKey) + pk = key.get_g1() + key_data = bytes(pk).hex() + entropy.hex() + fingerprint = pk.get_fingerprint() + else: + index = self._get_free_private_key_index() + pk_bytes = hexstr_to_bytes(mnemonic_or_pk) + key = G1Element.from_bytes(pk_bytes) + assert isinstance(key, G1Element) + key_data = pk_bytes.hex() + fingerprint = key.get_fingerprint() if fingerprint in [pk.get_fingerprint() for pk in self.get_all_public_keys()]: # Prevents duplicate add @@ -387,9 +390,9 @@ def add_public_key(self, pubkey: str, label: Optional[str] = None) -> G1Element: self.keyring_wrapper.set_passphrase( self.service, get_private_key_user(self.user, index), - bytes(key).hex(), + key_data, ) - except Exception: # pragma: no cover + except Exception: if label is not None: self.keyring_wrapper.delete_label(fingerprint) raise diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index a5911f3986a9..373fd924b004 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -27,7 +27,7 @@ @pytest.fixture(scope="function") def keyring_with_one_key(empty_keyring): keychain = empty_keyring - keychain.add_private_key(TEST_MNEMONIC_SEED) + keychain.add_key(TEST_MNEMONIC_SEED) return keychain @@ -287,7 +287,7 @@ def test_show_labels(self, empty_keyring, tmp_path): # Add 10 keys to the keychain, give every other a label keys = [KeyData.generate(f"key_{i}" if i % 2 == 0 else None) for i in range(10)] for key in keys: - keychain.add_private_key(key.mnemonic_str(), key.label) + keychain.add_key(key.mnemonic_str(), key.label) # Make sure all 10 keys are printed correct result = runner.invoke(cli, [*base_params, *cmd_params], catch_exceptions=False) assert result.exit_code == 0 @@ -336,7 +336,7 @@ def test_show_fingerprint(self, keyring_with_one_key, tmp_path): keychain = keyring_with_one_key # add a key - keychain.add_private_key(generate_mnemonic()) + keychain.add_key(generate_mnemonic()) assert len(keychain.get_all_private_keys()) == 2 keys_root_path = keychain.keyring_wrapper.keys_root_path @@ -584,7 +584,7 @@ def test_delete_all(self, empty_keyring): for i in range(5): mnemonic: str = generate_mnemonic() - keychain.add_private_key(mnemonic) + keychain.add_key(mnemonic) assert len(keychain.get_all_private_keys()) == 5 diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index 836285ea74db..93a780e40964 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -404,11 +404,11 @@ def mock_daemon_with_config_and_keys(get_keychain_for_function, root_path_popula keychain = Keychain() # populate the keychain with some test keys - keychain.add_private_key(test_key_data.mnemonic_str()) - keychain.add_private_key(test_key_data_2.mnemonic_str()) + keychain.add_key(test_key_data.mnemonic_str()) + keychain.add_key(test_key_data_2.mnemonic_str()) # Throw in an unused pubkey-only entry - keychain.add_public_key(bytes(G1Element()).hex()) + keychain.add_key(bytes(G1Element()).hex(), private=False) # Mock daemon server with net_config set for mainnet return Daemon(services={}, connections={}, net_config=config) @@ -419,8 +419,8 @@ async def daemon_client_with_config_and_keys(get_keychain_for_function, get_daem keychain = Keychain() # populate the keychain with some test keys - keychain.add_private_key(test_key_data.mnemonic_str()) - keychain.add_private_key(test_key_data_2.mnemonic_str()) + keychain.add_key(test_key_data.mnemonic_str()) + keychain.add_key(test_key_data_2.mnemonic_str()) daemon = get_daemon client = await connect_to_daemon( @@ -1061,7 +1061,7 @@ async def test_get_key(daemon_connection_and_temp_keychain): await ws.send_str(create_payload("get_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon")) assert_response(await ws.receive(), fingerprint_not_found_response_data(test_key_data.fingerprint)) - keychain.add_private_key(test_key_data.mnemonic_str()) + keychain.add_key(test_key_data.mnemonic_str()) # without `include_secrets` await ws.send_str(create_payload("get_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon")) @@ -1099,7 +1099,7 @@ async def test_get_keys(daemon_connection_and_temp_keychain): keys = [KeyData.generate() for _ in range(5)] keys_added = [] for key_data in keys: - keychain.add_private_key(key_data.mnemonic_str()) + keychain.add_key(key_data.mnemonic_str()) keys_added.append(key_data) get_keys_response_data_without_secrets = get_keys_response_data( @@ -1127,7 +1127,7 @@ async def test_get_public_key(daemon_connection_and_temp_keychain): await ws.send_str(create_payload("get_public_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon")) assert_response(await ws.receive(), fingerprint_not_found_response_data(test_key_data.fingerprint)) - keychain.add_private_key(test_key_data.mnemonic_str()) + keychain.add_key(test_key_data.mnemonic_str()) await ws.send_str(create_payload("get_public_key", {"fingerprint": test_key_data.fingerprint}, "test", "daemon")) response = await ws.receive() @@ -1153,7 +1153,7 @@ async def test_get_public_keys(daemon_connection_and_temp_keychain): keys = [KeyData.generate() for _ in range(5)] keys_added = [] for key_data in keys: - keychain.add_private_key(key_data.mnemonic_str()) + keychain.add_key(key_data.mnemonic_str()) keys_added.append(key_data) get_public_keys_response = get_public_keys_response_data(keys_added) @@ -1173,7 +1173,7 @@ async def test_get_public_keys(daemon_connection_and_temp_keychain): @pytest.mark.anyio async def test_key_renaming(daemon_connection_and_temp_keychain): ws, keychain = daemon_connection_and_temp_keychain - keychain.add_private_key(test_key_data.mnemonic_str()) + keychain.add_key(test_key_data.mnemonic_str()) # Rename the key three times for i in range(3): key_data = replace(test_key_data_no_secrets, label=f"renaming_{i}") @@ -1198,7 +1198,7 @@ async def test_key_renaming(daemon_connection_and_temp_keychain): async def test_key_label_deletion(daemon_connection_and_temp_keychain): ws, keychain = daemon_connection_and_temp_keychain - keychain.add_private_key(test_key_data.mnemonic_str(), "key_0") + keychain.add_key(test_key_data.mnemonic_str(), "key_0") assert keychain.get_key(test_key_data.fingerprint).label == "key_0" await ws.send_str(create_payload("delete_label", {"fingerprint": test_key_data.fingerprint}, "test", "daemon")) assert_response(await ws.receive(), success_response_data) @@ -1272,7 +1272,7 @@ async def test_key_label_methods( daemon_connection_and_temp_keychain, method: str, parameter: Dict[str, Any], response_data_dict: Dict[str, Any] ) -> None: ws, keychain = daemon_connection_and_temp_keychain - keychain.add_private_key(test_key_data.mnemonic_str(), "key_0") + keychain.add_key(test_key_data.mnemonic_str(), "key_0") await ws.send_str(create_payload(method, parameter, "test", "daemon")) assert_response(await ws.receive(), response_data_dict) diff --git a/tests/core/daemon/test_keychain_proxy.py b/tests/core/daemon/test_keychain_proxy.py index fb29c1b72780..e126f26a76f1 100644 --- a/tests/core/daemon/test_keychain_proxy.py +++ b/tests/core/daemon/test_keychain_proxy.py @@ -31,15 +31,15 @@ async def keychain_proxy(get_b_tools: BlockTools, request: Any) -> AsyncGenerato @pytest.fixture(scope="function") async def keychain_proxy_with_keys(keychain_proxy: KeychainProxy) -> KeychainProxy: - await keychain_proxy.add_private_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) - await keychain_proxy.add_private_key(TEST_KEY_2.mnemonic_str(), TEST_KEY_2.label) + await keychain_proxy.add_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) + await keychain_proxy.add_key(TEST_KEY_2.mnemonic_str(), TEST_KEY_2.label) return keychain_proxy @pytest.mark.anyio async def test_add_private_key(keychain_proxy: KeychainProxy) -> None: keychain = keychain_proxy - await keychain.add_private_key(TEST_KEY_3.mnemonic_str(), TEST_KEY_3.label) + await keychain.add_key(TEST_KEY_3.mnemonic_str(), TEST_KEY_3.label) key = await keychain.get_key(TEST_KEY_3.fingerprint, include_secrets=True) assert key == TEST_KEY_3 @@ -47,9 +47,9 @@ async def test_add_private_key(keychain_proxy: KeychainProxy) -> None: @pytest.mark.anyio async def test_add_public_key(keychain_proxy: KeychainProxy) -> None: keychain = keychain_proxy - await keychain.add_public_key(bytes(TEST_KEY_3.public_key).hex(), TEST_KEY_3.label) + await keychain.add_key(bytes(TEST_KEY_3.public_key).hex(), TEST_KEY_3.label, private=False) with pytest.raises(Exception, match="already exists"): - await keychain.add_public_key(bytes(TEST_KEY_3.public_key).hex(), "") + await keychain.add_key(bytes(TEST_KEY_3.public_key).hex(), "", private=False) key = await keychain.get_key(TEST_KEY_3.fingerprint, include_secrets=False) assert key is not None assert key.public_key == TEST_KEY_3.public_key @@ -81,7 +81,7 @@ async def test_get_key_for_fingerprint(keychain_proxy: KeychainProxy) -> None: keychain = keychain_proxy with pytest.raises(KeychainIsEmpty): await keychain.get_public_key_for_fingerprint(None) - await keychain_proxy.add_private_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) + await keychain_proxy.add_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) assert await keychain.get_public_key_for_fingerprint(TEST_KEY_1.fingerprint) == TEST_KEY_1.public_key assert await keychain.get_public_key_for_fingerprint(None) == TEST_KEY_1.public_key with pytest.raises(KeychainKeyNotFound): diff --git a/tests/core/data_layer/test_data_rpc.py b/tests/core/data_layer/test_data_rpc.py index 59ec32f8fd90..7605d291d818 100644 --- a/tests/core/data_layer/test_data_rpc.py +++ b/tests/core/data_layer/test_data_rpc.py @@ -2243,7 +2243,7 @@ async def test_wallet_log_in_changes_active_fingerprint( mnemonic = create_mnemonic() assert wallet_rpc_api.service.local_keychain is not None - private_key = wallet_rpc_api.service.local_keychain.add_private_key(mnemonic=mnemonic) + private_key = wallet_rpc_api.service.local_keychain.add_key(mnemonic_or_pk=mnemonic) secondary_fingerprint: int = private_key.get_g1().get_fingerprint() await wallet_rpc_api.log_in(request={"fingerprint": primary_fingerprint}) diff --git a/tests/core/util/test_keychain.py b/tests/core/util/test_keychain.py index b0bbc4d61d80..c5c26c8d91ea 100644 --- a/tests/core/util/test_keychain.py +++ b/tests/core/util/test_keychain.py @@ -67,13 +67,13 @@ def test_basic_add_delete(self, empty_temp_file_keyring: TempKeyring, seeded_ran with pytest.raises(ValueError, match="'ZZZZZZ' is not in the mnemonic dictionary; may be misspelled"): bytes_from_mnemonic(" ".join(bad_mnemonic)) - kc.add_private_key(mnemonic) + kc.add_key(mnemonic) assert kc._get_free_private_key_index() == 1 assert len(kc.get_all_private_keys()) == 1 - kc.add_private_key(mnemonic_2) + kc.add_key(mnemonic_2) with pytest.raises(KeychainFingerprintExists) as e: - kc.add_private_key(mnemonic_2) + kc.add_key(mnemonic_2) assert e.value.fingerprint == fingerprint_2 assert kc._get_free_private_key_index() == 2 assert len(kc.get_all_private_keys()) == 2 @@ -96,9 +96,9 @@ def test_basic_add_delete(self, empty_temp_file_keyring: TempKeyring, seeded_ran assert kc._get_free_private_key_index() == 0 assert len(kc.get_all_private_keys()) == 0 - kc.add_private_key(bytes_to_mnemonic(bytes32.random(seeded_random))) - kc.add_private_key(bytes_to_mnemonic(bytes32.random(seeded_random))) - kc.add_private_key(bytes_to_mnemonic(bytes32.random(seeded_random))) + kc.add_key(bytes_to_mnemonic(bytes32.random(seeded_random))) + kc.add_key(bytes_to_mnemonic(bytes32.random(seeded_random))) + kc.add_key(bytes_to_mnemonic(bytes32.random(seeded_random))) assert len(kc.get_all_public_keys()) == 3 @@ -106,7 +106,7 @@ def test_basic_add_delete(self, empty_temp_file_keyring: TempKeyring, seeded_ran assert kc.get_first_public_key() is not None kc.delete_all_keys() - kc.add_private_key(bytes_to_mnemonic(bytes32.random(seeded_random))) + kc.add_key(bytes_to_mnemonic(bytes32.random(seeded_random))) assert kc.get_first_public_key() is not None def test_add_private_key_label(self, empty_temp_file_keyring: TempKeyring): @@ -116,26 +116,26 @@ def test_add_private_key_label(self, empty_temp_file_keyring: TempKeyring): key_data_1 = KeyData.generate(label="key_1") key_data_2 = KeyData.generate(label=None) - keychain.add_private_key(mnemonic=key_data_0.mnemonic_str(), label=key_data_0.label) + keychain.add_key(mnemonic_or_pk=key_data_0.mnemonic_str(), label=key_data_0.label) assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True) # Try to add a new key with an existing label should raise with pytest.raises(KeychainLabelExists) as e: - keychain.add_private_key(mnemonic=key_data_1.mnemonic_str(), label=key_data_0.label) + keychain.add_key(mnemonic_or_pk=key_data_1.mnemonic_str(), label=key_data_0.label) assert e.value.fingerprint == key_data_0.fingerprint assert e.value.label == key_data_0.label # Adding the same key with a valid label should work fine - keychain.add_private_key(mnemonic=key_data_1.mnemonic_str(), label=key_data_1.label) + keychain.add_key(mnemonic_or_pk=key_data_1.mnemonic_str(), label=key_data_1.label) assert key_data_1 == keychain.get_key(key_data_1.fingerprint, include_secrets=True) # Trying to add an existing key should not have an impact on the existing label with pytest.raises(KeychainFingerprintExists): - keychain.add_private_key(mnemonic=key_data_0.mnemonic_str(), label="other label") + keychain.add_key(mnemonic_or_pk=key_data_0.mnemonic_str(), label="other label") assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True) # Adding a key with no label should not assign any label - keychain.add_private_key(mnemonic=key_data_2.mnemonic_str(), label=key_data_2.label) + keychain.add_key(mnemonic_or_pk=key_data_2.mnemonic_str(), label=key_data_2.label) assert key_data_2 == keychain.get_key(key_data_2.fingerprint, include_secrets=True) # All added keys should still be valid with their label @@ -149,7 +149,7 @@ def test_bip39_eip2333_test_vector(self, empty_temp_file_keyring: TempKeyring): mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" print("entropy to seed:", mnemonic_to_seed(mnemonic).hex()) - master_sk = kc.add_private_key(mnemonic) + master_sk = kc.add_key(mnemonic) tv_master_int = 8075452428075949470768183878078858156044736575259233735633523546099624838313 tv_child_int = 18507161868329770878190303689452715596635858303241878571348190917018711023613 assert master_sk == PrivateKey.from_bytes(tv_master_int.to_bytes(32, "big")) @@ -310,7 +310,7 @@ async def test_get_key(include_secrets: bool, get_temp_keyring: Keychain): with pytest.raises(KeychainFingerprintNotFound): keychain.get_key(expected_keys[-1].fingerprint, include_secrets) # Add it and validate all keys - keychain.add_private_key(mnemonic_str) + keychain.add_key(mnemonic_str) assert all(keychain.get_key(key_data.fingerprint, include_secrets) == key_data for key_data in expected_keys) # Remove 10 keys and validate the result `get_key` for each of them after each removal while len(expected_keys) > 0: @@ -336,7 +336,7 @@ async def test_get_keys(include_secrets: bool, get_temp_keyring: Keychain): if not include_secrets: key_data = replace(key_data, secrets=None) expected_keys.append(key_data) - keychain.add_private_key(mnemonic_str) + keychain.add_key(mnemonic_str) assert keychain.get_keys(include_secrets) == expected_keys # Remove all 10 keys and validate the result of `get_keys` after each removal while len(expected_keys) > 0: @@ -352,7 +352,7 @@ async def test_set_label(get_temp_keyring: Keychain) -> None: keychain: Keychain = get_temp_keyring # Generate a key and add it without label key_data_0 = KeyData.generate(label=None) - keychain.add_private_key(mnemonic=key_data_0.mnemonic_str(), label=None) + keychain.add_key(mnemonic_or_pk=key_data_0.mnemonic_str(), label=None) assert key_data_0 == keychain.get_key(key_data_0.fingerprint, include_secrets=True) # Set a label and validate it key_data_0 = replace(key_data_0, label="key_0") @@ -365,7 +365,7 @@ async def test_set_label(get_temp_keyring: Keychain) -> None: # Add a second key key_data_1 = KeyData.generate(label="key_1") assert key_data_1.label is not None - keychain.add_private_key(key_data_1.mnemonic_str()) + keychain.add_key(key_data_1.mnemonic_str()) # Try to set the already existing label for the second key with pytest.raises(KeychainLabelExists) as e: keychain.set_label(fingerprint=key_data_1.fingerprint, label=key_data_0.label) @@ -394,7 +394,7 @@ async def test_set_label(get_temp_keyring: Keychain) -> None: async def test_set_label_invalid_labels(label: str, message: str, get_temp_keyring: Keychain) -> None: keychain: Keychain = get_temp_keyring key_data = KeyData.generate() - keychain.add_private_key(key_data.mnemonic_str()) + keychain.add_key(key_data.mnemonic_str()) with pytest.raises(KeychainLabelInvalid, match=message) as e: keychain.set_label(key_data.fingerprint, label) assert e.value.label == label @@ -418,7 +418,7 @@ def assert_delete_raises(): assert_delete_raises() for key in [key_data_0, key_data_1]: - keychain.add_private_key(mnemonic=key.mnemonic_str(), label=key.label) + keychain.add_key(mnemonic_or_pk=key.mnemonic_str(), label=key.label) assert key == keychain.get_key(key.fingerprint, include_secrets=True) # Delete the label of the first key, validate it was removed and make sure the other key retains its label keychain.delete_label(key_data_0.fingerprint) @@ -447,7 +447,7 @@ async def test_delete_drops_labels(get_temp_keyring: Keychain, delete_all: bool) labels = [f"key_{i}" for i in range(5)] keys = [KeyData.generate(label=label) for label in labels] for key_data in keys: - keychain.add_private_key(mnemonic=key_data.mnemonic_str(), label=key_data.label) + keychain.add_key(mnemonic_or_pk=key_data.mnemonic_str(), label=key_data.label) assert key_data == keychain.get_key(key_data.fingerprint, include_secrets=True) assert key_data.label is not None assert keychain.keyring_wrapper.get_label(key_data.fingerprint) == key_data.label diff --git a/tests/farmer_harvester/test_farmer_harvester.py b/tests/farmer_harvester/test_farmer_harvester.py index ceecd259ebe3..adfcc8c6981a 100644 --- a/tests/farmer_harvester/test_farmer_harvester.py +++ b/tests/farmer_harvester/test_farmer_harvester.py @@ -60,7 +60,7 @@ async def test_start_with_empty_keychain( assert not farmer.started # Add a key to the keychain, this should lead to the start task passing # `setup_keys` and set `Farmer.initialized` - bt.local_keychain.add_private_key(generate_mnemonic()) + bt.local_keychain.add_key(generate_mnemonic()) await time_out_assert(5, farmer_is_started, True, farmer) assert not farmer.started @@ -127,7 +127,7 @@ async def handshake_done() -> bool: async with farmer_service.manage(): await time_out_assert(5, handshake_task_active, True) assert not await handshake_done() - bt.local_keychain.add_private_key(generate_mnemonic()) + bt.local_keychain.add_key(generate_mnemonic()) await time_out_assert(5, farmer_is_started, True, farmer) await time_out_assert(5, handshake_task_active, False) await time_out_assert(5, handshake_done, True) diff --git a/tests/wallet/test_wallet_node.py b/tests/wallet/test_wallet_node.py index 212617f41039..cbb6369311d9 100644 --- a/tests/wallet/test_wallet_node.py +++ b/tests/wallet/test_wallet_node.py @@ -39,7 +39,7 @@ async def test_get_private_key(root_path_populated_with_config: Path, get_temp_k keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) - sk = keychain.add_private_key(generate_mnemonic()) + sk = keychain.add_key(generate_mnemonic()) fingerprint = sk.get_g1().get_fingerprint() key = await node.get_private_key(fingerprint) @@ -54,12 +54,12 @@ async def test_get_private_key_default_key(root_path_populated_with_config: Path keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) - sk = keychain.add_private_key(generate_mnemonic()) + sk = keychain.add_key(generate_mnemonic()) fingerprint = sk.get_g1().get_fingerprint() # Add a couple more keys - keychain.add_private_key(generate_mnemonic()) - keychain.add_private_key(generate_mnemonic()) + keychain.add_key(generate_mnemonic()) + keychain.add_key(generate_mnemonic()) # When no fingerprint is provided, we should get the default (first) key key = await node.get_private_key(None) @@ -92,7 +92,7 @@ async def test_get_private_key_missing_key_use_default( keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) - sk = keychain.add_private_key(generate_mnemonic()) + sk = keychain.add_key(generate_mnemonic()) fingerprint = sk.get_g1().get_fingerprint() # Stupid sanity check that the fingerprint we're going to use isn't actually in the keychain @@ -110,7 +110,7 @@ def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychai keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) - sk = keychain.add_private_key(generate_mnemonic()) + sk = keychain.add_key(generate_mnemonic()) fingerprint = sk.get_g1().get_fingerprint() node.log_in(sk) @@ -136,7 +136,7 @@ def patched_update_last_used_fingerprint(self: Self) -> None: keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) - sk = keychain.add_private_key(generate_mnemonic()) + sk = keychain.add_key(generate_mnemonic()) fingerprint = sk.get_g1().get_fingerprint() # Expect log_in to succeed, even though we can't write the last used fingerprint @@ -153,7 +153,7 @@ def test_log_out(root_path_populated_with_config: Path, get_temp_keyring: Keycha keychain = get_temp_keyring config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) - sk = keychain.add_private_key(generate_mnemonic()) + sk = keychain.add_key(generate_mnemonic()) fingerprint = sk.get_g1().get_fingerprint() node.log_in(sk) @@ -436,7 +436,7 @@ async def restart_with_fingerprint(fingerprint: Optional[int]) -> None: # Load another key without funds, make sure the balance is empty. other_key = KeyData.generate() assert wallet_node.local_keychain is not None - wallet_node.local_keychain.add_private_key(other_key.mnemonic_str()) + wallet_node.local_keychain.add_key(other_key.mnemonic_str()) await restart_with_fingerprint(other_key.fingerprint) assert await wallet_node.get_balance(wallet_id) == Balance() # Load the initial fingerprint again and make sure the balance is still what we generated earlier From dd006b9d4a5ff9a1fe0a7362045cfc23ea430c26 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 26 Feb 2024 10:32:36 -0800 Subject: [PATCH 137/274] Consolidate get_key_for_fingerprint and get_public_key_for_fingerprint --- chia/daemon/keychain_proxy.py | 82 ++++++++++-------------- chia/daemon/keychain_server.py | 62 ++++++------------ tests/core/daemon/test_keychain_proxy.py | 14 ++-- 3 files changed, 59 insertions(+), 99 deletions(-) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 66dce13f635f..98e28476df18 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -330,37 +330,57 @@ async def get_first_private_key(self) -> Optional[PrivateKey]: return key + @overload async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: + ... + + @overload + async def get_key_for_fingerprint(self, fingerprint: Optional[int], private: Literal[True]) -> Optional[PrivateKey]: + ... + + @overload + async def get_key_for_fingerprint(self, fingerprint: Optional[int], private: Literal[False]) -> Optional[G1Element]: + ... + + @overload + async def get_key_for_fingerprint( + self, fingerprint: Optional[int], private: bool + ) -> Optional[Union[PrivateKey, G1Element]]: + ... + + async def get_key_for_fingerprint( + self, fingerprint: Optional[int], private: bool = True + ) -> Optional[Union[PrivateKey, G1Element]]: """ - Locates and returns a public key matching the provided fingerprint + Locates and returns a private key matching the provided fingerprint """ - key: Optional[PrivateKey] = None + key: Optional[Union[PrivateKey, G1Element]] = None if self.use_local_keychain(): - private_keys = self.keychain.get_all_private_keys() - if len(private_keys) == 0: + keys = self.keychain.get_keys(include_secrets=private) + if len(keys) == 0: raise KeychainIsEmpty() else: + selected_key = keys[0] if fingerprint is not None: - for sk, _ in private_keys: - if sk.get_g1().get_fingerprint() == fingerprint: - key = sk + for key_data in keys: + if key_data.public_key.get_fingerprint() == fingerprint: + selected_key = key_data break - if key is None: + else: raise KeychainKeyNotFound(fingerprint) - else: - key = private_keys[0][0] + key = selected_key.private_key if private else selected_key.public_key else: response, success = await self.get_response_for_request( - "get_key_for_fingerprint", {"fingerprint": fingerprint} + "get_key_for_fingerprint", {"fingerprint": fingerprint, "private": private} ) if success: pk = response["data"].get("pk", None) ent = response["data"].get("entropy", None) - if pk is None or ent is None: + if pk is None or (private and ent is None): err = f"Missing pk and/or ent in {response.get('command')} response" self.log.error(f"{err}") raise KeychainMalformedResponse(f"{err}") - else: + elif private: mnemonic = bytes_to_mnemonic(bytes.fromhex(ent)) seed = mnemonic_to_seed(mnemonic) private_key = AugSchemeMPL.key_gen(seed) @@ -369,42 +389,8 @@ async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ else: err = "G1Elements don't match" self.log.error(f"{err}") - else: - self.handle_error(response) - - return key - - async def get_public_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[G1Element]: - """ - Locates and returns a private key matching the provided fingerprint - """ - key: Optional[G1Element] = None - if self.use_local_keychain(): - public_keys = self.keychain.get_all_public_keys() - if len(public_keys) == 0: - raise KeychainIsEmpty() - else: - if fingerprint is not None: - for pk in public_keys: - if pk.get_fingerprint() == fingerprint: - key = pk - break - if key is None: - raise KeychainKeyNotFound(fingerprint) - else: - key = public_keys[0] - else: - response, success = await self.get_response_for_request( - "get_public_key_for_fingerprint", {"fingerprint": fingerprint} - ) - if success: - pk_str = response["data"].get("pk", None) - if pk_str is None: # pragma: no cover - err = f"Missing pk in {response.get('command')} response" - self.log.error(f"{err}") - raise KeychainMalformedResponse(f"{err}") else: - key = G1Element.from_bytes(bytes.fromhex(pk_str)) + key = G1Element.from_bytes(bytes.fromhex(pk)) else: self.handle_error(response) diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index f183cfee552c..fa279be58d2d 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -3,9 +3,9 @@ import logging from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Dict, List, Optional, Type +from typing import Any, Dict, List, Type -from chia_rs import G1Element, PrivateKey +from chia_rs import PrivateKey from chia.cmds.init_funcs import check_keys from chia.util.errors import KeychainException, KeychainFingerprintNotFound @@ -23,7 +23,6 @@ "get_all_private_keys", "get_first_private_key", "get_key_for_fingerprint", - "get_public_key_for_fingerprint", "get_key", "get_keys", "get_public_key", @@ -190,8 +189,6 @@ async def handle_command(self, command: str, data: Dict[str, Any]) -> Dict[str, return await self.get_first_private_key(data) elif command == "get_key_for_fingerprint": return await self.get_key_for_fingerprint(data) - elif command == "get_public_key_for_fingerprint": - return await self.get_public_key_for_fingerprint(data) elif command == "get_key": return await self.run_request(data, GetKeyRequest) elif command == "get_keys": @@ -342,48 +339,25 @@ async def get_first_private_key(self, request: Dict[str, Any]) -> Dict[str, Any] return {"success": True, "private_key": key} async def get_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[str, Any]: - if self.get_keychain_for_request(request).is_keyring_locked(): + keychain = self.get_keychain_for_request(request) + if keychain.is_keyring_locked(): return {"success": False, "error": KEYCHAIN_ERR_LOCKED} - private_keys = self.get_keychain_for_request(request).get_all_private_keys() - if len(private_keys) == 0: + private = request.get("private", True) + keys = keychain.get_keys(include_secrets=private) + if len(keys) == 0: return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} - fingerprint = request.get("fingerprint", None) - private_key: Optional[PrivateKey] = None - entropy: Optional[bytes] = None - if fingerprint is not None: - for sk, entropy in private_keys: - if sk.get_g1().get_fingerprint() == fingerprint: - private_key = sk - break - else: - private_key, entropy = private_keys[0] - - if private_key is not None and entropy is not None: - return {"success": True, "pk": bytes(private_key.get_g1()).hex(), "entropy": entropy.hex()} - else: + try: + if request["fingerprint"] is None: + key_data = keys[0] + else: + key_data = keychain.get_key(request["fingerprint"], include_secrets=private) + except KeychainFingerprintNotFound: return {"success": False, "error": KEYCHAIN_ERR_KEY_NOT_FOUND} - async def get_public_key_for_fingerprint(self, request: Dict[str, Any]) -> Dict[str, Any]: - if self.get_keychain_for_request(request).is_keyring_locked(): # pragma: no cover - return {"success": False, "error": KEYCHAIN_ERR_LOCKED} - - public_keys = self.get_keychain_for_request(request).get_all_public_keys() - if len(public_keys) == 0: # pragma: no cover - return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} - - fingerprint = request.get("fingerprint", None) - public_key: Optional[G1Element] = None - if fingerprint is not None: - for pk in public_keys: - if pk.get_fingerprint() == fingerprint: - public_key = pk - break - else: - public_key = public_keys[0] - - if public_key is not None: - return {"success": True, "pk": bytes(public_key).hex()} - else: - return {"success": False, "error": KEYCHAIN_ERR_KEY_NOT_FOUND} + return { + "success": True, + "pk": bytes(key_data.public_key).hex(), + "entropy": key_data.entropy.hex() if private else None, + } diff --git a/tests/core/daemon/test_keychain_proxy.py b/tests/core/daemon/test_keychain_proxy.py index e126f26a76f1..c65d0b69f397 100644 --- a/tests/core/daemon/test_keychain_proxy.py +++ b/tests/core/daemon/test_keychain_proxy.py @@ -55,16 +55,16 @@ async def test_add_public_key(keychain_proxy: KeychainProxy) -> None: assert key.public_key == TEST_KEY_3.public_key assert key.secrets is None - pk = await keychain.get_public_key_for_fingerprint(TEST_KEY_3.fingerprint) + pk = await keychain.get_key_for_fingerprint(TEST_KEY_3.fingerprint, private=False) assert pk is not None assert pk == TEST_KEY_3.public_key - pk = await keychain.get_public_key_for_fingerprint(None) + pk = await keychain.get_key_for_fingerprint(None, private=False) assert pk is not None assert pk == TEST_KEY_3.public_key with pytest.raises(KeychainKeyNotFound): - pk = await keychain.get_public_key_for_fingerprint(1234567890) + pk = await keychain.get_key_for_fingerprint(1234567890, private=False) @pytest.mark.parametrize("include_secrets", [True, False]) @@ -80,12 +80,12 @@ async def test_get_key(keychain_proxy_with_keys: KeychainProxy, include_secrets: async def test_get_key_for_fingerprint(keychain_proxy: KeychainProxy) -> None: keychain = keychain_proxy with pytest.raises(KeychainIsEmpty): - await keychain.get_public_key_for_fingerprint(None) + await keychain.get_key_for_fingerprint(None, private=False) await keychain_proxy.add_key(TEST_KEY_1.mnemonic_str(), TEST_KEY_1.label) - assert await keychain.get_public_key_for_fingerprint(TEST_KEY_1.fingerprint) == TEST_KEY_1.public_key - assert await keychain.get_public_key_for_fingerprint(None) == TEST_KEY_1.public_key + assert await keychain.get_key_for_fingerprint(TEST_KEY_1.fingerprint, private=False) == TEST_KEY_1.public_key + assert await keychain.get_key_for_fingerprint(None, private=False) == TEST_KEY_1.public_key with pytest.raises(KeychainKeyNotFound): - await keychain.get_public_key_for_fingerprint(1234567890) + await keychain.get_key_for_fingerprint(1234567890, private=False) @pytest.mark.parametrize("include_secrets", [True, False]) From 71d3ba6323cb67f711ebdaf8e1389e2ce8b901a7 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 26 Feb 2024 12:14:35 -0800 Subject: [PATCH 138/274] Test fix --- chia/daemon/keychain_server.py | 6 ++++-- tests/core/daemon/test_daemon.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index fa279be58d2d..b5b506c560f1 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -174,7 +174,9 @@ def get_keychain_for_request(self, request: Dict[str, Any]) -> Keychain: async def handle_command(self, command: str, data: Dict[str, Any]) -> Dict[str, Any]: try: if command == "add_private_key": - return await self.add_key({"mnemonic_or_pk": data["mnemonic"], "label": data["label"], "private": True}) + return await self.add_key( + {"mnemonic_or_pk": data.get("mnemonic", None), "label": data.get("label", None), "private": True} + ) elif command == "add_key": return await self.add_key(data) elif command == "check_keys": @@ -218,7 +220,7 @@ async def add_key(self, request: Dict[str, Any]) -> Dict[str, Any]: return { "success": False, "error": KEYCHAIN_ERR_MALFORMED_REQUEST, - "error_details": {"message": "missing key_information"}, + "error_details": {"message": "missing key information"}, } try: diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index 93a780e40964..b6e4757d379c 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -971,7 +971,7 @@ async def test_add_private_key(daemon_connection_and_temp_keychain): missing_mnemonic_response_data = { "success": False, "error": "malformed request", - "error_details": {"message": "missing mnemonic"}, + "error_details": {"message": "missing key information"}, } mnemonic_with_typo_response_data = { From 3c172ef09aa52690f001a7bba307425e14eeaae8 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 27 Feb 2024 09:37:32 -0800 Subject: [PATCH 139/274] Merge fix --- tests/core/cmds/test_keys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index ad9dc43c5d1e..67a4ca886c2f 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -35,8 +35,8 @@ def keyring_with_one_key(empty_keyring): @pytest.fixture(scope="function") def keyring_with_one_sk_one_pk(empty_keyring): keychain = empty_keyring - keychain.add_private_key(TEST_MNEMONIC_SEED) - keychain.add_public_key(bytes(G1Element()).hex()) + keychain.add_key(TEST_MNEMONIC_SEED) + keychain.add_key(bytes(G1Element()).hex(), private=False) return keychain From 6222fd9ca4a18461848c1af676cee42d4fd78ebe Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 27 Feb 2024 15:13:12 -0800 Subject: [PATCH 140/274] Bad merge --- chia/wallet/wallet_node.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index df4ea6616a9b..61795d9c1e5b 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -57,7 +57,6 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.header_block import HeaderBlock from chia.types.mempool_inclusion_status import MempoolInclusionStatus -from chia.types.observation_root import ObservationRoot from chia.types.spend_bundle import SpendBundle from chia.types.weight_proof import WeightProof from chia.util.config import lock_and_load_config, process_config_start_method, save_config @@ -67,6 +66,7 @@ from chia.util.ints import uint16, uint32, uint64, uint128 from chia.util.keychain import Keychain from chia.util.misc import to_batches +from chia.util.observation_root import ObservationRoot from chia.util.path import path_from_root from chia.util.profiler import mem_profile_task, profile_task from chia.util.streamable import Streamable, streamable @@ -274,13 +274,17 @@ async def get_key_for_fingerprint( return key - async def get_key(self, fingerprint: Optional[int], private: bool = True) -> Optional[Union[PrivateKey, G1Element]]: + async def get_key( + self, fingerprint: Optional[int], private: bool = True + ) -> Optional[Union[PrivateKey, ObservationRoot]]: """ Attempt to get the private key for the given fingerprint. If the fingerprint is None, get_key_for_fingerprint() will return the first private key. Similarly, if a key isn't returned for the provided fingerprint, the first key will be returned. """ - key: Optional[Union[PrivateKey, G1Element]] = await self.get_key_for_fingerprint(fingerprint, private=private) + key: Optional[Union[PrivateKey, ObservationRoot]] = await self.get_key_for_fingerprint( + fingerprint, private=private + ) if key is None and fingerprint is not None: key = await self.get_key_for_fingerprint(None, private=private) From 8b882ab9a940cc727d479019161c8103a5ba5676 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 28 Feb 2024 07:19:53 -0800 Subject: [PATCH 141/274] Bad merge --- tests/core/daemon/test_daemon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index b6e4757d379c..c8c3eb1e6bee 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -29,7 +29,7 @@ from chia.simulator.setup_services import setup_full_node from chia.util.config import load_config from chia.util.json_util import dict_to_json_str -from chia.util.keychain import Keychain, KeyData, supports_os_passphrase_storage +from chia.util.keychain import Keychain, KeyData, KeyTypes, supports_os_passphrase_storage from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, KeyringWrapper from chia.util.ws_message import create_payload, create_payload_dict from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk @@ -1035,14 +1035,14 @@ async def assert_add_private_key_with_label( key_data_0 = KeyData.generate() await assert_add_private_key_with_label( key_data_0, - {"mnemonic": key_data_0.mnemonic_str()}, + {"mnemonic": key_data_0.mnemonic_str(), "key_type": KeyTypes.G1_ELEMENT}, add_private_key_response_data(key_data_0.fingerprint), ) # with `label=None` key_data_1 = KeyData.generate() await assert_add_private_key_with_label( key_data_1, - {"mnemonic": key_data_1.mnemonic_str(), "label": None}, + {"mnemonic": key_data_1.mnemonic_str(), "key_type": KeyTypes.G1_ELEMENT, "label": None}, add_private_key_response_data(key_data_1.fingerprint), ) # with `label="key_2"` From be1b6b01f32375a68b0f61a793bda6907c442b8e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 28 Feb 2024 07:47:06 -0800 Subject: [PATCH 142/274] oops --- tests/core/daemon/test_daemon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index c8c3eb1e6bee..3b6475bb6dc3 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -236,6 +236,7 @@ async def get_keys_for_plotting(self, request: Dict[str, Any]) -> Dict[str, Any] def add_private_key_response_data(fingerprint: int) -> Dict[str, object]: return { "success": True, + "key_type": KeyTypes.G1_ELEMENT.value, "fingerprint": fingerprint, } @@ -1035,14 +1036,14 @@ async def assert_add_private_key_with_label( key_data_0 = KeyData.generate() await assert_add_private_key_with_label( key_data_0, - {"mnemonic": key_data_0.mnemonic_str(), "key_type": KeyTypes.G1_ELEMENT}, + {"mnemonic": key_data_0.mnemonic_str()}, add_private_key_response_data(key_data_0.fingerprint), ) # with `label=None` key_data_1 = KeyData.generate() await assert_add_private_key_with_label( key_data_1, - {"mnemonic": key_data_1.mnemonic_str(), "key_type": KeyTypes.G1_ELEMENT, "label": None}, + {"mnemonic": key_data_1.mnemonic_str(), "label": None}, add_private_key_response_data(key_data_1.fingerprint), ) # with `label="key_2"` From acce83290fa15ede12c603929458f92de0fa0c81 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 28 Feb 2024 09:44:47 -0800 Subject: [PATCH 143/274] pylint --- chia/util/keychain.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 465ec40486ad..632559a14ab9 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -357,27 +357,27 @@ def _get_free_private_key_index(self) -> int: @overload def add_key(self, mnemonic_or_pk: str) -> Tuple[PrivateKey, KeyTypes]: - ... + raise NotImplementedError() @overload def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[PrivateKey, KeyTypes]: - ... + raise NotImplementedError() @overload def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> Tuple[PrivateKey, KeyTypes]: - ... + raise NotImplementedError() @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False] ) -> Tuple[ObservationRoot, KeyTypes]: - ... + raise NotImplementedError() @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: bool ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: - ... + raise NotImplementedError() def add_key( self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True From 8d431a1db5083494947ec20a3c93c6dea294bed4 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 28 Feb 2024 12:27:11 -0800 Subject: [PATCH 144/274] Tweak keychain_proxy.get_key_for_fingerprint --- chia/daemon/keychain_proxy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 0dd4017c7c90..67dc2ba71a3b 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -379,7 +379,10 @@ async def get_key_for_fingerprint( break else: raise KeychainKeyNotFound(fingerprint) - key = selected_key.private_key if private else selected_key.observation_root + if private: + key = selected_key.private_key if selected_key.secrets is not None else None + else: + key = selected_key.observation_root else: response, success = await self.get_response_for_request( "get_key_for_fingerprint", {"fingerprint": fingerprint, "private": private} From d9443e71e569e7ec24422aa516bd63cb530399b3 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 28 Feb 2024 13:03:52 -0800 Subject: [PATCH 145/274] Fix wallet RPC test? --- tests/wallet/rpc/test_wallet_rpc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index bfb106dd78a4..e7860fdc3aad 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1729,7 +1729,10 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn sk_dict = await client.get_private_key(pks[1]) assert sk_dict["fingerprint"] == pks[1] - assert not (await client.log_in(1234567890))["success"] + assert await client.log_in(1234567890) == ( + 'RPC response failure: {"error": "fingerprint 1234567890 not found' + ' in keychain or keychain is empty", "success": false}' + ) # test hardened keys await _check_delete_key(client=client, wallet_node=wallet_node, farmer_fp=pks[0], pool_fp=pks[1], observer=False) From eb5eb596ab27de4b0ca36fbaef28450d04a3663f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 28 Feb 2024 13:19:52 -0800 Subject: [PATCH 146/274] pragma: no cover --- chia/util/keychain.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 632559a14ab9..8d49c8fe9df7 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -357,27 +357,27 @@ def _get_free_private_key_index(self) -> int: @overload def add_key(self, mnemonic_or_pk: str) -> Tuple[PrivateKey, KeyTypes]: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover @overload def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[PrivateKey, KeyTypes]: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover @overload def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> Tuple[PrivateKey, KeyTypes]: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False] ) -> Tuple[ObservationRoot, KeyTypes]: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: bool ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover def add_key( self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True From 8d228af499151e0b1d3d38ff2562534dee3305ea Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 29 Feb 2024 11:16:13 +1300 Subject: [PATCH 147/274] make_solution and remove old launcher_id --- chia/wallet/vault/vault_info.py | 1 - chia/wallet/vault/vault_wallet.py | 61 +++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index 314893679e20..c32672503255 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -20,7 +20,6 @@ class VaultInfo(Streamable): hidden_puzzle_hash: bytes32 inner_puzzle_hash: bytes32 is_recoverable: bool - launcher_coin_id: bytes32 lineage_proof: LineageProof diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index ff93847a0e44..e0e42a7b54f1 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -24,6 +24,7 @@ from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions +from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition, make_reserve_fee_condition from chia.wallet.signer_protocol import ( PathHint, Signature, @@ -171,7 +172,7 @@ async def generate_p2_singleton_spends( ) assert len(coins) > 0 - p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.vault_info.launcher_coin_id) + p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.vault_info.launcher_id) spends: List[CoinSpend] = [] for coin in list(coins): @@ -211,7 +212,7 @@ async def _generate_unsigned_transaction( change = spend_value - total_amount assert change >= 0 if change > 0: - change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.vault_info.launcher_coin_id) + change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.vault_info.launcher_id) primaries.append(Payment(change_puzzle_hash, uint64(change))) conditions = [primary.as_condition() for primary in primaries] @@ -260,7 +261,7 @@ async def _generate_unsigned_transaction( proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) - full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, vault_inner_puzzle) + full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, vault_inner_puzzle) full_solution = get_vault_full_solution( self.vault_info.lineage_proof, uint64(self.vault_info.coin.amount), @@ -313,7 +314,7 @@ async def generate_signed_vault_spend( proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) - full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, vault_inner_puzzle) + full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, vault_inner_puzzle) full_solution = get_vault_full_solution( self.vault_info.lineage_proof, uint64(self.vault_info.coin.amount), @@ -440,7 +441,47 @@ def make_solution( conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), ) -> Program: - raise NotImplementedError("vault wallet") + 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)) + delegated_puzzle = puzzle_for_conditions(condition_list) + delegated_solution = solution_for_conditions(condition_list) + + secp_puzzle = construct_p2_delegated_secp( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + self.vault_info.hidden_puzzle_hash, + ) + secp_solution = Program.to( + [ + delegated_puzzle, + delegated_solution, + None, # signature slot + self.vault_info.coin.name(), + ] + ) + if self.vault_info.is_recoverable: + recovery_puzzle_hash = get_recovery_puzzle( + secp_puzzle.get_tree_hash(), + self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, + self.recovery_info.timelock if self.vault_info.is_recoverable else None, + ).get_tree_hash() + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) + else: + merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash()) + proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) + vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) + + full_solution = get_vault_full_solution( + self.vault_info.lineage_proof, + uint64(self.vault_info.coin.amount), + vault_inner_solution, + ) + return full_solution async def get_puzzle(self, new: bool) -> Program: if new: @@ -476,7 +517,7 @@ def get_recovery_info(self) -> Tuple[Optional[G1Element], Optional[uint64]]: return None, None def get_p2_singleton_puzzle_hash(self) -> bytes32: - return get_p2_singleton_puzzle_hash(self.vault_info.launcher_coin_id) + return get_p2_singleton_puzzle_hash(self.vault_info.launcher_id) async def select_coins(self, amount: uint64, coin_selection_config: CoinSelectionConfig) -> Set[Coin]: unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( @@ -553,7 +594,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: proof = get_vault_proof(merkle_tree, recovery_puzzle_hash) inner_solution = get_vault_inner_solution(recovery_puzzle, recovery_solution, proof) - full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, inner_puzzle) + full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, inner_puzzle) assert full_puzzle.get_tree_hash() == vault_coin.puzzle_hash full_solution = get_vault_full_solution(self.vault_info.lineage_proof, amount, inner_solution) @@ -565,7 +606,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: ) recovery_finish_solution = Program.to([]) recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) - full_recovery_puzzle = get_vault_full_puzzle(self.vault_info.launcher_coin_id, recovery_inner_puzzle) + full_recovery_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, recovery_inner_puzzle) recovery_coin = Coin(self.vault_info.coin.name(), full_recovery_puzzle.get_tree_hash(), amount) recovery_solution = get_vault_inner_solution(recovery_finish_puzzle, recovery_finish_solution, proof) lineage = LineageProof(self.vault_info.coin.name(), inner_puzzle.get_tree_hash(), amount) @@ -652,12 +693,11 @@ async def sync_vault_launcher(self) -> None: lineage_proof = LineageProof(parent_state.coin.parent_coin_info, None, uint64(parent_state.coin.amount)) vault_info = VaultInfo( coin_state.coin, - launcher_id, + parent_state.coin.name(), secp_pk, hidden_puzzle_hash, inner_puzzle_hash, is_recoverable, - parent_state.coin.name(), lineage_proof, ) await self.save_info(vault_info) @@ -714,7 +754,6 @@ async def update_vault_singleton( hidden_puzzle_hash, next_inner_puzzle.get_tree_hash(), self.vault_info.is_recoverable, - self.vault_info.launcher_coin_id, lineage_proof, ) From 37117fc7f9b22ccf64ebf06a445631b201214401 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 29 Feb 2024 19:03:07 +1300 Subject: [PATCH 148/274] store vault_info in singleton_store's custom data field --- chia/wallet/vault/vault_info.py | 3 ++- chia/wallet/vault/vault_wallet.py | 22 +++++++++++++++++----- chia/wallet/wallet_singleton_store.py | 21 +++++++++++---------- chia/wallet/wallet_state_manager.py | 8 ++++---- tests/wallet/vault/test_vault_wallet.py | 20 +++++++++++++++++++- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index c32672503255..2eb2e636ea19 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -23,7 +23,8 @@ class VaultInfo(Streamable): lineage_proof: LineageProof +@streamable @dataclass(frozen=True) -class RecoveryInfo: +class RecoveryInfo(Streamable): bls_pk: G1Element timelock: uint64 diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index e0e42a7b54f1..a30ad4a3614c 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -757,12 +757,24 @@ async def update_vault_singleton( lineage_proof, ) - await self.wallet_state_manager.singleton_store.add_spend(self.id(), coin_spend) + await self.update_vault_store( + new_vault_info, self.recovery_info if self.vault_info.is_recoverable else None, coin_spend + ) await self.save_info(new_vault_info) async def save_info(self, vault_info: VaultInfo) -> None: self.vault_info = vault_info - current_info = self.wallet_info - data_str = json.dumps(vault_info.to_json_dict()) - wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) - self.wallet_info = wallet_info + + async def update_vault_store( + self, vault_info: VaultInfo, recovery_info: Optional[RecoveryInfo], coin_spend: CoinSpend + ) -> None: + custom_data = bytes( + json.dumps( + { + "vault_info": vault_info.to_json_dict(), + "recovery_info": recovery_info.to_json_dict() if recovery_info else None, + } + ), + "utf-8", + ) + await self.wallet_state_manager.singleton_store.add_spend(self.id(), coin_spend, custom_data=custom_data) diff --git a/chia/wallet/wallet_singleton_store.py b/chia/wallet/wallet_singleton_store.py index 2f286fbb8703..f2e5a6102306 100644 --- a/chia/wallet/wallet_singleton_store.py +++ b/chia/wallet/wallet_singleton_store.py @@ -117,32 +117,33 @@ async def add_eve_record( async def add_spend( self, wallet_id: uint32, - coin_state: CoinSpend, + coin_spend: CoinSpend, block_height: uint32 = uint32(0), pending: bool = True, + custom_data: Optional[bytes] = None, ) -> None: """Given a coin spend of a singleton, attempt to calculate the child coin and details for the new singleton record. Add the new record to the store and remove the old record if it exists """ # get singleton_id from puzzle_reveal - singleton_id = get_singleton_id_from_puzzle(coin_state.puzzle_reveal) + singleton_id = get_singleton_id_from_puzzle(coin_spend.puzzle_reveal) if not singleton_id: raise RuntimeError("Coin to add is not a valid singleton") # get details for singleton record conditions = conditions_dict_for_solution( - coin_state.puzzle_reveal, coin_state.solution, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM + coin_spend.puzzle_reveal, coin_spend.solution, DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM ) cc_cond = [cond for cond in conditions[ConditionOpcode.CREATE_COIN] if int_from_bytes(cond.vars[1]) % 2 == 1][0] - coin = Coin(coin_state.coin.name(), cc_cond.vars[0], int_from_bytes(cc_cond.vars[1])) - inner_puz = get_inner_puzzle_from_singleton(coin_state.puzzle_reveal) + coin = Coin(coin_spend.coin.name(), cc_cond.vars[0], int_from_bytes(cc_cond.vars[1])) + inner_puz = get_inner_puzzle_from_singleton(coin_spend.puzzle_reveal) if inner_puz is None: # pragma: no cover - raise RuntimeError("Could not get inner puzzle from puzzle reveal in coin spend %s", coin_state) + raise RuntimeError("Could not get inner puzzle from puzzle reveal in coin spend %s", coin_spend) - lineage_bytes = [x.as_atom() for x in coin_state.solution.to_program().first().as_iter()] + lineage_bytes = [x.as_atom() for x in coin_spend.solution.to_program().first().as_iter()] if len(lineage_bytes) == 2: lineage_proof = LineageProof(bytes32(lineage_bytes[0]), None, int_from_bytes(lineage_bytes[1])) else: @@ -151,13 +152,13 @@ async def add_spend( ) # Create and save the new singleton record new_record = SingletonRecord( - coin, singleton_id, wallet_id, coin_state, inner_puz.get_tree_hash(), pending, 0, lineage_proof, None + coin, singleton_id, wallet_id, coin_spend, inner_puz.get_tree_hash(), pending, 0, lineage_proof, custom_data ) await self.save_singleton(new_record) # check if coin is in DB and mark deleted if found - current_records = await self.get_records_by_coin_id(coin_state.coin.name()) + current_records = await self.get_records_by_coin_id(coin_spend.coin.name()) if len(current_records) > 0: - await self.delete_singleton_by_coin_id(coin_state.coin.name(), block_height) + await self.delete_singleton_by_coin_id(coin_spend.coin.name(), block_height) return def _to_singleton_record(self, row: Row) -> SingletonRecord: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index de2d9631c206..fbc286b70b45 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -807,9 +807,7 @@ async def determine_coin_type( vault_check = match_vault_puzzle(uncurried.mod, uncurried.args) if vault_check: - await self.handle_vault(puzzle, coin_spend, coin_state) - # Return None since we don't want to add the vault singleton to the normal wallet coin store - return None, None + return await self.handle_vault(puzzle, coin_spend, coin_state), None dao_ids = [] wallets = self.wallets.values() @@ -1090,7 +1088,7 @@ async def handle_vault( puzzle: Program, coin_spend: CoinSpend, coin_state: CoinState, - ) -> None: + ) -> Optional[WalletIdentifier]: if isinstance(self.observation_root, VaultRoot): for wallet in self.wallets.values(): if wallet.type() == WalletType.STANDARD_WALLET: @@ -1099,6 +1097,8 @@ async def handle_vault( if coin_state.coin.amount % 2 == 1: # Update the vault singleton record await wallet.update_vault_singleton(puzzle, coin_spend, coin_state) + return WalletIdentifier.create(wallet) + return None async def handle_dao_cat( self, diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 2307d10f78ac..bb8494a125d1 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from hashlib import sha256 from typing import Awaitable, Callable, List @@ -15,6 +16,7 @@ from chia.wallet.signer_protocol import Spend from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG +from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault from tests.conftest import ConsensusMode @@ -168,7 +170,7 @@ async def test_vault_creation( ] amount = uint64(1000000) fee = uint64(100) - balance_delta = 1011000100 + balance_delta = 1011000099 unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( amount, recipient_ph, DEFAULT_TX_CONFIG, primaries=primaries, fee=fee @@ -187,6 +189,7 @@ async def test_vault_creation( signed_response = await wallet.apply_signatures(spends, signing_responses) await env.wallet_state_manager.submit_transactions([signed_response]) + vault_eve_id = wallet.vault_info.coin.name() await wallet_environments.process_pending_states( [ @@ -205,3 +208,18 @@ async def test_vault_creation( ), ], ) + + # check the wallet and singleton store have the latest vault coin + assert wallet.vault_info.coin.parent_coin_info == vault_eve_id + record = (await wallet.wallet_state_manager.singleton_store.get_records_by_coin_id(wallet.vault_info.coin.name()))[ + 0 + ] + assert record is not None + + assert isinstance(record.custom_data, bytes) + custom_data = json.loads(record.custom_data) + vault_info = VaultInfo.from_json_dict(custom_data["vault_info"]) + assert vault_info == wallet.vault_info + if wallet.vault_info.is_recoverable: + recovery_info = RecoveryInfo.from_json_dict(custom_data["recovery_info"]) + assert recovery_info == wallet.recovery_info From 8ad5e362e96785440fa79039981649601c52d386 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 29 Feb 2024 21:24:36 +1300 Subject: [PATCH 149/274] add vault keytype to keychain.add_key --- chia/util/keychain.py | 3 +++ tests/wallet/vault/test_vault_wallet.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index ebf26b3d0cde..0d8b5778f42d 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -427,6 +427,9 @@ def add_key( if len(pk_bytes) == 48: key = G1Element.from_bytes(pk_bytes) key_type = KeyTypes.G1_ELEMENT + elif len(pk_bytes) == 32: + key = VaultRoot(pk_bytes) + key_type = KeyTypes.VAULT_LAUNCHER else: raise ValueError(f"Cannot identify type of pubkey {mnemonic_or_pk}") # pragma: no cover key_data = pk_bytes.hex() diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index bb8494a125d1..38a1e496a114 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -81,7 +81,7 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b ), ] ) - await env.node.keychain_proxy.add_public_key(launcher_id.hex()) + await env.node.keychain_proxy.add_key(launcher_id.hex(), label="vault", private=False) await env.restart(vault_root.get_fingerprint()) await wallet_environments.full_node.wait_for_wallet_synced(env.node, 20) From 6bbc4364f6b62d0bcb1b703571f76bb9cc2abca3 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 5 Mar 2024 22:29:55 +1300 Subject: [PATCH 150/274] cleanup from quex's comments --- chia/wallet/vault/vault_wallet.py | 147 ++++------------------ chia/wallet/wallet.py | 1 + chia/wallet/wallet_protocol.py | 1 + chia/wallet/wallet_state_manager.py | 4 +- tests/wallet/test_main_wallet_protocol.py | 1 + tests/wallet/vault/test_vault_wallet.py | 10 ++ 6 files changed, 37 insertions(+), 127 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index a30ad4a3614c..6a48e1a41664 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -24,10 +24,8 @@ from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions, solution_for_conditions -from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition, make_reserve_fee_condition from chia.wallet.signer_protocol import ( PathHint, - Signature, SignedTransaction, SigningInstructions, SigningResponse, @@ -41,6 +39,7 @@ from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend +from chia.wallet.util.wallet_types import WalletIdentifier from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, construct_vault_merkle_tree, @@ -216,8 +215,9 @@ async def _generate_unsigned_transaction( primaries.append(Payment(change_puzzle_hash, uint64(change))) conditions = [primary.as_condition() for primary in primaries] - next_puzzle_hash = await self.get_new_puzzlehash() - # TODO: should the vault inner puz create this condition? + next_puzzle_hash = ( + self.vault_info.coin.puzzle_hash if tx_config.reuse_puzhash else (await self.get_new_puzzlehash()) + ) recreate_vault_condition = CreateCoin( next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] ).to_program() @@ -273,83 +273,6 @@ async def _generate_unsigned_transaction( return all_spends - async def generate_signed_vault_spend( - self, - signed_message: bytes, - delegated_puzzle: Program, - delegated_solution: Program, - p2_spends: List[CoinSpend], - primaries: List[Payment], - fee: uint64 = uint64(0), - ) -> List[TransactionRecord]: - secp_puzzle = construct_p2_delegated_secp( - self.vault_info.pubkey, - self.wallet_state_manager.constants.GENESIS_CHALLENGE, - self.vault_info.hidden_puzzle_hash, - ) - vault_inner_puzzle = get_vault_inner_puzzle( - self.vault_info.pubkey, - self.wallet_state_manager.constants.GENESIS_CHALLENGE, - self.vault_info.hidden_puzzle_hash, - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, - ) - secp_solution = Program.to( - [ - delegated_puzzle, - delegated_solution, - signed_message, - self.vault_info.coin.name(), - ] - ) - if self.vault_info.is_recoverable: - recovery_puzzle_hash = get_recovery_puzzle( - secp_puzzle.get_tree_hash(), - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, - ).get_tree_hash() - merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) - else: - merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash()) - proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) - vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) - - full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, vault_inner_puzzle) - full_solution = get_vault_full_solution( - self.vault_info.lineage_proof, - uint64(self.vault_info.coin.amount), - vault_inner_solution, - ) - - vault_spend = make_spend(self.vault_info.coin, full_puzzle, full_solution) - all_spends = [*p2_spends, vault_spend] - spend_bundle = SpendBundle(all_spends, G2Element()) - - amount = uint64(sum([payment.amount for payment in primaries])) - target_puzzle_hash = primaries[0].puzzle_hash - - tx_record = TransactionRecord( - confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), - to_puzzle_hash=target_puzzle_hash, - amount=amount, - fee_amount=fee, - confirmed=False, - sent=uint32(0), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), - wallet_id=self.id(), - sent_to=[], - memos=[], - trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), - name=spend_bundle.name(), - valid_times=parse_timelock_info(tuple()), - ) - await self.wallet_state_manager.add_pending_transactions([tx_record], sign=False) - return [tx_record] - def puzzle_for_pk(self, pubkey: G1Element) -> Program: raise NotImplementedError("vault wallet") @@ -372,7 +295,12 @@ async def get_puzzle_hash(self, new: bool) -> bytes32: async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: pk = self.vault_info.pubkey - vault_spend = [spend for spend in coin_spends if spend.as_coin_spend().coin == self.vault_info.coin][0] + # match the vault puzzle + for spend in coin_spends: + mod, curried_args = spend.puzzle.uncurry() + if match_vault_puzzle(mod, curried_args): + vault_spend = spend + break inner_sol = vault_spend.solution.at("rrf") secp_puz = inner_sol.at("rf") secp_sol = inner_sol.at("rrf") @@ -403,7 +331,7 @@ async def apply_signatures( signed_spends.append(spend) return SignedTransaction( TransactionInfo(signed_spends), - [Signature("bls_12381_aug_scheme", G2Element().to_bytes())], + [], ) async def execute_signing_instructions( @@ -440,48 +368,14 @@ def make_solution( primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + **kwargs: Any, ) -> 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)) - delegated_puzzle = puzzle_for_conditions(condition_list) - delegated_solution = solution_for_conditions(condition_list) - - secp_puzzle = construct_p2_delegated_secp( - self.vault_info.pubkey, - self.wallet_state_manager.constants.GENESIS_CHALLENGE, - self.vault_info.hidden_puzzle_hash, - ) - secp_solution = Program.to( - [ - delegated_puzzle, - delegated_solution, - None, # signature slot - self.vault_info.coin.name(), - ] - ) - if self.vault_info.is_recoverable: - recovery_puzzle_hash = get_recovery_puzzle( - secp_puzzle.get_tree_hash(), - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, - ).get_tree_hash() - merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) - else: - merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash()) - proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) - vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) - - full_solution = get_vault_full_solution( - self.vault_info.lineage_proof, - uint64(self.vault_info.coin.amount), - vault_inner_solution, - ) - return full_solution + coin_id = kwargs.get("coin_id") + if coin_id is None: + raise ValueError("Vault p2_singleton solutions require a coin id") + p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin_id]) + return p2_singleton_solution async def get_puzzle(self, new: bool) -> Program: if new: @@ -506,7 +400,12 @@ def require_derivation_paths(self) -> bool: return False async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: - raise NotImplementedError("vault wallet") + wallet_identifier: Optional[ + WalletIdentifier + ] = await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(hint) + if wallet_identifier: + return True + return False def handle_own_derivation(self) -> bool: return True diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 3e93c6285b0d..77283387b8f7 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -213,6 +213,7 @@ def make_solution( primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + **kwargs: Any, ) -> Program: assert fee >= 0 condition_list: List[Any] = [condition.to_program() for condition in conditions] diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 2db7a58002ef..52ec4e43e7ca 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -173,6 +173,7 @@ def make_solution( primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + **kwargs: Any, ) -> Program: ... diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index fbc286b70b45..1779bc29acf8 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -142,7 +142,7 @@ last_change_height_cs, ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType -from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash +from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash, match_vault_puzzle from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, ProofsChecker, construct_pending_approval_state @@ -803,8 +803,6 @@ async def determine_coin_type( uncurried = uncurry_puzzle(puzzle) - from chia.wallet.vault.vault_drivers import match_vault_puzzle - vault_check = match_vault_puzzle(uncurried.mod, uncurried.args) if vault_check: return await self.handle_vault(puzzle, coin_spend, coin_state), None diff --git a/tests/wallet/test_main_wallet_protocol.py b/tests/wallet/test_main_wallet_protocol.py index 705ed87199bb..6108bc536e9a 100644 --- a/tests/wallet/test_main_wallet_protocol.py +++ b/tests/wallet/test_main_wallet_protocol.py @@ -166,6 +166,7 @@ def make_solution( primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + **kwargs: Any, ) -> Program: condition_list: List[Condition] = [CreateCoin(p.puzzle_hash, p.amount, p.memos) for p in primaries] condition_list.append(ReserveFee(fee)) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 38a1e496a114..f56f5f74d02d 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -223,3 +223,13 @@ async def test_vault_creation( if wallet.vault_info.is_recoverable: recovery_info = RecoveryInfo.from_json_dict(custom_data["recovery_info"]) assert recovery_info == wallet.recovery_info + + # test make_solution + coin = (await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG)).pop() + wallet.make_solution(primaries, coin_id=coin.name()) + with pytest.raises(ValueError): + wallet.make_solution(primaries) + + # test match_hinted_coin + matched = await wallet.match_hinted_coin(wallet.vault_info.coin, wallet.vault_info.inner_puzzle_hash) + assert matched From 88903644e7884f20c693b6fdb5e968facfd27964 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 11 Mar 2024 18:19:13 +1300 Subject: [PATCH 151/274] make vault_wallet a dataclass and restructure vault_info and recovery_info --- chia/wallet/vault/vault_info.py | 18 ++--- chia/wallet/vault/vault_wallet.py | 88 ++++++++++++++----------- tests/wallet/vault/test_vault_wallet.py | 8 +-- 3 files changed, 63 insertions(+), 51 deletions(-) diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index 2eb2e636ea19..bca403232118 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Optional from chia_rs import G1Element @@ -11,6 +12,13 @@ from chia.wallet.lineage_proof import LineageProof +@streamable +@dataclass(frozen=True) +class RecoveryInfo(Streamable): + bls_pk: Optional[G1Element] = None + timelock: Optional[uint64] = None + + @streamable @dataclass(frozen=True) class VaultInfo(Streamable): @@ -19,12 +27,6 @@ class VaultInfo(Streamable): pubkey: bytes hidden_puzzle_hash: bytes32 inner_puzzle_hash: bytes32 - is_recoverable: bool lineage_proof: LineageProof - - -@streamable -@dataclass(frozen=True) -class RecoveryInfo(Streamable): - bls_pk: G1Element - timelock: uint64 + is_recoverable: bool + recovery_info: RecoveryInfo diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 6a48e1a41664..51a15c505252 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -3,6 +3,7 @@ import json import logging import time +from dataclasses import dataclass from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element @@ -65,7 +66,20 @@ from chia.wallet.wallet_protocol import GSTOptionalArgs +@dataclass class Vault(Wallet): + _vault_info: Optional[VaultInfo] = None + + @property + def vault_info(self) -> VaultInfo: + if self._vault_info is None: + raise ValueError("VaultInfo is not set") + return self._vault_info + + @vault_info.setter + def vault_info(self, new_vault_info: VaultInfo) -> None: + self._vault_info = new_vault_info + @staticmethod async def create( wallet_state_manager: Any, @@ -86,8 +100,8 @@ async def get_new_puzzle(self) -> Program: self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, ) return puzzle @@ -237,8 +251,8 @@ async def _generate_unsigned_transaction( self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, self.vault_info.hidden_puzzle_hash, - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, ) secp_solution = Program.to( @@ -252,8 +266,8 @@ async def _generate_unsigned_transaction( if self.vault_info.is_recoverable: recovery_puzzle_hash = get_recovery_puzzle( secp_puzzle.get_tree_hash(), - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, ).get_tree_hash() merkle_tree = construct_vault_merkle_tree(secp_puzzle.get_tree_hash(), recovery_puzzle_hash) else: @@ -395,7 +409,7 @@ def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: raise ValueError("This won't work") def require_derivation_paths(self) -> bool: - if getattr(self, "vault_info", None): + if getattr(self, "_vault_info", None): return True return False @@ -410,11 +424,6 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: def handle_own_derivation(self) -> bool: return True - def get_recovery_info(self) -> Tuple[Optional[G1Element], Optional[uint64]]: - if self.vault_info.is_recoverable: - return self.recovery_info.bls_pk, self.recovery_info.timelock - return None, None - def get_p2_singleton_puzzle_hash(self) -> bytes32: return get_p2_singleton_puzzle_hash(self.vault_info.launcher_id) @@ -439,13 +448,12 @@ async def select_coins(self, amount: uint64, coin_selection_config: CoinSelectio def derivation_for_index(self, index: int) -> List[DerivationRecord]: hidden_puzzle = get_vault_hidden_puzzle_with_index(uint32(index)) hidden_puzzle_hash = hidden_puzzle.get_tree_hash() - bls_pk, timelock = self.get_recovery_info() inner_puzzle_hash = get_vault_inner_puzzle_hash( self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, - bls_pk, - timelock, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, ) record = DerivationRecord( uint32(index), inner_puzzle_hash, self.vault_info.pubkey, self.type(), self.id(), False @@ -454,11 +462,11 @@ def derivation_for_index(self, index: int) -> List[DerivationRecord]: async def create_recovery_spends(self) -> List[TransactionRecord]: """ - Returns two spendbundles - 1. The spend recovering the vault which can be taken to the appropriate BLS wallet for signing - 2. The spend that completes the recovery after the timelock has elapsed + Returns two tx records + 1. Recover the vault which can be taken to the appropriate BLS wallet for signing + 2. Complete the recovery after the timelock has elapsed """ - assert self.vault_info.is_recoverable + assert self.vault_info.recovery_info is not None wallet_node: Any = self.wallet_state_manager.wallet_node peer = wallet_node.get_full_node_peer() assert peer is not None @@ -473,8 +481,8 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, self.vault_info.hidden_puzzle_hash, - self.recovery_info.bls_pk, - self.recovery_info.timelock, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, ) assert inner_puzzle.get_tree_hash() == self.vault_info.inner_puzzle_hash @@ -484,10 +492,12 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: self.vault_info.hidden_puzzle_hash, ).get_tree_hash() - recovery_puzzle = get_recovery_puzzle(secp_puzzle_hash, self.recovery_info.bls_pk, self.recovery_info.timelock) + recovery_puzzle = get_recovery_puzzle( + secp_puzzle_hash, self.vault_info.recovery_info.bls_pk, self.vault_info.recovery_info.timelock + ) recovery_puzzle_hash = recovery_puzzle.get_tree_hash() - - recovery_solution = get_recovery_solution(amount, self.recovery_info.bls_pk) + assert isinstance(self.vault_info.recovery_info.bls_pk, G1Element) + recovery_solution = get_recovery_solution(amount, self.vault_info.recovery_info.bls_pk) merkle_tree = construct_vault_merkle_tree(secp_puzzle_hash, recovery_puzzle_hash) proof = get_vault_proof(merkle_tree, recovery_puzzle_hash) @@ -500,8 +510,10 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: recovery_spend = SpendBundle([make_spend(vault_coin, full_puzzle, full_solution)], G2Element()) # 2. Generate the Finish Recovery Spend + assert isinstance(self.vault_info.recovery_info.bls_pk, G1Element) + assert isinstance(self.vault_info.recovery_info.timelock, uint64) recovery_finish_puzzle = get_recovery_finish_puzzle( - self.recovery_info.bls_pk, self.recovery_info.timelock, amount + self.vault_info.recovery_info.bls_pk, self.vault_info.recovery_info.timelock, amount ) recovery_finish_solution = Program.to([]) recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) @@ -582,10 +594,13 @@ async def sync_vault_launcher(self) -> None: secp_pk = memos.at("f").as_atom() hidden_puzzle_hash = bytes32(memos.at("rf").as_atom()) if memos.list_len() == 4: - is_recoverable = True bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) timelock = uint64(memos.at("rrrf").as_int()) - self.recovery_info = RecoveryInfo(bls_pk, timelock) + recovery_info = RecoveryInfo(bls_pk, timelock) + is_recoverable = True + else: + recovery_info = RecoveryInfo(None, None) + is_recoverable = False inner_puzzle_hash = get_vault_inner_puzzle_hash( secp_pk, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock ) @@ -596,8 +611,9 @@ async def sync_vault_launcher(self) -> None: secp_pk, hidden_puzzle_hash, inner_puzzle_hash, - is_recoverable, lineage_proof, + is_recoverable, + recovery_info, ) await self.save_info(vault_info) await self.wallet_state_manager.create_more_puzzle_hashes() @@ -631,8 +647,8 @@ async def update_vault_singleton( self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, - self.recovery_info.bls_pk if self.vault_info.is_recoverable else None, - self.recovery_info.timelock if self.vault_info.is_recoverable else None, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, ) # get the parent state to create lineage proof @@ -652,26 +668,22 @@ async def update_vault_singleton( self.vault_info.pubkey, hidden_puzzle_hash, next_inner_puzzle.get_tree_hash(), - self.vault_info.is_recoverable, lineage_proof, + self.vault_info.is_recoverable, + self.vault_info.recovery_info, ) - await self.update_vault_store( - new_vault_info, self.recovery_info if self.vault_info.is_recoverable else None, coin_spend - ) + await self.update_vault_store(new_vault_info, coin_spend) await self.save_info(new_vault_info) async def save_info(self, vault_info: VaultInfo) -> None: self.vault_info = vault_info - async def update_vault_store( - self, vault_info: VaultInfo, recovery_info: Optional[RecoveryInfo], coin_spend: CoinSpend - ) -> None: + async def update_vault_store(self, vault_info: VaultInfo, coin_spend: CoinSpend) -> None: custom_data = bytes( json.dumps( { "vault_info": vault_info.to_json_dict(), - "recovery_info": recovery_info.to_json_dict() if recovery_info else None, } ), "utf-8", diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index f56f5f74d02d..56e9a1de49f1 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -120,8 +120,7 @@ async def test_vault_creation( await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) if with_recovery: - assert wallet.vault_info.is_recoverable - assert wallet.recovery_info is not None + assert wallet.vault_info.recovery_info is not None [recovery_spend, finish_spend] = await wallet.create_recovery_spends() assert recovery_spend assert finish_spend @@ -220,9 +219,8 @@ async def test_vault_creation( custom_data = json.loads(record.custom_data) vault_info = VaultInfo.from_json_dict(custom_data["vault_info"]) assert vault_info == wallet.vault_info - if wallet.vault_info.is_recoverable: - recovery_info = RecoveryInfo.from_json_dict(custom_data["recovery_info"]) - assert recovery_info == wallet.recovery_info + recovery_info = RecoveryInfo.from_json_dict(custom_data["vault_info"]["recovery_info"]) + assert recovery_info == wallet.vault_info.recovery_info # test make_solution coin = (await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG)).pop() From 7eb45ab3be228002198e90ae90aba4ea46f75066 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 15 Mar 2024 11:01:04 -0700 Subject: [PATCH 152/274] Redesign clvm_streamable --- chia/rpc/util.py | 13 +- chia/rpc/wallet_rpc_client.py | 16 +- chia/util/streamable.py | 29 ++-- chia/wallet/signer_protocol.py | 52 ++++-- chia/wallet/util/clvm_streamable.py | 242 ++++++++++++--------------- tests/wallet/test_signer_protocol.py | 228 ++++++++++++------------- 6 files changed, 288 insertions(+), 292 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 99e64b9419cf..6d09004b4820 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -17,7 +17,7 @@ from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.clvm_streamable import clvm_serialization_mode +from chia.wallet.util.clvm_streamable import byte_serialize_clvm_streamable, json_serialize_with_clvm_streamable from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.tx_config import TXConfig, TXConfigLoader @@ -43,8 +43,13 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: o *args, **kwargs, ) - with clvm_serialization_mode(not request.get("full_jsonify", False)): + if request.get("full_jsonify", False): return response_obj.to_json_dict() + else: + response_dict = json_serialize_with_clvm_streamable(response_obj) + if isinstance(response_dict, str): # pragma: no cover + raise ValueError("Internal Error. Marshalled endpoint was made with clvm_streamable.") + return response_dict return rpc_endpoint @@ -144,7 +149,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s if request.get("full_jsonify", False): response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] else: - response["unsigned_transactions"] = [bytes(tx.as_program()).hex() for tx in unsigned_txs] + response["unsigned_transactions"] = [byte_serialize_clvm_streamable(tx).hex() for tx in unsigned_txs] new_txs: List[TransactionRecord] = [] if request.get("sign", self.service.config.get("auto_sign_txs", True)): @@ -154,7 +159,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s response["transactions"] = [ TransactionRecord.to_json_dict_convenience(tx, self.service.config) for tx in new_txs ] - response["signing_responses"] = [bytes(r.as_program()).hex() for r in signing_responses] + response["signing_responses"] = [byte_serialize_clvm_streamable(r).hex() for r in signing_responses] else: new_txs = tx_records # pragma: no cover diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index d815071f00b9..0111d6cd74e4 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -28,6 +28,7 @@ from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey +from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig from chia.wallet.util.wallet_types import WalletType @@ -1621,33 +1622,36 @@ async def gather_signing_info( self, args: GatherSigningInfo, ) -> GatherSigningInfoResponse: - return GatherSigningInfoResponse.from_json_dict( + return json_deserialize_with_clvm_streamable( await self.fetch( "gather_signing_info", args.to_json_dict(), - ) + ), + GatherSigningInfoResponse, ) async def apply_signatures( self, args: ApplySignatures, ) -> ApplySignaturesResponse: - return ApplySignaturesResponse.from_json_dict( + return json_deserialize_with_clvm_streamable( await self.fetch( "apply_signatures", args.to_json_dict(), - ) + ), + ApplySignaturesResponse, ) async def submit_transactions( self, args: SubmitTransactions, ) -> SubmitTransactionsResponse: - return SubmitTransactionsResponse.from_json_dict( + return json_deserialize_with_clvm_streamable( await self.fetch( "submit_transactions", args.to_json_dict(), - ) + ), + SubmitTransactionsResponse, ) async def vault_create( diff --git a/chia/util/streamable.py b/chia/util/streamable.py index 8a8ebbabaa5b..9e0e78f954cd 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -207,24 +207,28 @@ def streamable_from_dict(klass: Type[_T_Streamable], item: Any) -> _T_Streamable raise -def function_to_convert_one_item(f_type: Type[Any]) -> ConvertFunctionType: +def function_to_convert_one_item( + f_type: Type[Any], json_parser: Optional[Callable[[object], Streamable]] = None +) -> ConvertFunctionType: if is_type_SpecificOptional(f_type): - convert_inner_func = function_to_convert_one_item(get_args(f_type)[0]) + convert_inner_func = function_to_convert_one_item(get_args(f_type)[0], json_parser) return lambda item: convert_optional(convert_inner_func, item) elif is_type_Tuple(f_type): args = get_args(f_type) convert_inner_tuple_funcs = [] for arg in args: - convert_inner_tuple_funcs.append(function_to_convert_one_item(arg)) + convert_inner_tuple_funcs.append(function_to_convert_one_item(arg, json_parser)) # Ignoring for now as the proper solution isn't obvious return lambda items: convert_tuple(convert_inner_tuple_funcs, items) # type: ignore[arg-type] elif is_type_List(f_type): inner_type = get_args(f_type)[0] - convert_inner_func = function_to_convert_one_item(inner_type) + convert_inner_func = function_to_convert_one_item(inner_type, json_parser) # Ignoring for now as the proper solution isn't obvious return lambda items: convert_list(convert_inner_func, items) # type: ignore[arg-type] elif hasattr(f_type, "from_json_dict"): - return lambda item: f_type.from_json_dict(item) + if json_parser is None: + json_parser = f_type.from_json_dict + return lambda item: json_parser(item) elif issubclass(f_type, bytes): # Type is bytes, data is a hex string or bytes return lambda item: convert_byte_type(f_type, item) @@ -268,29 +272,28 @@ def function_to_post_init_process_one_item(f_type: Type[object]) -> ConvertFunct return lambda item: post_init_process_item(f_type, item) -def recurse_jsonify(d: Any) -> Any: +def recurse_jsonify(d: Any, next_recursion_step: Optional[Callable[[Any, Any], Any]] = None) -> Any: """ Makes bytes objects into strings with 0x, and makes large ints into strings. """ - if hasattr(d, "override_json_serialization"): - overrid_ret: Union[List[Any], Dict[str, Any], str, None, int] = d.override_json_serialization(recurse_jsonify) - return overrid_ret - elif dataclasses.is_dataclass(d): + if next_recursion_step is None: + next_recursion_step = recurse_jsonify + if dataclasses.is_dataclass(d): new_dict = {} for field in dataclasses.fields(d): - new_dict[field.name] = recurse_jsonify(getattr(d, field.name)) + new_dict[field.name] = next_recursion_step(getattr(d, field.name), None) return new_dict elif isinstance(d, list) or isinstance(d, tuple): new_list = [] for item in d: - new_list.append(recurse_jsonify(item)) + new_list.append(next_recursion_step(item, None)) return new_list elif isinstance(d, dict): new_dict = {} for name, val in d.items(): - new_dict[name] = recurse_jsonify(val) + new_dict[name] = next_recursion_step(val, None) return new_dict elif issubclass(type(d), bytes): diff --git a/chia/wallet/signer_protocol.py b/chia/wallet/signer_protocol.py index bc559481d21e..302dc0706f33 100644 --- a/chia/wallet/signer_protocol.py +++ b/chia/wallet/signer_protocol.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import List from chia.types.blockchain_format.coin import Coin as _Coin @@ -8,19 +9,24 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.util.ints import uint64 -from chia.wallet.util.clvm_streamable import ClvmStreamable +from chia.util.streamable import Streamable +from chia.wallet.util.clvm_streamable import clvm_streamable # This file contains the base types for communication between a wallet and an offline transaction signer. # These types should be compliant with CHIP-TBD -class Coin(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class Coin(Streamable): parent_coin_id: bytes32 puzzle_hash: bytes32 amount: uint64 -class Spend(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class Spend(Streamable): coin: Coin puzzle: Program solution: Program @@ -49,52 +55,72 @@ def as_coin_spend(self) -> CoinSpend: ) -class TransactionInfo(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class TransactionInfo(Streamable): spends: List[Spend] -class SigningTarget(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class SigningTarget(Streamable): fingerprint: bytes message: bytes hook: bytes32 -class SumHint(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class SumHint(Streamable): fingerprints: List[bytes] synthetic_offset: bytes final_pubkey: bytes -class PathHint(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class PathHint(Streamable): root_fingerprint: bytes path: List[uint64] -class KeyHints(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class KeyHints(Streamable): sum_hints: List[SumHint] path_hints: List[PathHint] -class SigningInstructions(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class SigningInstructions(Streamable): key_hints: KeyHints targets: List[SigningTarget] -class UnsignedTransaction(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class UnsignedTransaction(Streamable): transaction_info: TransactionInfo signing_instructions: SigningInstructions -class SigningResponse(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class SigningResponse(Streamable): signature: bytes hook: bytes32 -class Signature(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class Signature(Streamable): type: str signature: bytes -class SignedTransaction(ClvmStreamable): +@clvm_streamable +@dataclass(frozen=True) +class SignedTransaction(Streamable): transaction_info: TransactionInfo signatures: List[Signature] diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index fe692cfe7670..6b178d8c5cac 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -1,145 +1,109 @@ from __future__ import annotations -import contextvars -import threading -from contextlib import contextmanager -from dataclasses import dataclass, fields -from io import BytesIO -from typing import Any, BinaryIO, Callable, Dict, Iterator, Type, TypeVar +import dataclasses +from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union, get_args, get_type_hints from hsms.clvm_serde import from_program_for_type, to_program_for_type -from typing_extensions import dataclass_transform from chia.types.blockchain_format.program import Program -from chia.util.byte_types import hexstr_to_bytes -from chia.util.streamable import ConversionError, Streamable, streamable - - -# This class is meant to be a context var shared by multiple calls to the methods on ClvmStreamable objects -# It is ideally thread/coroutine safe meaning when code flow is non-linear, changes in one branch do not affect others -@dataclass -class ClvmSerializationConfig: - use: bool = False - - -class _ClvmSerializationMode: - config = contextvars.ContextVar("config", default=threading.local()) - - @classmethod - def get_config(cls) -> ClvmSerializationConfig: - return getattr(cls.config.get(), "config", ClvmSerializationConfig()) - - @classmethod - def set_config(cls, config: ClvmSerializationConfig) -> None: - cls.config.get().config = config - - -@contextmanager -def clvm_serialization_mode(use: bool) -> Iterator[None]: - old_config = _ClvmSerializationMode.get_config() - _ClvmSerializationMode.set_config(ClvmSerializationConfig(use=use)) - yield - _ClvmSerializationMode.set_config(old_config) - - -@dataclass_transform() -class ClvmStreamableMeta(type): - """ - We use a metaclass to define custom behavior during class initialization. We define logic such that classes that - inherit from ClvmStreamable (which uses this metaclass) behave as if they had been defined like this: - - @streamable - @dataclass(frozen=True) - class ChildClass(Streamable): - # custom streamable functions + hsms clvm_serde as/from_program methods - ... - - To streamline the process above and prevent mistakes/inconsistencies, we use the metaclass. - - TODO: Metaclasses are generally considered bad practice and we should probably pivot from this approach. - What is unclear, however, is how to keep the existing simple ergonomics and still hint that every class that this - logic has been applied to has all of the proper properties. Manadatory inheritance from a class that uses this - metaclass makes this simple because you simply need to check that something is ClvmStreamable to have those - guarantees. Perhaps in the future a decorator can be used to something like the effect of this metaclass. - """ - - def __init__(cls: ClvmStreamableMeta, *args: Any) -> None: - if cls.__name__ == "ClvmStreamable": - return - # Not sure how to fix the hints here, but it works - dcls: Type[ClvmStreamable] = streamable(dataclass(frozen=True)(cls)) # type: ignore[arg-type] - # Iterate over the fields of the class - for field_obj in fields(dcls): - field_name = field_obj.name - field_metadata = {"key": field_name} - field_metadata.update(field_obj.metadata) - setattr(field_obj, "metadata", field_metadata) - setattr(dcls, "as_program", to_program_for_type(dcls)) - setattr(dcls, "from_program", lambda prog: from_program_for_type(dcls)(prog)) - super().__init__(*args) - - -_T_ClvmStreamable = TypeVar("_T_ClvmStreamable", bound="ClvmStreamable") - - -class ClvmStreamable(Streamable, metaclass=ClvmStreamableMeta): - """ - Classes that inherit from this base class gain access to clvm serialization from hsms clvm_serde library. - Children also gain the ability to serialize differently under the clvm_serialization_mode context manager above. - If not called under the context manager, they will serialize according to the Streamable protocol. - """ - - def as_program(self) -> Program: - raise NotImplementedError() # pragma: no cover - - @classmethod - def from_program(cls: Type[_T_ClvmStreamable], prog: Program) -> _T_ClvmStreamable: - raise NotImplementedError() # pragma: no cover - - def stream(self, f: BinaryIO) -> None: - if _ClvmSerializationMode.get_config().use: - f.write(bytes(self.as_program())) - else: - super().stream(f) - - @classmethod - def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: - assert isinstance(f, BytesIO) - # This try/except is to faciliate deserializing blobs that have been serialized according to either the - # clvm_serde or streamable libraries. - try: - result = cls.from_program(Program.from_bytes(bytes(f.getbuffer()))) - f.read() - return result - except Exception: - return super().parse(f) - - def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], Dict[str, Any]]) -> Any: - if _ClvmSerializationMode.get_config().use: - # If we are using clvm_serde, we stop JSON serialization at this point and instead return the clvm blob - return bytes(self).hex() - else: - new_dict = {} - for field in fields(self): - new_dict[field.name] = default_recurse_jsonify(getattr(self, field.name)) - return new_dict - - @classmethod - def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStreamable: - # If we have reached this point, the Streamable library has determined we are a responsible for deserializing - # the value at this position in the dictionary. In order to preserve the ability to parse either streamable - # or clvm_serde objects in any context, we first check whether the value to be deserialized is a string. - # If it is, we know this value was serialized according to clvm_serde and we deserialize it as a clvm blob. - # If it is not, we know it was serialized according to streamable and we deserialize as a normal JSON dict - if isinstance(json_dict, str): - try: - byts = hexstr_to_bytes(json_dict) - except ValueError as e: - raise ConversionError(json_dict, cls, e) - - try: - return cls.from_program(Program.from_bytes(byts)) - except Exception as e: - raise ConversionError(json_dict, cls, e) - else: - return super().from_json_dict(json_dict) +from chia.util.streamable import ( + Streamable, + function_to_convert_one_item, + is_type_List, + is_type_SpecificOptional, + is_type_Tuple, + recurse_jsonify, + streamable, +) + +_T_Streamable = TypeVar("_T_Streamable", bound=Streamable) + + +def clvm_streamable(cls: Type[Streamable]) -> Type[Streamable]: + wrapped_cls: Type[Streamable] = streamable(cls) + setattr(wrapped_cls, "__clvm_streamable__", True) + + hints = get_type_hints(cls) + # no way to hint that wrapped_cls is a dataclass here but @streamable checks that + for field in dataclasses.fields(wrapped_cls): # type: ignore[arg-type] + if is_type_Tuple(hints[field.name]): + raise ValueError("@clvm_streamable does not support tuples") + + return wrapped_cls + + +def program_serialize_clvm_streamable(clvm_streamable: Streamable) -> Program: + # Underlying hinting problem with clvm_serde + return to_program_for_type(type(clvm_streamable))(clvm_streamable) # type: ignore[no-any-return] + + +def byte_serialize_clvm_streamable(clvm_streamable: Streamable) -> bytes: + return bytes(program_serialize_clvm_streamable(clvm_streamable)) + + +def json_serialize_with_clvm_streamable( + streamable: Any, next_recursion_step: Optional[Callable[[Any, Any], Dict[str, Any]]] = None +) -> Union[str, Dict[str, Any]]: + if next_recursion_step is None: + next_recursion_step = recurse_jsonify + if hasattr(streamable, "__clvm_streamable__"): + # If we are using clvm_serde, we stop JSON serialization at this point and instead return the clvm blob + return byte_serialize_clvm_streamable(streamable).hex() + else: + return next_recursion_step(streamable, json_serialize_with_clvm_streamable) + + +def program_deserialize_clvm_streamable(program: Program, clvm_streamable_type: Type[_T_Streamable]) -> _T_Streamable: + # Underlying hinting problem with clvm_serde + return from_program_for_type(clvm_streamable_type)(program) # type: ignore[no-any-return] + + +def byte_deserialize_clvm_streamable(blob: bytes, clvm_streamable_type: Type[_T_Streamable]) -> _T_Streamable: + return program_deserialize_clvm_streamable(Program.from_bytes(blob), clvm_streamable_type) + + +def is_compound_type(typ: Any) -> bool: + return is_type_SpecificOptional(typ) or is_type_Tuple(typ) or is_type_List(typ) + + +def json_deserialize_with_clvm_streamable( + json_dict: Union[str, Dict[str, Any]], streamable_type: Type[_T_Streamable] +) -> _T_Streamable: + if isinstance(json_dict, str): + return byte_deserialize_clvm_streamable(bytes.fromhex(json_dict), streamable_type) + else: + + def bind_type_to_this_function( + type: Type[object], + ) -> Callable[[object], Streamable]: + def convert_function(item: object) -> Streamable: + # Type ignore due to underlying incompatibilites with streamable library + return json_deserialize_with_clvm_streamable(item, type) # type: ignore + + return convert_function + + old_streamable_fields = streamable_type.streamable_fields() + new_streamable_fields = [] + for old_field in old_streamable_fields: + if is_compound_type(old_field.type): + inner_type = get_args(old_field.type)[0] + if hasattr(inner_type, "__clvm_streamable__"): + new_streamable_fields.append( + dataclasses.replace( + old_field, + convert_function=function_to_convert_one_item( + old_field.type, bind_type_to_this_function(inner_type) + ), + ) + ) + else: + new_streamable_fields.append(old_field) + elif hasattr(old_field.type, "__clvm_streamable__"): + new_streamable_fields.append( + dataclasses.replace(old_field, convert_function=bind_type_to_this_function(old_field.type)) + ) + else: + new_streamable_fields.append(old_field) + + setattr(streamable_type, "_streamable_fields", tuple(new_streamable_fields)) + return streamable_type.from_json_dict(json_dict) diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index 61c45ee4df79..d3d44314d73d 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -1,10 +1,7 @@ from __future__ import annotations -import asyncio import dataclasses -import threading -import time -from typing import List, Optional +from typing import List, Optional, Tuple import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey @@ -17,7 +14,7 @@ from chia.types.coin_spend import CoinSpend, make_spend from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint64 -from chia.util.streamable import ConversionError, Streamable, streamable +from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import AggSigMe from chia.wallet.derivation_record import DerivationRecord from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( @@ -36,14 +33,120 @@ TransactionInfo, UnsignedTransaction, ) -from chia.wallet.util.clvm_streamable import ClvmSerializationConfig, _ClvmSerializationMode, clvm_serialization_mode +from chia.wallet.util.clvm_streamable import ( + byte_deserialize_clvm_streamable, + byte_serialize_clvm_streamable, + clvm_streamable, + json_deserialize_with_clvm_streamable, + json_serialize_with_clvm_streamable, + program_deserialize_clvm_streamable, + program_serialize_clvm_streamable, +) from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager from tests.environments.wallet import WalletStateTransition, WalletTestFramework -def test_signing_serialization() -> None: +@clvm_streamable +@dataclasses.dataclass(frozen=True) +class Temp(Streamable): + a: str + + +def test_basic_serialization() -> None: + instance = Temp(a="1") + assert program_serialize_clvm_streamable(instance) == Program.to(["1"]) + assert byte_serialize_clvm_streamable(instance).hex() == "ff3180" + assert json_serialize_with_clvm_streamable(instance) == "ff3180" + assert program_deserialize_clvm_streamable(Program.to(["1"]), Temp) == instance + assert byte_deserialize_clvm_streamable(bytes.fromhex("ff3180"), Temp) == instance + assert json_deserialize_with_clvm_streamable("ff3180", Temp) == instance + + +@streamable +@dataclasses.dataclass(frozen=True) +class OutsideStreamable(Streamable): + inside: Temp + a: str + + +@clvm_streamable +@dataclasses.dataclass(frozen=True) +class OutsideCLVM(Streamable): + inside: Temp + a: str + + +def test_nested_serialization() -> None: + instance = OutsideStreamable(a="1", inside=Temp(a="1")) + assert json_serialize_with_clvm_streamable(instance) == {"inside": "ff3180", "a": "1"} + assert json_deserialize_with_clvm_streamable({"inside": "ff3180", "a": "1"}, OutsideStreamable) == instance + assert OutsideStreamable.from_json_dict({"a": "1", "inside": {"a": "1"}}) == instance + + instance_clvm = OutsideCLVM(a="1", inside=Temp(a="1")) + assert program_serialize_clvm_streamable(instance_clvm) == Program.to([["1"], "1"]) + assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff3180ff3180" + assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff3180ff3180" + assert program_deserialize_clvm_streamable(Program.to([["1"], "1"]), OutsideCLVM) == instance_clvm + assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff3180ff3180"), OutsideCLVM) == instance_clvm + assert json_deserialize_with_clvm_streamable("ffff3180ff3180", OutsideCLVM) == instance_clvm + + +@streamable +@dataclasses.dataclass(frozen=True) +class Compound(Streamable): + optional: Optional[Temp] + list: List[Temp] + + +@clvm_streamable +@dataclasses.dataclass(frozen=True) +class CompoundCLVM(Streamable): + optional: Optional[Temp] + list: List[Temp] + + +def test_compound_type_serialization() -> None: + # regular streamable + regular values + instance = Compound(optional=Temp(a="1"), list=[Temp(a="1")]) + assert json_serialize_with_clvm_streamable(instance) == {"optional": "ff3180", "list": ["ff3180"]} + assert json_deserialize_with_clvm_streamable({"optional": "ff3180", "list": ["ff3180"]}, Compound) == instance + assert Compound.from_json_dict({"optional": {"a": "1"}, "list": [{"a": "1"}]}) == instance + + # regular streamable + falsey values + instance = Compound(optional=None, list=[]) + assert json_serialize_with_clvm_streamable(instance) == {"optional": None, "list": []} + assert json_deserialize_with_clvm_streamable({"optional": None, "list": []}, Compound) == instance + assert Compound.from_json_dict({"optional": None, "list": []}) == instance + + # clvm streamable + regular values + instance_clvm = CompoundCLVM(optional=Temp(a="1"), list=[Temp(a="1")]) + assert program_serialize_clvm_streamable(instance_clvm) == Program.to([[True, "1"], [["1"]]]) + assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff01ff3180ffffff31808080" + assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff01ff3180ffffff31808080" + assert program_deserialize_clvm_streamable(Program.to([[True, "1"], [["1"]]]), CompoundCLVM) == instance_clvm + assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff01ff3180ffffff31808080"), CompoundCLVM) == instance_clvm + assert json_deserialize_with_clvm_streamable("ffff01ff3180ffffff31808080", CompoundCLVM) == instance_clvm + + # clvm streamable + falsey values + instance_clvm = CompoundCLVM(optional=None, list=[]) + assert program_serialize_clvm_streamable(instance_clvm) == Program.to([[0], []]) + assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff8080ff8080" + assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff8080ff8080" + assert program_deserialize_clvm_streamable(Program.to([[0, 0], []]), CompoundCLVM) == instance_clvm + assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff8080ff8080"), CompoundCLVM) == instance_clvm + assert json_deserialize_with_clvm_streamable("ffff8080ff8080", CompoundCLVM) == instance_clvm + + with pytest.raises(ValueError, match="@clvm_streamable"): + + @clvm_streamable + @dataclasses.dataclass(frozen=True) + class DoesntWork(Streamable): + optional: Tuple[str] + + +def test_unsigned_transaction_type() -> None: pubkey: G1Element = G1Element() message: bytes = b"message" @@ -62,8 +165,7 @@ def test_signing_serialization() -> None: ), ) - assert tx == UnsignedTransaction.from_program(Program.from_bytes(bytes(tx.as_program()))) - + assert tx == json_deserialize_with_clvm_streamable(json_serialize_with_clvm_streamable(tx), UnsignedTransaction) as_json_dict = { "coin": { "parent_coin_id": "0x" + tx.transaction_info.spends[0].coin.parent_coin_id.hex(), @@ -75,114 +177,6 @@ def test_signing_serialization() -> None: } assert tx.transaction_info.spends[0].to_json_dict() == as_json_dict - # Test from_json_dict with the special case where it encounters the as_program serialization in the middle of JSON - assert tx.transaction_info.spends[0] == Spend.from_json_dict( - { - "coin": bytes(tx.transaction_info.spends[0].coin.as_program()).hex(), - "puzzle": bytes(tx.transaction_info.spends[0].puzzle).hex(), - "solution": bytes(tx.transaction_info.spends[0].solution).hex(), - } - ) - - # Test the optional serialization as blobs - with clvm_serialization_mode(True): - assert ( - tx.transaction_info.spends[0].to_json_dict() - == bytes(tx.transaction_info.spends[0].as_program()).hex() # type: ignore[comparison-overlap] - ) - - # Make sure it's still a dict if using a Streamable object - @streamable - @dataclasses.dataclass(frozen=True) - class TempStreamable(Streamable): - streamable_key: Spend - - with clvm_serialization_mode(True): - assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == { - "streamable_key": bytes(tx.transaction_info.spends[0].as_program()).hex() - } - - with clvm_serialization_mode(False): - assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == {"streamable_key": as_json_dict} - - with clvm_serialization_mode(False): - assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == {"streamable_key": as_json_dict} - with clvm_serialization_mode(True): - assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == { - "streamable_key": bytes(tx.transaction_info.spends[0].as_program()).hex() - } - with clvm_serialization_mode(False): - assert TempStreamable(tx.transaction_info.spends[0]).to_json_dict() == {"streamable_key": as_json_dict} - - streamable_blob = bytes(tx.transaction_info.spends[0]) - with clvm_serialization_mode(True): - clvm_streamable_blob = bytes(tx.transaction_info.spends[0]) - - assert streamable_blob != clvm_streamable_blob - Spend.from_bytes(streamable_blob) - Spend.from_bytes(clvm_streamable_blob) - assert Spend.from_bytes(streamable_blob) == Spend.from_bytes(clvm_streamable_blob) == tx.transaction_info.spends[0] - - with clvm_serialization_mode(False): - assert bytes(tx.transaction_info.spends[0]) == streamable_blob - - inside_streamable_blob = bytes(TempStreamable(tx.transaction_info.spends[0])) - with clvm_serialization_mode(True): - inside_clvm_streamable_blob = bytes(TempStreamable(tx.transaction_info.spends[0])) - - assert inside_streamable_blob != inside_clvm_streamable_blob - assert ( - TempStreamable.from_bytes(inside_streamable_blob) - == TempStreamable.from_bytes(inside_clvm_streamable_blob) - == TempStreamable(tx.transaction_info.spends[0]) - ) - - # Test some json loading errors - - with pytest.raises(ConversionError): - Spend.from_json_dict("blah") - with pytest.raises(ConversionError): - UnsignedTransaction.from_json_dict(streamable_blob.hex()) - - -def test_serialization_config_thread_safe() -> None: - def get_and_check_config(use: bool, wait_before: int, wait_after: int) -> None: - with clvm_serialization_mode(use): - time.sleep(wait_before) - assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig(use) - time.sleep(wait_after) - assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig() - - thread_1 = threading.Thread(target=get_and_check_config, args=(True, 0, 2)) - thread_2 = threading.Thread(target=get_and_check_config, args=(False, 1, 3)) - thread_3 = threading.Thread(target=get_and_check_config, args=(True, 2, 4)) - thread_4 = threading.Thread(target=get_and_check_config, args=(False, 3, 5)) - - thread_1.start() - thread_2.start() - thread_3.start() - thread_4.start() - - thread_1.join() - thread_2.join() - thread_3.join() - thread_4.join() - - -@pytest.mark.anyio -async def test_serialization_config_coroutine_safe() -> None: - async def get_and_check_config(use: bool, wait_before: int, wait_after: int) -> None: - with clvm_serialization_mode(use): - await asyncio.sleep(wait_before) - assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig(use) - await asyncio.sleep(wait_after) - assert _ClvmSerializationMode.get_config() == ClvmSerializationConfig() - - await get_and_check_config(True, 0, 2) - await get_and_check_config(False, 1, 3) - await get_and_check_config(True, 2, 4) - await get_and_check_config(False, 3, 5) - @pytest.mark.parametrize( "wallet_environments", From 6e2c7c5e7bccce4ac803994493c074ca09bc5e35 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Mar 2024 13:12:48 -0700 Subject: [PATCH 153/274] Fix test imports --- chia/_tests/wallet/test_signer_protocol.py | 2 +- chia/_tests/wallet/vault/test_vault_clsp.py | 2 +- chia/_tests/wallet/vault/test_vault_wallet.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chia/_tests/wallet/test_signer_protocol.py b/chia/_tests/wallet/test_signer_protocol.py index 653fc4842263..2f763effd64a 100644 --- a/chia/_tests/wallet/test_signer_protocol.py +++ b/chia/_tests/wallet/test_signer_protocol.py @@ -8,8 +8,8 @@ import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey -from tests.environments.wallet import WalletStateTransition, WalletTestFramework +from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia.rpc.wallet_request_types import ApplySignatures, GatherSigningInfo, SubmitTransactions from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.types.blockchain_format.coin import Coin as ConsensusCoin diff --git a/chia/_tests/wallet/vault/test_vault_clsp.py b/chia/_tests/wallet/vault/test_vault_clsp.py index 9d72f652fc81..0bdad674b3c6 100644 --- a/chia/_tests/wallet/vault/test_vault_clsp.py +++ b/chia/_tests/wallet/vault/test_vault_clsp.py @@ -5,8 +5,8 @@ import pytest from chia_rs import PrivateKey from ecdsa import NIST256p, SigningKey -from tests.clvm.test_puzzles import secret_exponent_for_index +from chia._tests.clvm.test_puzzles import secret_exponent_for_index from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.program import INFINITE_COST, Program from chia.types.blockchain_format.sized_bytes import bytes32 diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index b7c783d1ba3b..d1f2014133cd 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -4,9 +4,9 @@ import pytest from ecdsa import NIST256p, SigningKey -from tests.conftest import ConsensusMode -from tests.environments.wallet import WalletStateTransition, WalletTestFramework +from chia._tests.conftest import ConsensusMode +from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia.util.ints import uint32, uint64 from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG From fa96d059e25e6ceadeb6ca5c41dab9fa3c34c5bb Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Mar 2024 13:16:29 -0700 Subject: [PATCH 154/274] black --- chia/daemon/keychain_proxy.py | 33 +++++++++++++++------------------ chia/util/keychain.py | 15 +++++---------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 98e28476df18..250a4bfe4156 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -171,24 +171,21 @@ def handle_error(self, response: WsRpcMessage) -> None: raise Exception(f"{error}") @overload - async def add_key(self, mnemonic_or_pk: str) -> PrivateKey: - ... + async def add_key(self, mnemonic_or_pk: str) -> PrivateKey: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: - ... + async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: - ... + async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: - ... + async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: bool) -> Union[PrivateKey, G1Element]: - ... + async def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: bool + ) -> Union[PrivateKey, G1Element]: ... async def add_key( self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True @@ -331,22 +328,22 @@ async def get_first_private_key(self) -> Optional[PrivateKey]: return key @overload - async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: - ... + async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: ... @overload - async def get_key_for_fingerprint(self, fingerprint: Optional[int], private: Literal[True]) -> Optional[PrivateKey]: - ... + async def get_key_for_fingerprint( + self, fingerprint: Optional[int], private: Literal[True] + ) -> Optional[PrivateKey]: ... @overload - async def get_key_for_fingerprint(self, fingerprint: Optional[int], private: Literal[False]) -> Optional[G1Element]: - ... + async def get_key_for_fingerprint( + self, fingerprint: Optional[int], private: Literal[False] + ) -> Optional[G1Element]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool - ) -> Optional[Union[PrivateKey, G1Element]]: - ... + ) -> Optional[Union[PrivateKey, G1Element]]: ... async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool = True diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 8496d3bd03e9..a23149a731d4 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -333,24 +333,19 @@ def _get_free_private_key_index(self) -> int: return index @overload - def add_key(self, mnemonic_or_pk: str) -> PrivateKey: - ... + def add_key(self, mnemonic_or_pk: str) -> PrivateKey: ... @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: - ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> PrivateKey: ... @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: - ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> PrivateKey: ... @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: - ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False]) -> G1Element: ... @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: bool) -> Union[PrivateKey, G1Element]: - ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: bool) -> Union[PrivateKey, G1Element]: ... def add_key( self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True From 334b4bd150fb6222aee77f772bfee2efc03f541d Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 18 Mar 2024 13:40:58 -0700 Subject: [PATCH 155/274] fix one more test --- chia/_tests/wallet/vault/test_vault_lifecycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py index 4c507ddf969d..9796e7ebe9c5 100644 --- a/chia/_tests/wallet/vault/test_vault_lifecycle.py +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -7,8 +7,8 @@ from chia_rs import AugSchemeMPL, G2Element, PrivateKey from clvm.casts import int_to_bytes from ecdsa import NIST256p, SigningKey -from tests.clvm.test_puzzles import secret_exponent_for_index +from chia._tests.clvm.test_puzzles import secret_exponent_for_index from chia.clvm.spend_sim import CostLogger, sim_and_client from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.coin import Coin From 713bf95a00df742ecc3c134a7ae90ae304f0ba38 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 19 Mar 2024 09:33:38 -0700 Subject: [PATCH 156/274] Address comments by @altendky --- chia/wallet/util/clvm_streamable.py | 8 +- tests/wallet/test_clvm_streamable.py | 116 +++++++++++++++++++++++++++ tests/wallet/test_signer_protocol.py | 111 +------------------------ 3 files changed, 122 insertions(+), 113 deletions(-) create mode 100644 tests/wallet/test_clvm_streamable.py diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 6b178d8c5cac..f885a2bd8e04 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -21,7 +21,7 @@ def clvm_streamable(cls: Type[Streamable]) -> Type[Streamable]: wrapped_cls: Type[Streamable] = streamable(cls) - setattr(wrapped_cls, "__clvm_streamable__", True) + setattr(wrapped_cls, "_clvm_streamable", True) hints = get_type_hints(cls) # no way to hint that wrapped_cls is a dataclass here but @streamable checks that @@ -46,7 +46,7 @@ def json_serialize_with_clvm_streamable( ) -> Union[str, Dict[str, Any]]: if next_recursion_step is None: next_recursion_step = recurse_jsonify - if hasattr(streamable, "__clvm_streamable__"): + if hasattr(streamable, "_clvm_streamable"): # If we are using clvm_serde, we stop JSON serialization at this point and instead return the clvm blob return byte_serialize_clvm_streamable(streamable).hex() else: @@ -87,7 +87,7 @@ def convert_function(item: object) -> Streamable: for old_field in old_streamable_fields: if is_compound_type(old_field.type): inner_type = get_args(old_field.type)[0] - if hasattr(inner_type, "__clvm_streamable__"): + if hasattr(inner_type, "_clvm_streamable"): new_streamable_fields.append( dataclasses.replace( old_field, @@ -98,7 +98,7 @@ def convert_function(item: object) -> Streamable: ) else: new_streamable_fields.append(old_field) - elif hasattr(old_field.type, "__clvm_streamable__"): + elif hasattr(old_field.type, "_clvm_streamable"): new_streamable_fields.append( dataclasses.replace(old_field, convert_function=bind_type_to_this_function(old_field.type)) ) diff --git a/tests/wallet/test_clvm_streamable.py b/tests/wallet/test_clvm_streamable.py new file mode 100644 index 000000000000..20f9010a5875 --- /dev/null +++ b/tests/wallet/test_clvm_streamable.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import dataclasses +from typing import List, Optional, Tuple + +import pytest + +from chia.types.blockchain_format.program import Program +from chia.util.streamable import Streamable, streamable +from chia.wallet.util.clvm_streamable import ( + byte_deserialize_clvm_streamable, + byte_serialize_clvm_streamable, + clvm_streamable, + json_deserialize_with_clvm_streamable, + json_serialize_with_clvm_streamable, + program_deserialize_clvm_streamable, + program_serialize_clvm_streamable, +) + + +@clvm_streamable +@dataclasses.dataclass(frozen=True) +class BasicCLVMStreamable(Streamable): + a: str + + +def test_basic_serialization() -> None: + instance = BasicCLVMStreamable(a="1") + assert program_serialize_clvm_streamable(instance) == Program.to(["1"]) + assert byte_serialize_clvm_streamable(instance).hex() == "ff3180" + assert json_serialize_with_clvm_streamable(instance) == "ff3180" + assert program_deserialize_clvm_streamable(Program.to(["1"]), BasicCLVMStreamable) == instance + assert byte_deserialize_clvm_streamable(bytes.fromhex("ff3180"), BasicCLVMStreamable) == instance + assert json_deserialize_with_clvm_streamable("ff3180", BasicCLVMStreamable) == instance + + +@streamable +@dataclasses.dataclass(frozen=True) +class OutsideStreamable(Streamable): + inside: BasicCLVMStreamable + a: str + + +@clvm_streamable +@dataclasses.dataclass(frozen=True) +class OutsideCLVM(Streamable): + inside: BasicCLVMStreamable + a: str + + +def test_nested_serialization() -> None: + instance = OutsideStreamable(a="1", inside=BasicCLVMStreamable(a="1")) + assert json_serialize_with_clvm_streamable(instance) == {"inside": "ff3180", "a": "1"} + assert json_deserialize_with_clvm_streamable({"inside": "ff3180", "a": "1"}, OutsideStreamable) == instance + assert OutsideStreamable.from_json_dict({"a": "1", "inside": {"a": "1"}}) == instance + + instance_clvm = OutsideCLVM(a="1", inside=BasicCLVMStreamable(a="1")) + assert program_serialize_clvm_streamable(instance_clvm) == Program.to([["1"], "1"]) + assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff3180ff3180" + assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff3180ff3180" + assert program_deserialize_clvm_streamable(Program.to([["1"], "1"]), OutsideCLVM) == instance_clvm + assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff3180ff3180"), OutsideCLVM) == instance_clvm + assert json_deserialize_with_clvm_streamable("ffff3180ff3180", OutsideCLVM) == instance_clvm + + +@streamable +@dataclasses.dataclass(frozen=True) +class Compound(Streamable): + optional: Optional[BasicCLVMStreamable] + list: List[BasicCLVMStreamable] + + +@clvm_streamable +@dataclasses.dataclass(frozen=True) +class CompoundCLVM(Streamable): + optional: Optional[BasicCLVMStreamable] + list: List[BasicCLVMStreamable] + + +def test_compound_type_serialization() -> None: + # regular streamable + regular values + instance = Compound(optional=BasicCLVMStreamable(a="1"), list=[BasicCLVMStreamable(a="1")]) + assert json_serialize_with_clvm_streamable(instance) == {"optional": "ff3180", "list": ["ff3180"]} + assert json_deserialize_with_clvm_streamable({"optional": "ff3180", "list": ["ff3180"]}, Compound) == instance + assert Compound.from_json_dict({"optional": {"a": "1"}, "list": [{"a": "1"}]}) == instance + + # regular streamable + falsey values + instance = Compound(optional=None, list=[]) + assert json_serialize_with_clvm_streamable(instance) == {"optional": None, "list": []} + assert json_deserialize_with_clvm_streamable({"optional": None, "list": []}, Compound) == instance + assert Compound.from_json_dict({"optional": None, "list": []}) == instance + + # clvm streamable + regular values + instance_clvm = CompoundCLVM(optional=BasicCLVMStreamable(a="1"), list=[BasicCLVMStreamable(a="1")]) + assert program_serialize_clvm_streamable(instance_clvm) == Program.to([[True, "1"], [["1"]]]) + assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff01ff3180ffffff31808080" + assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff01ff3180ffffff31808080" + assert program_deserialize_clvm_streamable(Program.to([[True, "1"], [["1"]]]), CompoundCLVM) == instance_clvm + assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff01ff3180ffffff31808080"), CompoundCLVM) == instance_clvm + assert json_deserialize_with_clvm_streamable("ffff01ff3180ffffff31808080", CompoundCLVM) == instance_clvm + + # clvm streamable + falsey values + instance_clvm = CompoundCLVM(optional=None, list=[]) + assert program_serialize_clvm_streamable(instance_clvm) == Program.to([[0], []]) + assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff8080ff8080" + assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff8080ff8080" + assert program_deserialize_clvm_streamable(Program.to([[0, 0], []]), CompoundCLVM) == instance_clvm + assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff8080ff8080"), CompoundCLVM) == instance_clvm + assert json_deserialize_with_clvm_streamable("ffff8080ff8080", CompoundCLVM) == instance_clvm + + with pytest.raises(ValueError, match="@clvm_streamable"): + + @clvm_streamable + @dataclasses.dataclass(frozen=True) + class DoesntWork(Streamable): + tuples_are_not_supported: Tuple[str] diff --git a/tests/wallet/test_signer_protocol.py b/tests/wallet/test_signer_protocol.py index d3d44314d73d..83b3e4623375 100644 --- a/tests/wallet/test_signer_protocol.py +++ b/tests/wallet/test_signer_protocol.py @@ -1,7 +1,7 @@ from __future__ import annotations import dataclasses -from typing import List, Optional, Tuple +from typing import List, Optional import pytest from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey @@ -14,7 +14,6 @@ from chia.types.coin_spend import CoinSpend, make_spend from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint64 -from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import AggSigMe from chia.wallet.derivation_record import DerivationRecord from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( @@ -33,119 +32,13 @@ TransactionInfo, UnsignedTransaction, ) -from chia.wallet.util.clvm_streamable import ( - byte_deserialize_clvm_streamable, - byte_serialize_clvm_streamable, - clvm_streamable, - json_deserialize_with_clvm_streamable, - json_serialize_with_clvm_streamable, - program_deserialize_clvm_streamable, - program_serialize_clvm_streamable, -) +from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable, json_serialize_with_clvm_streamable from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager from tests.environments.wallet import WalletStateTransition, WalletTestFramework -@clvm_streamable -@dataclasses.dataclass(frozen=True) -class Temp(Streamable): - a: str - - -def test_basic_serialization() -> None: - instance = Temp(a="1") - assert program_serialize_clvm_streamable(instance) == Program.to(["1"]) - assert byte_serialize_clvm_streamable(instance).hex() == "ff3180" - assert json_serialize_with_clvm_streamable(instance) == "ff3180" - assert program_deserialize_clvm_streamable(Program.to(["1"]), Temp) == instance - assert byte_deserialize_clvm_streamable(bytes.fromhex("ff3180"), Temp) == instance - assert json_deserialize_with_clvm_streamable("ff3180", Temp) == instance - - -@streamable -@dataclasses.dataclass(frozen=True) -class OutsideStreamable(Streamable): - inside: Temp - a: str - - -@clvm_streamable -@dataclasses.dataclass(frozen=True) -class OutsideCLVM(Streamable): - inside: Temp - a: str - - -def test_nested_serialization() -> None: - instance = OutsideStreamable(a="1", inside=Temp(a="1")) - assert json_serialize_with_clvm_streamable(instance) == {"inside": "ff3180", "a": "1"} - assert json_deserialize_with_clvm_streamable({"inside": "ff3180", "a": "1"}, OutsideStreamable) == instance - assert OutsideStreamable.from_json_dict({"a": "1", "inside": {"a": "1"}}) == instance - - instance_clvm = OutsideCLVM(a="1", inside=Temp(a="1")) - assert program_serialize_clvm_streamable(instance_clvm) == Program.to([["1"], "1"]) - assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff3180ff3180" - assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff3180ff3180" - assert program_deserialize_clvm_streamable(Program.to([["1"], "1"]), OutsideCLVM) == instance_clvm - assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff3180ff3180"), OutsideCLVM) == instance_clvm - assert json_deserialize_with_clvm_streamable("ffff3180ff3180", OutsideCLVM) == instance_clvm - - -@streamable -@dataclasses.dataclass(frozen=True) -class Compound(Streamable): - optional: Optional[Temp] - list: List[Temp] - - -@clvm_streamable -@dataclasses.dataclass(frozen=True) -class CompoundCLVM(Streamable): - optional: Optional[Temp] - list: List[Temp] - - -def test_compound_type_serialization() -> None: - # regular streamable + regular values - instance = Compound(optional=Temp(a="1"), list=[Temp(a="1")]) - assert json_serialize_with_clvm_streamable(instance) == {"optional": "ff3180", "list": ["ff3180"]} - assert json_deserialize_with_clvm_streamable({"optional": "ff3180", "list": ["ff3180"]}, Compound) == instance - assert Compound.from_json_dict({"optional": {"a": "1"}, "list": [{"a": "1"}]}) == instance - - # regular streamable + falsey values - instance = Compound(optional=None, list=[]) - assert json_serialize_with_clvm_streamable(instance) == {"optional": None, "list": []} - assert json_deserialize_with_clvm_streamable({"optional": None, "list": []}, Compound) == instance - assert Compound.from_json_dict({"optional": None, "list": []}) == instance - - # clvm streamable + regular values - instance_clvm = CompoundCLVM(optional=Temp(a="1"), list=[Temp(a="1")]) - assert program_serialize_clvm_streamable(instance_clvm) == Program.to([[True, "1"], [["1"]]]) - assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff01ff3180ffffff31808080" - assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff01ff3180ffffff31808080" - assert program_deserialize_clvm_streamable(Program.to([[True, "1"], [["1"]]]), CompoundCLVM) == instance_clvm - assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff01ff3180ffffff31808080"), CompoundCLVM) == instance_clvm - assert json_deserialize_with_clvm_streamable("ffff01ff3180ffffff31808080", CompoundCLVM) == instance_clvm - - # clvm streamable + falsey values - instance_clvm = CompoundCLVM(optional=None, list=[]) - assert program_serialize_clvm_streamable(instance_clvm) == Program.to([[0], []]) - assert byte_serialize_clvm_streamable(instance_clvm).hex() == "ffff8080ff8080" - assert json_serialize_with_clvm_streamable(instance_clvm) == "ffff8080ff8080" - assert program_deserialize_clvm_streamable(Program.to([[0, 0], []]), CompoundCLVM) == instance_clvm - assert byte_deserialize_clvm_streamable(bytes.fromhex("ffff8080ff8080"), CompoundCLVM) == instance_clvm - assert json_deserialize_with_clvm_streamable("ffff8080ff8080", CompoundCLVM) == instance_clvm - - with pytest.raises(ValueError, match="@clvm_streamable"): - - @clvm_streamable - @dataclasses.dataclass(frozen=True) - class DoesntWork(Streamable): - optional: Tuple[str] - - def test_unsigned_transaction_type() -> None: pubkey: G1Element = G1Element() message: bytes = b"message" From eb34622a342bf6152bc55b60fce0e6f76f0766b2 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 20 Mar 2024 17:30:24 +1300 Subject: [PATCH 157/274] remove negative_change_allowed var, clarify pk in keychain.get_key_data --- chia/util/keychain.py | 27 ++++++++++++++------------- chia/wallet/vault/vault_wallet.py | 5 ++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 0d8b5778f42d..fa5c5378b60d 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -340,27 +340,28 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: if read_str is None or len(read_str) == 0: raise KeychainUserNotFound(self.service, user) str_bytes = bytes.fromhex(read_str) - - pk_bytes: bytes = str_bytes[: G1Element.SIZE] - if len(pk_bytes) == 32: - observation_root: ObservationRoot = VaultRoot.from_bytes(pk_bytes) + pk = str_bytes + entropy = None + if len(str_bytes) == 32: + observation_root: ObservationRoot = VaultRoot.from_bytes(str_bytes) fingerprint = observation_root.get_fingerprint() - entropy = None key_type = KeyTypes.VAULT_LAUNCHER.value - elif len(pk_bytes) == 48: - observation_root = G1Element.from_bytes(pk_bytes) + elif len(str_bytes) == 48: + observation_root = G1Element.from_bytes(str_bytes) fingerprint = observation_root.get_fingerprint() - if len(str_bytes) == G1Element.SIZE + 32: - entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] - else: - entropy = None + key_type = KeyTypes.G1_ELEMENT.value + elif len(str_bytes) == G1Element.SIZE + 32: + pk = str_bytes[: G1Element.SIZE] + observation_root = G1Element.from_bytes(pk) + fingerprint = observation_root.get_fingerprint() + entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] key_type = KeyTypes.G1_ELEMENT.value else: - raise ValueError(f"Public key must be either 32 or 48 bytes, got {len(pk_bytes)} bytes") + raise ValueError(f"Public key must be either 32 or 48 bytes, got {len(pk)} bytes") return KeyData( fingerprint=uint32(fingerprint), - public_key=pk_bytes, + public_key=pk, label=self.keyring_wrapper.get_label(fingerprint), secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, key_type=key_type, diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 51a15c505252..9243f45bd075 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -79,7 +79,7 @@ def vault_info(self) -> VaultInfo: @vault_info.setter def vault_info(self, new_vault_info: VaultInfo) -> None: self._vault_info = new_vault_info - + @staticmethod async def create( wallet_state_manager: Any, @@ -125,7 +125,6 @@ async def generate_signed_transaction( """ Creates Un-signed transactions to be passed into signer. """ - negative_change_allowed: bool = kwargs.get("negative_change_allowed", False) if primaries is None: non_change_amount: int = amount else: @@ -140,7 +139,7 @@ async def generate_signed_transaction( coins=coins, primaries_input=primaries, memos=memos, - negative_change_allowed=negative_change_allowed, + negative_change_allowed=kwargs.get("negative_change_allowed", False), puzzle_decorator_override=puzzle_decorator_override, extra_conditions=extra_conditions, ) From 7ca3002da04d6c31755cb5751a2b7d6f39afea89 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 20 Mar 2024 07:12:25 -0700 Subject: [PATCH 158/274] Test coverage --- tests/cmds/test_cmd_framework.py | 42 +++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/cmds/test_cmd_framework.py b/tests/cmds/test_cmd_framework.py index 910e4afba5e5..deb83cda9e18 100644 --- a/tests/cmds/test_cmd_framework.py +++ b/tests/cmds/test_cmd_framework.py @@ -232,7 +232,7 @@ def run(self) -> None: @chia_command(cmd, "temp_cmd_optional_bad", "blah") class TempCMDOptionalBad3: - optional: Optional[int] = option("--optional", default="string") + optional: Optional[int] = option("--optional", default="string", required=False) def run(self) -> None: ... @@ -302,6 +302,46 @@ class TempCMDBadType: def run(self) -> None: ... + # Test invalid default + with pytest.raises(TypeError): + + @chia_command(cmd, "temp_cmd_bad_default", "blah") + class TempCMDBadDefault: + integer: int = option("--int", default="string") + + def run(self) -> None: + ... + + # Test bytes parsing + @chia_command(cmd, "temp_cmd_bad_bytes", "blah") + class TempCMDBadBytes: + blob: bytes = option("--blob", required=True) + + def run(self) -> None: + ... + + @chia_command(cmd, "temp_cmd_bad_bytes32", "blah") + class TempCMDBadBytes32: + blob32: bytes = option("--blob32", required=True) + + def run(self) -> None: + ... + + runner = CliRunner() + result = runner.invoke( + cmd, + ["temp_cmd_bad_bytes", "--blob", "not a blob"], + catch_exceptions=False, + ) + assert "not a valid hex string" in result.output + + result = runner.invoke( + cmd, + ["temp_cmd_bad_bytes32", "--blob32", "not a blob"], + catch_exceptions=False, + ) + assert "not a valid hex string" in result.output + @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="doesn't matter") @pytest.mark.parametrize( From c793920193632338a9b1613d320ed783d62d1640 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 20 Mar 2024 09:48:30 -0700 Subject: [PATCH 159/274] bytes32 --- tests/cmds/test_cmd_framework.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cmds/test_cmd_framework.py b/tests/cmds/test_cmd_framework.py index deb83cda9e18..c1b93a78e1c2 100644 --- a/tests/cmds/test_cmd_framework.py +++ b/tests/cmds/test_cmd_framework.py @@ -322,7 +322,7 @@ def run(self) -> None: @chia_command(cmd, "temp_cmd_bad_bytes32", "blah") class TempCMDBadBytes32: - blob32: bytes = option("--blob32", required=True) + blob32: bytes32 = option("--blob32", required=True) def run(self) -> None: ... @@ -337,10 +337,10 @@ def run(self) -> None: result = runner.invoke( cmd, - ["temp_cmd_bad_bytes32", "--blob32", "not a blob"], + ["temp_cmd_bad_bytes32", "--blob32", "0xdeadbeef"], catch_exceptions=False, ) - assert "not a valid hex string" in result.output + assert "not a valid 32-byte hex string" in result.output @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN], reason="doesn't matter") From 2fec730fec75e6ef48c5483dd7a60d00b5947c94 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 20 Mar 2024 13:09:16 -0700 Subject: [PATCH 160/274] Better CLI mnemonic check --- chia/cmds/keys_funcs.py | 11 +++++++++-- chia/util/keychain.py | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 20f7dd06d713..f9c587806a21 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -17,7 +17,14 @@ from chia.util.errors import KeychainException from chia.util.file_keyring import MAX_LABEL_LENGTH from chia.util.ints import uint32 -from chia.util.keychain import Keychain, KeyData, bytes_to_mnemonic, generate_mnemonic, mnemonic_to_seed +from chia.util.keychain import ( + Keychain, + KeyData, + bytes_to_mnemonic, + check_mnemonic_validity, + generate_mnemonic, + mnemonic_to_seed, +) from chia.util.keyring_wrapper import KeyringWrapper from chia.wallet.derive_keys import ( master_pk_to_wallet_pk_unhardened, @@ -78,7 +85,7 @@ def add_key_info(mnemonic_or_pk: str, label: Optional[str]) -> None: """ unlock_keyring() try: - if mnemonic_or_pk.count(" ") == 23: + if check_mnemonic_validity(mnemonic_or_pk): sk = Keychain().add_key(mnemonic_or_pk, label, private=True) fingerprint = sk.get_g1().get_fingerprint() print(f"Added private key with public key fingerprint {fingerprint}") diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 8496d3bd03e9..d64b3f365006 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -88,6 +88,11 @@ def bytes_to_mnemonic(mnemonic_bytes: bytes) -> str: return " ".join(mnemonics) +def check_mnemonic_validity(mnemonic_str: str) -> bool: + mnemonic: List[str] = mnemonic_str.split(" ") + return len(mnemonic) in [12, 15, 18, 21, 24] + + def mnemonic_from_short_words(mnemonic_str: str) -> str: """ Since the first 4 letters of each word is unique (or the full word, if less than 4 characters), and its common From acaf5aae0aac722723390f0e2238b76a665a91b6 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 21 Mar 2024 09:24:01 +1300 Subject: [PATCH 161/274] get launcher_id from ObservationRoot --- chia/wallet/vault/vault_info.py | 1 - chia/wallet/vault/vault_wallet.py | 43 +++++++++++++------------ tests/wallet/vault/test_vault_wallet.py | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/chia/wallet/vault/vault_info.py b/chia/wallet/vault/vault_info.py index bca403232118..65d63e78c57a 100644 --- a/chia/wallet/vault/vault_info.py +++ b/chia/wallet/vault/vault_info.py @@ -23,7 +23,6 @@ class RecoveryInfo(Streamable): @dataclass(frozen=True) class VaultInfo(Streamable): coin: Coin - launcher_id: bytes32 pubkey: bytes hidden_puzzle_hash: bytes32 inner_puzzle_hash: bytes32 diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 9243f45bd075..956e8a5cef91 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -76,9 +76,10 @@ def vault_info(self) -> VaultInfo: raise ValueError("VaultInfo is not set") return self._vault_info - @vault_info.setter - def vault_info(self, new_vault_info: VaultInfo) -> None: - self._vault_info = new_vault_info + @property + def launcher_id(self) -> bytes32: + assert isinstance(self.wallet_state_manager.observation_root, VaultRoot) + return bytes32(self.wallet_state_manager.observation_root.launcher_id) @staticmethod async def create( @@ -184,7 +185,7 @@ async def generate_p2_singleton_spends( ) assert len(coins) > 0 - p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.vault_info.launcher_id) + p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.launcher_id) spends: List[CoinSpend] = [] for coin in list(coins): @@ -224,7 +225,7 @@ async def _generate_unsigned_transaction( change = spend_value - total_amount assert change >= 0 if change > 0: - change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.vault_info.launcher_id) + change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.launcher_id) primaries.append(Payment(change_puzzle_hash, uint64(change))) conditions = [primary.as_condition() for primary in primaries] @@ -274,7 +275,7 @@ async def _generate_unsigned_transaction( proof = get_vault_proof(merkle_tree, secp_puzzle.get_tree_hash()) vault_inner_solution = get_vault_inner_solution(secp_puzzle, secp_solution, proof) - full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, vault_inner_puzzle) + full_puzzle = get_vault_full_puzzle(self.launcher_id, vault_inner_puzzle) full_solution = get_vault_full_solution( self.vault_info.lineage_proof, uint64(self.vault_info.coin.amount), @@ -424,7 +425,7 @@ def handle_own_derivation(self) -> bool: return True def get_p2_singleton_puzzle_hash(self) -> bytes32: - return get_p2_singleton_puzzle_hash(self.vault_info.launcher_id) + return get_p2_singleton_puzzle_hash(self.launcher_id) async def select_coins(self, amount: uint64, coin_selection_config: CoinSelectionConfig) -> Set[Coin]: unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( @@ -502,7 +503,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: proof = get_vault_proof(merkle_tree, recovery_puzzle_hash) inner_solution = get_vault_inner_solution(recovery_puzzle, recovery_solution, proof) - full_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, inner_puzzle) + full_puzzle = get_vault_full_puzzle(self.launcher_id, inner_puzzle) assert full_puzzle.get_tree_hash() == vault_coin.puzzle_hash full_solution = get_vault_full_solution(self.vault_info.lineage_proof, amount, inner_solution) @@ -516,7 +517,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: ) recovery_finish_solution = Program.to([]) recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) - full_recovery_puzzle = get_vault_full_puzzle(self.vault_info.launcher_id, recovery_inner_puzzle) + full_recovery_puzzle = get_vault_full_puzzle(self.launcher_id, recovery_inner_puzzle) recovery_coin = Coin(self.vault_info.coin.name(), full_recovery_puzzle.get_tree_hash(), amount) recovery_solution = get_vault_inner_solution(recovery_finish_puzzle, recovery_finish_solution, proof) lineage = LineageProof(self.vault_info.coin.name(), inner_puzzle.get_tree_hash(), amount) @@ -574,16 +575,15 @@ async def sync_vault_launcher(self) -> None: assert peer is not None assert isinstance(self.wallet_state_manager.observation_root, VaultRoot) - launcher_id = bytes32(self.wallet_state_manager.observation_root.launcher_id) - coin_states = await wallet_node.get_coin_state([launcher_id], peer) + coin_states = await wallet_node.get_coin_state([self.launcher_id], peer) if not coin_states: - raise ValueError(f"No coin found for launcher id: {launcher_id}.") + raise ValueError(f"No coin found for launcher id: {self.launcher_id}.") coin_state: CoinState = coin_states[0] - parent_state: CoinState = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] + # parent_state: CoinState = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] - assert parent_state.spent_height is not None - launcher_spend = await fetch_coin_spend(uint32(parent_state.spent_height), parent_state.coin, peer) + assert coin_state.spent_height is not None + launcher_spend = await fetch_coin_spend(uint32(coin_state.spent_height), coin_state.coin, peer) launcher_solution = launcher_spend.solution.to_program() is_recoverable = False @@ -600,13 +600,15 @@ async def sync_vault_launcher(self) -> None: else: recovery_info = RecoveryInfo(None, None) is_recoverable = False - inner_puzzle_hash = get_vault_inner_puzzle_hash( + inner_puzzle = get_vault_inner_puzzle( secp_pk, self.wallet_state_manager.constants.GENESIS_CHALLENGE, hidden_puzzle_hash, bls_pk, timelock ) - lineage_proof = LineageProof(parent_state.coin.parent_coin_info, None, uint64(parent_state.coin.amount)) + inner_puzzle_hash = inner_puzzle.get_tree_hash() + lineage_proof = LineageProof(coin_state.coin.parent_coin_info, None, uint64(coin_state.coin.amount)) + vault_puzzle_hash = get_vault_full_puzzle(coin_state.coin.name(), inner_puzzle).get_tree_hash() + vault_coin = Coin(self.launcher_id, vault_puzzle_hash, uint64(coin_state.coin.amount)) vault_info = VaultInfo( - coin_state.coin, - parent_state.coin.name(), + vault_coin, secp_pk, hidden_puzzle_hash, inner_puzzle_hash, @@ -663,7 +665,6 @@ async def update_vault_singleton( ) new_vault_info = VaultInfo( coin_state.coin, - self.vault_info.launcher_id, self.vault_info.pubkey, hidden_puzzle_hash, next_inner_puzzle.get_tree_hash(), @@ -676,7 +677,7 @@ async def update_vault_singleton( await self.save_info(new_vault_info) async def save_info(self, vault_info: VaultInfo) -> None: - self.vault_info = vault_info + self._vault_info = vault_info async def update_vault_store(self, vault_info: VaultInfo, coin_spend: CoinSpend) -> None: custom_data = bytes( diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 56e9a1de49f1..1bed32fbd9fc 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -47,7 +47,7 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b assert vault_tx eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] - launcher_id = eve_coin.name() + launcher_id = eve_coin.parent_coin_info vault_root = VaultRoot.from_bytes(launcher_id) await wallet_environments.process_pending_states( [ From 6f46486c595b694b7e9000f744f22c59b4136ff5 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Mar 2024 07:22:02 -0700 Subject: [PATCH 162/274] Use functools.partial --- chia/wallet/util/clvm_streamable.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index f885a2bd8e04..ac98522ec6b3 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -1,6 +1,7 @@ from __future__ import annotations import dataclasses +import functools from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union, get_args, get_type_hints from hsms.clvm_serde import from_program_for_type, to_program_for_type @@ -72,16 +73,6 @@ def json_deserialize_with_clvm_streamable( if isinstance(json_dict, str): return byte_deserialize_clvm_streamable(bytes.fromhex(json_dict), streamable_type) else: - - def bind_type_to_this_function( - type: Type[object], - ) -> Callable[[object], Streamable]: - def convert_function(item: object) -> Streamable: - # Type ignore due to underlying incompatibilites with streamable library - return json_deserialize_with_clvm_streamable(item, type) # type: ignore - - return convert_function - old_streamable_fields = streamable_type.streamable_fields() new_streamable_fields = [] for old_field in old_streamable_fields: @@ -92,7 +83,8 @@ def convert_function(item: object) -> Streamable: dataclasses.replace( old_field, convert_function=function_to_convert_one_item( - old_field.type, bind_type_to_this_function(inner_type) + old_field.type, + functools.partial(json_deserialize_with_clvm_streamable, streamable_type=inner_type), ), ) ) @@ -100,7 +92,12 @@ def convert_function(item: object) -> Streamable: new_streamable_fields.append(old_field) elif hasattr(old_field.type, "_clvm_streamable"): new_streamable_fields.append( - dataclasses.replace(old_field, convert_function=bind_type_to_this_function(old_field.type)) + dataclasses.replace( + old_field, + convert_function=functools.partial( + json_deserialize_with_clvm_streamable, streamable_type=old_field.type + ), + ) ) else: new_streamable_fields.append(old_field) From 568025315ee1020ab7707455bfc5ae0587235f5a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Mar 2024 09:42:26 -0700 Subject: [PATCH 163/274] Rename full jsonify to chip 29 --- chia/rpc/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 6d09004b4820..a0282e1cf499 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -43,7 +43,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args: object, **kwargs: o *args, **kwargs, ) - if request.get("full_jsonify", False): + if not request.get("chip-29", True): return response_obj.to_json_dict() else: response_dict = json_serialize_with_clvm_streamable(response_obj) From 70f5665267b1dc3217f82a4cf281dcf281a2d7c7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 21 Mar 2024 13:15:36 -0700 Subject: [PATCH 164/274] whoops missed a couple --- chia/rpc/util.py | 2 +- tests/wallet/rpc/test_wallet_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index a0282e1cf499..ffbd2b9ecb6c 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -146,7 +146,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s ] unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs(tx_records) - if request.get("full_jsonify", False): + if not request.get("chip-29", True): response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] else: response["unsigned_transactions"] = [byte_serialize_clvm_streamable(tx).hex() for tx in unsigned_txs] diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 6012aa3648e9..c40a4efbf256 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -337,7 +337,7 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment "exclude_coin_amounts": [250000000000], "exclude_coins": [non_existent_coin.to_json_dict()], "reuse_puzhash": True, - "full_jsonify": True, + "chip-29": False, "push": True, }, ) From 622109356ae5b37faf9596708379986379e2b73e Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 20 Mar 2024 13:09:16 -0700 Subject: [PATCH 165/274] Better CLI mnemonic check --- chia/cmds/keys_funcs.py | 11 +++++++++-- chia/util/keychain.py | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 20f7dd06d713..f9c587806a21 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -17,7 +17,14 @@ from chia.util.errors import KeychainException from chia.util.file_keyring import MAX_LABEL_LENGTH from chia.util.ints import uint32 -from chia.util.keychain import Keychain, KeyData, bytes_to_mnemonic, generate_mnemonic, mnemonic_to_seed +from chia.util.keychain import ( + Keychain, + KeyData, + bytes_to_mnemonic, + check_mnemonic_validity, + generate_mnemonic, + mnemonic_to_seed, +) from chia.util.keyring_wrapper import KeyringWrapper from chia.wallet.derive_keys import ( master_pk_to_wallet_pk_unhardened, @@ -78,7 +85,7 @@ def add_key_info(mnemonic_or_pk: str, label: Optional[str]) -> None: """ unlock_keyring() try: - if mnemonic_or_pk.count(" ") == 23: + if check_mnemonic_validity(mnemonic_or_pk): sk = Keychain().add_key(mnemonic_or_pk, label, private=True) fingerprint = sk.get_g1().get_fingerprint() print(f"Added private key with public key fingerprint {fingerprint}") diff --git a/chia/util/keychain.py b/chia/util/keychain.py index a23149a731d4..c3d1f36d67c6 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -88,6 +88,11 @@ def bytes_to_mnemonic(mnemonic_bytes: bytes) -> str: return " ".join(mnemonics) +def check_mnemonic_validity(mnemonic_str: str) -> bool: + mnemonic: List[str] = mnemonic_str.split(" ") + return len(mnemonic) in [12, 15, 18, 21, 24] + + def mnemonic_from_short_words(mnemonic_str: str) -> str: """ Since the first 4 letters of each word is unique (or the full word, if less than 4 characters), and its common From 0ac8aef308943b000db42f8e5a766a8b163c3fb0 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 Mar 2024 15:29:07 -0700 Subject: [PATCH 166/274] black --- chia/util/observation_root.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/chia/util/observation_root.py b/chia/util/observation_root.py index 8e6db7100284..95a3901046db 100644 --- a/chia/util/observation_root.py +++ b/chia/util/observation_root.py @@ -4,12 +4,9 @@ class ObservationRoot(Protocol): - def get_fingerprint(self) -> int: - ... + def get_fingerprint(self) -> int: ... - def __bytes__(self) -> bytes: - ... + def __bytes__(self) -> bytes: ... @classmethod - def from_bytes(cls, blob: bytes) -> ObservationRoot: - ... + def from_bytes(cls, blob: bytes) -> ObservationRoot: ... From 3dc341d97d3c34df9ba37c8d6f2b4a856971d90e Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 Mar 2024 15:38:43 -0700 Subject: [PATCH 167/274] pylint --- chia/util/keychain.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index a060557d2d0e..222617044ef7 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -360,26 +360,30 @@ def _get_free_private_key_index(self) -> int: except KeychainUserNotFound: return index + # pylint requires these NotImplementedErrors for some reason @overload - def add_key(self, mnemonic_or_pk: str) -> Tuple[PrivateKey, KeyTypes]: ... + def add_key(self, mnemonic_or_pk: str) -> Tuple[PrivateKey, KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[PrivateKey, KeyTypes]: ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[PrivateKey, KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload - def add_key( - self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True] - ) -> Tuple[PrivateKey, KeyTypes]: ... + def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> Tuple[PrivateKey, KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False] - ) -> Tuple[ObservationRoot, KeyTypes]: ... + ) -> Tuple[ObservationRoot, KeyTypes]: + raise NotImplementedError() # pragma: no cover @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: bool - ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: ... + ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: + raise NotImplementedError() # pragma: no cover def add_key( self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True From 1fd43c1e4630dde9c6c17dcb605a4b70ebfc53fc Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 Mar 2024 15:50:35 -0700 Subject: [PATCH 168/274] Missed one --- chia/_tests/cmds/wallet/test_tx_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/cmds/wallet/test_tx_decorators.py b/chia/_tests/cmds/wallet/test_tx_decorators.py index bf2ab163979c..8a79e9507828 100644 --- a/chia/_tests/cmds/wallet/test_tx_decorators.py +++ b/chia/_tests/cmds/wallet/test_tx_decorators.py @@ -4,8 +4,8 @@ import click from click.testing import CliRunner -from tests.cmds.wallet.test_consts import STD_TX +from chia._tests.cmds.wallet.test_consts import STD_TX from chia.cmds.cmds_util import TransactionBundle, tx_out_cmd from chia.wallet.transaction_record import TransactionRecord From 917fe0f89a2614ecbb6be5e9ed9bdb8ad32791d2 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 Mar 2024 16:16:45 -0700 Subject: [PATCH 169/274] black --- chia/data_layer/data_layer_wallet.py | 34 +++++------ chia/pools/pool_wallet.py | 6 +- chia/wallet/wallet_protocol.py | 89 ++++++++++------------------ 3 files changed, 51 insertions(+), 78 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index bf73850a1388..ff89ec382643 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -274,9 +274,9 @@ async def track_new_launcher_id( await self.wallet_state_manager.add_interested_puzzle_hashes([launcher_id], [self.id()]) await self.wallet_state_manager.add_interested_coin_ids([new_singleton.name()]) - new_singleton_coin_record: Optional[ - WalletCoinRecord - ] = await self.wallet_state_manager.coin_store.get_coin_record(new_singleton.name()) + new_singleton_coin_record: Optional[WalletCoinRecord] = ( + await self.wallet_state_manager.coin_store.get_coin_record(new_singleton.name()) + ) while new_singleton_coin_record is not None and new_singleton_coin_record.spent_block_height > 0: # We've already synced this before, so we need to sort of force a resync parent_spend = await fetch_coin_spend(new_singleton_coin_record.spent_block_height, new_singleton, peer) @@ -416,10 +416,10 @@ async def create_update_state_spend( if root_hash is None: root_hash = singleton_record.root - inner_puzzle_derivation: Optional[ - DerivationRecord - ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( - singleton_record.inner_puzzle_hash + inner_puzzle_derivation: Optional[DerivationRecord] = ( + await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( + singleton_record.inner_puzzle_hash + ) ) if inner_puzzle_derivation is None: raise ValueError(f"DL Wallet does not have permission to update Singleton with launcher ID {launcher_id}") @@ -710,10 +710,10 @@ async def get_owned_singletons(self) -> List[SingletonRecord]: # this is likely due to a race between getting the list and acquiring the extra data continue - inner_puzzle_derivation: Optional[ - DerivationRecord - ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( - singleton_record.inner_puzzle_hash + inner_puzzle_derivation: Optional[DerivationRecord] = ( + await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( + singleton_record.inner_puzzle_hash + ) ) if inner_puzzle_derivation is not None: collected.append(singleton_record) @@ -756,9 +756,9 @@ async def delete_mirror( parent_coin: Coin = ( await self.wallet_state_manager.wallet_node.get_coin_state([mirror_coin.parent_coin_info], peer=peer) )[0].coin - inner_puzzle_derivation: Optional[ - DerivationRecord - ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(parent_coin.puzzle_hash) + inner_puzzle_derivation: Optional[DerivationRecord] = ( + await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(parent_coin.puzzle_hash) + ) if inner_puzzle_derivation is None: raise ValueError(f"DL Wallet does not have permission to delete mirror with ID {mirror_id}") @@ -967,9 +967,9 @@ async def potentially_handle_resubmit(self, launcher_id: bytes32) -> None: # pr # Let's check our standard wallet for fee transactions related to these dl txs all_spends: List[SpendBundle] = [tx.spend_bundle for tx in relevant_dl_txs if tx.spend_bundle is not None] all_removal_ids: Set[bytes32] = {removal.name() for sb in all_spends for removal in sb.removals()} - unconfirmed_std_txs: List[ - TransactionRecord - ] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.standard_wallet.id()) + unconfirmed_std_txs: List[TransactionRecord] = ( + await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.standard_wallet.id()) + ) relevant_std_txs: List[TransactionRecord] = [ tx for tx in unconfirmed_std_txs if any(c.name() in all_removal_ids for c in tx.removals) ] diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 6f43488ff9c8..78be32533c53 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -898,9 +898,9 @@ async def new_peak(self, peak_height: uint32) -> None: # Add some buffer (+2) to reduce chances of a reorg if peak_height > leave_height + 2: - unconfirmed: List[ - TransactionRecord - ] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id) + unconfirmed: List[TransactionRecord] = ( + await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id) + ) next_tip: Optional[Coin] = get_most_recent_singleton_coin_from_coin_spend(tip_spend) assert next_tip is not None diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 6a8db45537a7..873252ab4a08 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -38,50 +38,37 @@ class WalletProtocol(Protocol[T]): @classmethod - def type(cls) -> WalletType: - ... + def type(cls) -> WalletType: ... - def id(self) -> uint32: - ... + def id(self) -> uint32: ... - async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: Optional[T]) -> None: - ... + async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: Optional[T]) -> None: ... async def select_coins( self, amount: uint64, coin_selection_config: CoinSelectionConfig, - ) -> Set[Coin]: - ... + ) -> Set[Coin]: ... - async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint128: - ... + async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint128: ... - async def get_unconfirmed_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128: - ... + async def get_unconfirmed_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128: ... - async def get_spendable_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128: - ... + async def get_spendable_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128: ... - async def get_pending_change_balance(self) -> uint64: - ... + async def get_pending_change_balance(self) -> uint64: ... - async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128: - ... + async def get_max_send_amount(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128: ... # not all wallet supports this. To signal support, make # require_derivation_paths() return true - def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: - ... + def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: ... - def require_derivation_paths(self) -> bool: - ... + def require_derivation_paths(self) -> bool: ... - def get_name(self) -> str: - ... + def get_name(self) -> str: ... - async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: - ... + async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: ... wallet_info: WalletInfo # WalletStateManager is only imported for type hinting thus leaving pylint @@ -91,22 +78,18 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: class MainWalletProtocol(WalletProtocol[ClawbackMetadata], Protocol): @property - def max_send_quantity(self) -> int: - ... + def max_send_quantity(self) -> int: ... @staticmethod async def create( wallet_state_manager: Any, info: WalletInfo, name: str = ..., - ) -> MainWalletProtocol: - ... + ) -> MainWalletProtocol: ... - async def get_new_puzzle(self) -> Program: - ... + async def get_new_puzzle(self) -> Program: ... - async def get_new_puzzlehash(self) -> bytes32: - ... + async def get_new_puzzlehash(self) -> bytes32: ... # This isn't part of the WalletProtocol but it should be # Also this doesn't likely conform to the eventual one that ends up in WalletProtocol @@ -122,55 +105,45 @@ async def generate_signed_transaction( puzzle_decorator_override: Optional[List[Dict[str, Any]]] = None, extra_conditions: Tuple[Condition, ...] = tuple(), **kwargs: Unpack[GSTOptionalArgs], - ) -> List[TransactionRecord]: - ... + ) -> List[TransactionRecord]: ... - def puzzle_for_pk(self, pubkey: G1Element) -> Program: - ... + def puzzle_for_pk(self, pubkey: G1Element) -> Program: ... - async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: - ... + async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: ... - async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMode) -> Tuple[G1Element, G2Element]: - ... + async def sign_message( + self, message: str, puzzle_hash: bytes32, mode: SigningMode + ) -> Tuple[G1Element, G2Element]: ... - async def get_puzzle_hash(self, new: bool) -> bytes32: - ... + async def get_puzzle_hash(self, new: bool) -> bytes32: ... async def apply_signatures( self, spends: List[Spend], signing_responses: List[SigningResponse] - ) -> SignedTransaction: - ... + ) -> SignedTransaction: ... async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False - ) -> List[SigningResponse]: - ... + ) -> List[SigningResponse]: ... - async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: - ... + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: ... - async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: - ... + async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: ... async def create_tandem_xch_tx( self, fee: uint64, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> TransactionRecord: - ... + ) -> TransactionRecord: ... def make_solution( self, primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), - ) -> Program: - ... + ) -> Program: ... - async def get_puzzle(self, new: bool) -> Program: - ... + async def get_puzzle(self, new: bool) -> Program: ... class GSTOptionalArgs(TypedDict): From 48dd4299a339297db4cfe402085b2a178c46c75f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 07:08:59 -0700 Subject: [PATCH 170/274] Farm at least one block --- chia/_tests/wallet/test_main_wallet_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py index 3e5028b106b5..ed754f4d1943 100644 --- a/chia/_tests/wallet/test_main_wallet_protocol.py +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -231,7 +231,7 @@ async def bls_got_setup(wallet_environments: WalletTestFramework, monkeypatch: p [ { "num_environments": 1, - "blocks_needed": [0], + "blocks_needed": [1], } ], indirect=True, From cb6ab4f0ca12761b95c0b9d1184b12a4a703941f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 07:33:19 -0700 Subject: [PATCH 171/274] Revert last change and fix 0 block farming --- chia/_tests/wallet/conftest.py | 9 +++++---- chia/_tests/wallet/test_main_wallet_protocol.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chia/_tests/wallet/conftest.py b/chia/_tests/wallet/conftest.py index 5fc63153b620..24ac9b054391 100644 --- a/chia/_tests/wallet/conftest.py +++ b/chia/_tests/wallet/conftest.py @@ -165,10 +165,11 @@ async def wallet_environments( wallet_states: List[WalletState] = [] for service, blocks_needed in zip(wallet_services, request.param["blocks_needed"]): - await full_node[0]._api.farm_blocks_to_wallet( - count=blocks_needed, wallet=service._node.wallet_state_manager.main_wallet - ) - await full_node[0]._api.wait_for_wallet_synced(wallet_node=service._node, timeout=20) + if blocks_needed > 0: + await full_node[0]._api.farm_blocks_to_wallet( + count=blocks_needed, wallet=service._node.wallet_state_manager.main_wallet + ) + await full_node[0]._api.wait_for_wallet_synced(wallet_node=service._node, timeout=20) wallet_states.append( WalletState( Balance( diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py index ed754f4d1943..3e5028b106b5 100644 --- a/chia/_tests/wallet/test_main_wallet_protocol.py +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -231,7 +231,7 @@ async def bls_got_setup(wallet_environments: WalletTestFramework, monkeypatch: p [ { "num_environments": 1, - "blocks_needed": [1], + "blocks_needed": [0], } ], indirect=True, From 5fea5e583620672f0bfd50f68bf05c2f4ef9683f Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 08:11:05 -0700 Subject: [PATCH 172/274] black --- chia/wallet/util/clvm_streamable.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 723e30eb39cc..61ce9b4b995b 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -115,9 +115,9 @@ def parse(cls: Type[_T_ClvmStreamable], f: BinaryIO) -> _T_ClvmStreamable: assert isinstance(f, BytesIO) translation_layer: Optional[TranslationLayer] = _ClvmSerializationMode.get_config().translation_layer if translation_layer is not None: - cls_mapping: Optional[ - TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable] - ] = translation_layer.get_mapping(cls) + cls_mapping: Optional[TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = ( + translation_layer.get_mapping(cls) + ) if cls_mapping is not None: new_cls: Type[Union[_T_ClvmStreamable, ClvmStreamable]] = cls_mapping.to_type else: @@ -160,9 +160,9 @@ def override_json_serialization(self, default_recurse_jsonify: Callable[[Any], D def from_json_dict(cls: Type[_T_ClvmStreamable], json_dict: Any) -> _T_ClvmStreamable: translation_layer: Optional[TranslationLayer] = _ClvmSerializationMode.get_config().translation_layer if translation_layer is not None: - cls_mapping: Optional[ - TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable] - ] = translation_layer.get_mapping(cls) + cls_mapping: Optional[TranslationLayerMapping[_T_ClvmStreamable, ClvmStreamable]] = ( + translation_layer.get_mapping(cls) + ) if cls_mapping is not None: new_cls: Type[Union[_T_ClvmStreamable, ClvmStreamable]] = cls_mapping.to_type else: From 2481f78a5a682ced3b32a2ae451a5e7ad6dcae64 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 08:39:03 -0700 Subject: [PATCH 173/274] bad merge --- chia/_tests/cmds/test_cmd_framework.py | 6 +++--- chia/cmds/cmd_classes.py | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/chia/_tests/cmds/test_cmd_framework.py b/chia/_tests/cmds/test_cmd_framework.py index eb7fa8252555..c7904343908f 100644 --- a/chia/_tests/cmds/test_cmd_framework.py +++ b/chia/_tests/cmds/test_cmd_framework.py @@ -6,10 +6,10 @@ import click import pytest from click.testing import CliRunner -from tests.conftest import ConsensusMode -from tests.environments.wallet import WalletTestFramework -from tests.wallet.conftest import * # noqa +from chia._tests.conftest import ConsensusMode +from chia._tests.environments.wallet import WalletTestFramework +from chia._tests.wallet.conftest import * # noqa from chia.cmds.cmd_classes import ChiaCommand, Context, NeedsWalletRPC, chia_command, option from chia.types.blockchain_format.sized_bytes import bytes32 diff --git a/chia/cmds/cmd_classes.py b/chia/cmds/cmd_classes.py index 051641f689ce..c70c1e3b4cc7 100644 --- a/chia/cmds/cmd_classes.py +++ b/chia/cmds/cmd_classes.py @@ -34,13 +34,11 @@ class SyncChiaCommand(Protocol): - def run(self) -> None: - ... + def run(self) -> None: ... class AsyncChiaCommand(Protocol): - async def run(self) -> None: - ... + async def run(self) -> None: ... ChiaCommand = Union[SyncChiaCommand, AsyncChiaCommand] From 4003bca828cdee03ad18c10dab972436b841bce3 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 10:06:35 -0700 Subject: [PATCH 174/274] Merge fix --- chia/cmds/signer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/signer.py b/chia/cmds/signer.py index b4d2e5916dfd..759c67cee60b 100644 --- a/chia/cmds/signer.py +++ b/chia/cmds/signer.py @@ -254,7 +254,7 @@ async def run(self) -> None: replace( self.txs_in.transaction_bundle.txs[0], spend_bundle=new_spend_bundle, name=new_spend_bundle.name() ), - *(replace(tx, spend_bundle=None) for tx in self.txs_in.transaction_bundle.txs), + *(replace(tx, spend_bundle=None) for tx in self.txs_in.transaction_bundle.txs[1:]), ] self.txs_out.handle_transaction_output(new_transactions) From 9ba9060fd46ae4068fd3fa6a9bbcdf3f739fff59 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 10:38:42 -0700 Subject: [PATCH 175/274] Inadvertent changes --- chia/_tests/build-init-files.py | 0 chia/_tests/check_pytest_monitor_output.py | 0 chia/_tests/check_sql_statements.py | 0 chia/_tests/chia-start-sim | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 chia/_tests/build-init-files.py mode change 100644 => 100755 chia/_tests/check_pytest_monitor_output.py mode change 100644 => 100755 chia/_tests/check_sql_statements.py mode change 100644 => 100755 chia/_tests/chia-start-sim diff --git a/chia/_tests/build-init-files.py b/chia/_tests/build-init-files.py old mode 100644 new mode 100755 diff --git a/chia/_tests/check_pytest_monitor_output.py b/chia/_tests/check_pytest_monitor_output.py old mode 100644 new mode 100755 diff --git a/chia/_tests/check_sql_statements.py b/chia/_tests/check_sql_statements.py old mode 100644 new mode 100755 diff --git a/chia/_tests/chia-start-sim b/chia/_tests/chia-start-sim old mode 100644 new mode 100755 From eb62c8138da54808ac857aeb7c148a4cfaae139a Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 10:38:42 -0700 Subject: [PATCH 176/274] Inadvertent changes --- chia/_tests/build-init-files.py | 0 chia/_tests/check_pytest_monitor_output.py | 0 chia/_tests/check_sql_statements.py | 0 chia/_tests/chia-start-sim | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 chia/_tests/build-init-files.py mode change 100644 => 100755 chia/_tests/check_pytest_monitor_output.py mode change 100644 => 100755 chia/_tests/check_sql_statements.py mode change 100644 => 100755 chia/_tests/chia-start-sim diff --git a/chia/_tests/build-init-files.py b/chia/_tests/build-init-files.py old mode 100644 new mode 100755 diff --git a/chia/_tests/check_pytest_monitor_output.py b/chia/_tests/check_pytest_monitor_output.py old mode 100644 new mode 100755 diff --git a/chia/_tests/check_sql_statements.py b/chia/_tests/check_sql_statements.py old mode 100644 new mode 100755 diff --git a/chia/_tests/chia-start-sim b/chia/_tests/chia-start-sim old mode 100644 new mode 100755 From e62e08c98216e01a3dd739028fe2da1bfb6526e7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 27 Mar 2024 12:43:40 -0700 Subject: [PATCH 177/274] Syncronize with quex.tx_out_decorator2 --- chia/_tests/cmds/cmd_test_utils.py | 3 +- chia/_tests/cmds/wallet/test_coins.py | 3 + chia/_tests/cmds/wallet/test_dao.py | 8 + chia/_tests/cmds/wallet/test_did.py | 22 +- chia/_tests/cmds/wallet/test_nft.py | 14 +- chia/_tests/cmds/wallet/test_notifications.py | 6 +- chia/_tests/cmds/wallet/test_vcs.py | 30 ++- chia/_tests/cmds/wallet/test_wallet.py | 33 ++- chia/cmds/coin_funcs.py | 41 ++-- chia/cmds/coins.py | 20 +- chia/cmds/dao.py | 85 +++++--- chia/cmds/dao_funcs.py | 204 +++++++++++------- chia/cmds/data.py | 4 +- chia/cmds/plotnft.py | 4 +- chia/cmds/wallet.py | 140 +++++++----- chia/cmds/wallet_funcs.py | 192 ++++++++++++----- chia/rpc/wallet_rpc_api.py | 3 + chia/rpc/wallet_rpc_client.py | 20 ++ 18 files changed, 553 insertions(+), 279 deletions(-) diff --git a/chia/_tests/cmds/cmd_test_utils.py b/chia/_tests/cmds/cmd_test_utils.py index 7c2a6ae7c542..c46ad38792be 100644 --- a/chia/_tests/cmds/cmd_test_utils.py +++ b/chia/_tests/cmds/cmd_test_utils.py @@ -254,8 +254,9 @@ async def send_transaction_multi( tx_config: TXConfig, coins: Optional[List[Coin]] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> SendTransactionMultiResponse: - self.add_to_log("send_transaction_multi", (wallet_id, additions, tx_config, coins, fee)) + self.add_to_log("send_transaction_multi", (wallet_id, additions, tx_config, coins, fee, push)) name = bytes32([2] * 32) return SendTransactionMultiResponse( [STD_UTX], diff --git a/chia/_tests/cmds/wallet/test_coins.py b/chia/_tests/cmds/wallet/test_coins.py index ad2c1ae35ea7..12b88b187e61 100644 --- a/chia/_tests/cmds/wallet/test_coins.py +++ b/chia/_tests/cmds/wallet/test_coins.py @@ -135,6 +135,7 @@ async def select_coins( Coin(get_bytes32(3), get_bytes32(4), uint64(1234560000)), ], 1000000000, + True, ), ( 1, @@ -152,6 +153,7 @@ async def select_coins( Coin(get_bytes32(5), get_bytes32(6), uint64(300000000000)), ], 1000000000, + True, ), ], } @@ -213,6 +215,7 @@ async def get_coin_records_by_names( DEFAULT_TX_CONFIG, [Coin(get_bytes32(1), get_bytes32(2), uint64(100000000000))], 1000000000, + True, ) ], } diff --git a/chia/_tests/cmds/wallet/test_dao.py b/chia/_tests/cmds/wallet/test_dao.py index 4e014ec0bae6..00b56f8121fa 100644 --- a/chia/_tests/cmds/wallet/test_dao.py +++ b/chia/_tests/cmds/wallet/test_dao.py @@ -48,6 +48,7 @@ async def create_new_dao_wallet( name: Optional[str] = None, fee: uint64 = uint64(0), fee_for_cat: uint64 = uint64(0), + push: bool = True, ) -> CreateNewDAOWalletResponse: if not treasury_id: treasury_id = bytes32(token_bytes(32)) @@ -140,6 +141,7 @@ async def dao_add_funds_to_treasury( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOAddFundsToTreasuryResponse: return DAOAddFundsToTreasuryResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -278,6 +280,7 @@ async def dao_vote_on_proposal( tx_config: TXConfig, is_yes_vote: bool, fee: uint64 = uint64(0), + push: bool = True, ) -> DAOVoteOnProposalResponse: return DAOVoteOnProposalResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -289,6 +292,7 @@ async def dao_close_proposal( fee: uint64 = uint64(0), self_destruct: bool = False, reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOCloseProposalResponse: return DAOCloseProposalResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -306,6 +310,7 @@ async def dao_create_proposal( new_dao_rules: Optional[Dict[str, uint64]] = None, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOCreateProposalResponse: return DAOCreateProposalResponse([STD_UTX], [STD_TX], bytes32([0] * 32), STD_TX.name, STD_TX) @@ -489,6 +494,7 @@ async def dao_send_to_lockup( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOSendToLockupResponse: return DAOSendToLockupResponse([STD_UTX], [STD_TX], STD_TX.name, [STD_TX]) @@ -498,6 +504,7 @@ async def dao_free_coins_from_finished_proposals( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOFreeCoinsFromFinishedProposalsResponse: return DAOFreeCoinsFromFinishedProposalsResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -508,6 +515,7 @@ async def dao_exit_lockup( coins: Optional[List[Dict[str, Union[str, int]]]] = None, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOExitLockupResponse: return DAOExitLockupResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) diff --git a/chia/_tests/cmds/wallet/test_did.py b/chia/_tests/cmds/wallet/test_did.py index 190cff16f268..63072fba841d 100644 --- a/chia/_tests/cmds/wallet/test_did.py +++ b/chia/_tests/cmds/wallet/test_did.py @@ -32,10 +32,11 @@ async def create_new_did_wallet( name: Optional[str] = "DID Wallet", backup_ids: Optional[List[str]] = None, required_num: int = 0, + push: bool = True, ) -> Dict[str, Union[str, int]]: if backup_ids is None: backup_ids = [] - self.add_to_log("create_new_did_wallet", (amount, fee, name, backup_ids, required_num)) + self.add_to_log("create_new_did_wallet", (amount, fee, name, backup_ids, required_num, push)) return {"wallet_id": 3, "my_did": "did:chia:testdid123456"} inst_rpc_client = DidCreateRpcClient() # pylint: disable=no-value-for-parameter @@ -48,7 +49,7 @@ async def create_new_did_wallet( ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "create_new_did_wallet": [(3, 100000000000, "test", [], 0)], + "create_new_did_wallet": [(3, 100000000000, "test", [], 0, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -180,8 +181,9 @@ async def update_did_metadata( wallet_id: int, metadata: Dict[str, object], tx_config: TXConfig, + push: bool = True, ) -> DIDUpdateMetadataResponse: - self.add_to_log("update_did_metadata", (wallet_id, metadata, tx_config)) + self.add_to_log("update_did_metadata", (wallet_id, metadata, tx_config, push)) return DIDUpdateMetadataResponse([STD_UTX], [STD_TX], SpendBundle([], G2Element()), uint32(wallet_id)) inst_rpc_client = DidUpdateMetadataRpcClient() # pylint: disable=no-value-for-parameter @@ -203,7 +205,7 @@ async def update_did_metadata( assert_list = [f"Successfully updated DID wallet ID: {w_id}, Spend Bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "update_did_metadata": [(w_id, {"test": True}, DEFAULT_TX_CONFIG.override(reuse_puzhash=True))], + "update_did_metadata": [(w_id, {"test": True}, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -252,9 +254,9 @@ def test_did_message_spend(capsys: object, get_test_cli_clients: Tuple[TestRpcCl # set RPC Client class DidMessageSpendRpcClient(TestWalletRpcClient): async def did_message_spend( - self, wallet_id: int, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] + self, wallet_id: int, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...], push: bool ) -> DIDMessageSpendResponse: - self.add_to_log("did_message_spend", (wallet_id, tx_config, extra_conditions)) + self.add_to_log("did_message_spend", (wallet_id, tx_config, extra_conditions, True)) return DIDMessageSpendResponse([STD_UTX], [STD_TX], SpendBundle([], G2Element())) inst_rpc_client = DidMessageSpendRpcClient() # pylint: disable=no-value-for-parameter @@ -286,6 +288,7 @@ async def did_message_spend( *(CreateCoinAnnouncement(ann) for ann in c_announcements), *(CreatePuzzleAnnouncement(ann) for ann in puz_announcements), ), + True, ) ], } @@ -304,8 +307,9 @@ async def did_transfer_did( fee: int, with_recovery: bool, tx_config: TXConfig, + push: bool, ) -> DIDTransferDIDResponse: - self.add_to_log("did_transfer_did", (wallet_id, address, fee, with_recovery, tx_config)) + self.add_to_log("did_transfer_did", (wallet_id, address, fee, with_recovery, tx_config, push)) return DIDTransferDIDResponse( [STD_UTX], [STD_TX], @@ -340,6 +344,8 @@ async def did_transfer_did( ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "did_transfer_did": [(w_id, t_address, 500000000000, True, DEFAULT_TX_CONFIG.override(reuse_puzhash=True))], + "did_transfer_did": [ + (w_id, t_address, 500000000000, True, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), True) + ], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/_tests/cmds/wallet/test_nft.py b/chia/_tests/cmds/wallet/test_nft.py index b0056518c070..c40627601751 100644 --- a/chia/_tests/cmds/wallet/test_nft.py +++ b/chia/_tests/cmds/wallet/test_nft.py @@ -96,6 +96,7 @@ async def mint_nft( royalty_percentage: int = 0, did_id: Optional[str] = None, reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> NFTMintNFTResponse: self.add_to_log( "mint_nft", @@ -115,6 +116,7 @@ async def mint_nft( royalty_percentage, did_id, reuse_puzhash, + push, ), ) return NFTMintNFTResponse( @@ -171,6 +173,7 @@ async def mint_nft( 500000000000, 0, "0xcee228b8638c67cb66a55085be99fa3b457ae5b56915896f581990f600b2c652", + True, ) ], } @@ -190,8 +193,9 @@ async def add_uri_to_nft( uri: str, fee: int, tx_config: TXConfig, + push: bool, ) -> NFTAddURIResponse: - self.add_to_log("add_uri_to_nft", (wallet_id, nft_coin_id, key, uri, fee, tx_config)) + self.add_to_log("add_uri_to_nft", (wallet_id, nft_coin_id, key, uri, fee, tx_config, push)) return NFTAddURIResponse([STD_UTX], [STD_TX], uint32(wallet_id), SpendBundle([], G2Element())) inst_rpc_client = NFTAddUriRpcClient() # pylint: disable=no-value-for-parameter @@ -223,6 +227,7 @@ async def add_uri_to_nft( "https://example.com/nft", 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), + True, ) ], } @@ -241,8 +246,9 @@ async def transfer_nft( target_address: str, fee: int, tx_config: TXConfig, + push: bool, ) -> NFTTransferNFTResponse: - self.add_to_log("transfer_nft", (wallet_id, nft_coin_id, target_address, fee, tx_config)) + self.add_to_log("transfer_nft", (wallet_id, nft_coin_id, target_address, fee, tx_config, push)) return NFTTransferNFTResponse( [STD_UTX], [STD_TX], @@ -269,11 +275,11 @@ async def transfer_nft( ] # these are various things that should be in the output assert STD_TX.spend_bundle is not None - assert_list = [f"NFT transferred successfully with spend bundle: {STD_TX.spend_bundle.to_json_dict()}"] + assert_list = ["NFT transferred successfully", f"spend bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "transfer_nft": [ - (4, nft_coin_id, target_address, 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True)) + (4, nft_coin_id, target_address, 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), True) ], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/_tests/cmds/wallet/test_notifications.py b/chia/_tests/cmds/wallet/test_notifications.py index 4e161357559a..d0080e5a5161 100644 --- a/chia/_tests/cmds/wallet/test_notifications.py +++ b/chia/_tests/cmds/wallet/test_notifications.py @@ -21,9 +21,9 @@ def test_notifications_send(capsys: object, get_test_cli_clients: Tuple[TestRpcC # set RPC Client class NotificationsSendRpcClient(TestWalletRpcClient): async def send_notification( - self, target: bytes32, msg: bytes, amount: uint64, fee: uint64 = uint64(0) + self, target: bytes32, msg: bytes, amount: uint64, fee: uint64 = uint64(0), push: bool = True ) -> TransactionRecord: - self.add_to_log("send_notification", (target, msg, amount, fee)) + self.add_to_log("send_notification", (target, msg, amount, fee, push)) class FakeTransactionRecord: def __init__(self, name: str) -> None: @@ -53,7 +53,7 @@ def __init__(self, name: str) -> None: ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "send_notification": [(target_ph, bytes(msg, "utf8"), 20000000, 1000000000)], + "send_notification": [(target_ph, bytes(msg, "utf8"), 20000000, 1000000000, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/_tests/cmds/wallet/test_vcs.py b/chia/_tests/cmds/wallet/test_vcs.py index 398f2695472d..6f96b2f5bc1e 100644 --- a/chia/_tests/cmds/wallet/test_vcs.py +++ b/chia/_tests/cmds/wallet/test_vcs.py @@ -31,8 +31,9 @@ async def vc_mint( tx_config: TXConfig, target_address: Optional[bytes32] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> VCMintResponse: - self.add_to_log("vc_mint", (did_id, tx_config, target_address, fee)) + self.add_to_log("vc_mint", (did_id, tx_config, target_address, fee, push)) return VCMintResponse( [STD_UTX], @@ -64,7 +65,7 @@ async def vc_mint( f"Transaction {get_bytes32(2).hex()}", ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) - expected_calls: logType = {"vc_mint": [(did_bytes, DEFAULT_TX_CONFIG, target_bytes, 500000000000)]} + expected_calls: logType = {"vc_mint": [(did_bytes, DEFAULT_TX_CONFIG, target_bytes, 500000000000, True)]} test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -117,8 +118,11 @@ async def vc_spend( new_proof_hash: Optional[bytes32] = None, provider_inner_puzhash: Optional[bytes32] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> VCSpendResponse: - self.add_to_log("vc_spend", (vc_id, tx_config, new_puzhash, new_proof_hash, provider_inner_puzhash, fee)) + self.add_to_log( + "vc_spend", (vc_id, tx_config, new_puzhash, new_proof_hash, provider_inner_puzhash, fee, push) + ) return VCSpendResponse([STD_UTX], [STD_TX]) inst_rpc_client = VcsUpdateProofsRpcClient() # pylint: disable=no-value-for-parameter @@ -145,7 +149,15 @@ async def vc_spend( run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "vc_spend": [ - (vc_bytes, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), target_ph, new_proof, None, uint64(500000000000)) + ( + vc_bytes, + DEFAULT_TX_CONFIG.override(reuse_puzhash=True), + target_ph, + new_proof, + None, + uint64(500000000000), + True, + ) ] } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -219,8 +231,9 @@ async def vc_revoke( vc_parent_id: bytes32, tx_config: TXConfig, fee: uint64 = uint64(0), + push: bool = True, ) -> VCRevokeResponse: - self.add_to_log("vc_revoke", (vc_parent_id, tx_config, fee)) + self.add_to_log("vc_revoke", (vc_parent_id, tx_config, fee, push)) return VCRevokeResponse([STD_UTX], [STD_TX]) inst_rpc_client = VcsRevokeRpcClient() # pylint: disable=no-value-for-parameter @@ -242,8 +255,8 @@ async def vc_revoke( expected_calls: logType = { "vc_get": [(vc_id,)], "vc_revoke": [ - (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000)), - (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000)), + (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000), True), + (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000), True), ], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -260,6 +273,7 @@ async def crcat_approve_pending( min_amount_to_claim: uint64, tx_config: TXConfig, fee: uint64 = uint64(0), + push: bool = True, ) -> List[TransactionRecord]: self.add_to_log( "crcat_approve_pending", @@ -268,6 +282,7 @@ async def crcat_approve_pending( min_amount_to_claim, tx_config, fee, + push, ), ) return [STD_TX] @@ -305,6 +320,7 @@ async def crcat_approve_pending( reuse_puzhash=True, ), uint64(500000000000), + True, ) ], "get_wallets": [(None,)], diff --git a/chia/_tests/cmds/wallet/test_wallet.py b/chia/_tests/cmds/wallet/test_wallet.py index cef2d09e28ec..f3e3a7151365 100644 --- a/chia/_tests/cmds/wallet/test_wallet.py +++ b/chia/_tests/cmds/wallet/test_wallet.py @@ -507,10 +507,21 @@ async def spend_clawback_coins( coin_ids: List[bytes32], fee: int = 0, force: bool = False, + push: bool = True, ) -> Dict[str, Any]: - self.add_to_log("spend_clawback_coins", (coin_ids, fee, force)) + self.add_to_log("spend_clawback_coins", (coin_ids, fee, force, push)) tx_hex_list = [get_bytes32(6).hex(), get_bytes32(7).hex(), get_bytes32(8).hex()] - return {"transaction_ids": tx_hex_list} + return { + "transaction_ids": tx_hex_list, + "transactions": [ + STD_TX.to_json_dict_convenience( + { + "selected_network": "mainnet", + "network_overrides": {"config": {"mainnet": {"address_prefix": "xch"}}}, + } + ) + ], + } inst_rpc_client = ClawbackWalletRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -528,7 +539,7 @@ async def spend_clawback_coins( run_cli_command_and_assert(capsys, root_dir, command_args, ["transaction_ids", str(r_tx_ids_hex)]) # these are various things that should be in the output expected_calls: logType = { - "spend_clawback_coins": [(tx_ids, 1000000000000, False)], + "spend_clawback_coins": [(tx_ids, 1000000000000, False, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -926,8 +937,9 @@ async def take_offer( tx_config: TXConfig, solver: Optional[Dict[str, Any]] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> TakeOfferResponse: - self.add_to_log("take_offer", (offer, tx_config, solver, fee)) + self.add_to_log("take_offer", (offer, tx_config, solver, fee, push)) return TakeOfferResponse( [STD_UTX], [STD_TX], @@ -970,7 +982,7 @@ async def take_offer( (cat2,), (bytes32.from_hexstr("accce8e1c71b56624f2ecaeff5af57eac41365080449904d0717bd333c04806d"),), ], - "take_offer": [(Offer.from_bech32(test_offer_file_bech32), DEFAULT_TX_CONFIG, None, 1000000000000)], + "take_offer": [(Offer.from_bech32(test_offer_file_bech32), DEFAULT_TX_CONFIG, None, 1000000000000, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -999,9 +1011,14 @@ async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> Tra ) async def cancel_offer( - self, trade_id: bytes32, tx_config: TXConfig, fee: uint64 = uint64(0), secure: bool = True + self, + trade_id: bytes32, + tx_config: TXConfig, + fee: uint64 = uint64(0), + secure: bool = True, + push: bool = True, ) -> CancelOfferResponse: - self.add_to_log("cancel_offer", (trade_id, tx_config, fee, secure)) + self.add_to_log("cancel_offer", (trade_id, tx_config, fee, secure, push)) return CancelOfferResponse([STD_UTX], [STD_TX]) inst_rpc_client = CancelOfferRpcClient() # pylint: disable=no-value-for-parameter @@ -1022,7 +1039,7 @@ async def cancel_offer( run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "get_offer": [(test_offer_id_bytes, True)], - "cancel_offer": [(test_offer_id_bytes, DEFAULT_TX_CONFIG, 1000000000000, True)], + "cancel_offer": [(test_offer_id_bytes, DEFAULT_TX_CONFIG, 1000000000000, True, True)], "cat_asset_id_to_name": [ (cat1,), (cat2,), diff --git a/chia/cmds/coin_funcs.py b/chia/cmds/coin_funcs.py index f7893518bbe5..8aeff605686c 100644 --- a/chia/cmds/coin_funcs.py +++ b/chia/cmds/coin_funcs.py @@ -124,7 +124,8 @@ async def async_combine( target_coin_amount: Decimal, target_coin_ids_str: Sequence[str], largest_first: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): target_coin_ids: List[bytes32] = [bytes32.from_hexstr(coin_id) for coin_id in target_coin_ids_str] final_fee = uint64(int(fee * units["chia"])) @@ -135,10 +136,10 @@ async def async_combine( mojo_per_unit = get_mojo_per_unit(wallet_type) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return + return [] if not await wallet_client.get_synced(): print("Wallet not synced. Please wait.") - return + return [] is_xch: bool = wallet_type == WalletType.STANDARD_WALLET # this lets us know if we are directly combining Chia tx_config = CMDTXConfigLoader( @@ -165,10 +166,10 @@ async def async_combine( conf_coins = [cr for cr in conf_coins if cr.name in target_coin_ids] if len(conf_coins) == 0: print("No coins to combine.") - return + return [] if len(conf_coins) == 1: print("Only one coin found, you need at least two coins to combine.") - return + return [] if largest_first: conf_coins.sort(key=lambda r: r.coin.amount, reverse=True) else: @@ -181,15 +182,18 @@ async def async_combine( total_amount: uint128 = uint128(sum(coin.amount for coin in removals)) if is_xch and total_amount - final_fee <= 0: print("Total amount is less than 0 after fee, exiting.") - return + return [] target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, False)) additions = [{"amount": (total_amount - final_fee) if is_xch else total_amount, "puzzle_hash": target_ph}] transaction: TransactionRecord = ( - await wallet_client.send_transaction_multi(wallet_id, additions, tx_config, removals, final_fee) + await wallet_client.send_transaction_multi(wallet_id, additions, tx_config, removals, final_fee, push=push) ).transaction tx_id = transaction.name.hex() - print(f"Transaction sent: {tx_id}") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + if push: + print(f"Transaction sent: {tx_id}") + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + + return [transaction] async def async_split( @@ -202,22 +206,23 @@ async def async_split( amount_per_coin: Decimal, target_coin_id_str: str, # TODO: [add TXConfig args] -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): final_fee = uint64(int(fee * units["chia"])) target_coin_id: bytes32 = bytes32.from_hexstr(target_coin_id_str) if number_of_coins > 500: print(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.") - return + return [] try: wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(wallet_type) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return + return [] if not await wallet_client.get_synced(): print("Wallet not synced. Please wait.") - return + return [] is_xch: bool = wallet_type == WalletType.STANDARD_WALLET # this lets us know if we are directly spitting Chia final_amount_per_coin = uint64(int(amount_per_coin * mojo_per_unit)) total_amount = final_amount_per_coin * number_of_coins @@ -231,7 +236,7 @@ async def async_split( f"is less than the total amount of the split: {total_amount / mojo_per_unit}, exiting." ) print("Try using a smaller fee or amount.") - return + return [] additions: List[Dict[str, Union[uint64, bytes32]]] = [] for i in range(number_of_coins): # for readability. # we always use new addresses @@ -244,12 +249,13 @@ async def async_split( transaction: TransactionRecord = ( await wallet_client.send_transaction_multi( - wallet_id, additions, tx_config, [removal_coin_record.coin], final_fee + wallet_id, additions, tx_config, [removal_coin_record.coin], final_fee, push=push ) ).transaction tx_id = transaction.name.hex() - print(f"Transaction sent: {tx_id}") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + if push: + print(f"Transaction sent: {tx_id}") + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") dust_threshold = config.get("xch_spam_amount", 1000000) # min amount per coin in mojo spam_filter_after_n_txs = config.get("spam_filter_after_n_txs", 200) # how many txs to wait before filtering if final_amount_per_coin < dust_threshold and wallet_type == WalletType.STANDARD_WALLET: @@ -259,3 +265,4 @@ async def async_split( f"{'will' if number_of_coins > spam_filter_after_n_txs else 'may'} not show up in your wallet unless " f"you decrease the dust limit to below {final_amount_per_coin} mojos or disable it by setting it to 0." ) + return [transaction] diff --git a/chia/cmds/coins.py b/chia/cmds/coins.py index 9eba3617a5dc..a2c789e75928 100644 --- a/chia/cmds/coins.py +++ b/chia/cmds/coins.py @@ -2,11 +2,13 @@ import asyncio from decimal import Decimal -from typing import Optional, Sequence +from typing import List, Optional, Sequence import click from chia.cmds import options +from chia.cmds.cmds_util import tx_out_cmd +from chia.wallet.transaction_record import TransactionRecord @click.group("coins", help="Manage your wallets coins") @@ -85,7 +87,6 @@ def list_cmd( ) -# MARK: tx_endpoint @coins_cmd.command("combine", help="Combine dust coins") @click.option( "-p", @@ -151,6 +152,7 @@ def list_cmd( default=False, help="Sort coins from largest to smallest or smallest to largest.", ) +@tx_out_cmd def combine_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -163,10 +165,11 @@ def combine_cmd( fee: str, input_coins: Sequence[str], largest_first: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .coin_funcs import async_combine - asyncio.run( + return asyncio.run( async_combine( wallet_rpc_port=wallet_rpc_port, fingerprint=fingerprint, @@ -179,11 +182,11 @@ def combine_cmd( target_coin_amount=Decimal(target_amount), target_coin_ids_str=input_coins, largest_first=largest_first, + push=push, ) ) -# MARK: tx_endpoint @coins_cmd.command("split", help="Split up larger coins") @click.option( "-p", @@ -218,6 +221,7 @@ def combine_cmd( required=True, ) @click.option("-t", "--target-coin-id", type=str, required=True, help="The coin id of the coin we are splitting.") +@tx_out_cmd def split_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -226,10 +230,11 @@ def split_cmd( fee: str, amount_per_coin: str, target_coin_id: str, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .coin_funcs import async_split - asyncio.run( + return asyncio.run( async_split( wallet_rpc_port=wallet_rpc_port, fingerprint=fingerprint, @@ -238,5 +243,6 @@ def split_cmd( number_of_coins=number_of_coins, amount_per_coin=Decimal(amount_per_coin), target_coin_id_str=target_coin_id, + push=push, ) ) diff --git a/chia/cmds/dao.py b/chia/cmds/dao.py index 103b522bc328..0ab570d7ad30 100644 --- a/chia/cmds/dao.py +++ b/chia/cmds/dao.py @@ -1,12 +1,13 @@ from __future__ import annotations import asyncio -from typing import Optional, Sequence +from typing import List, Optional, Sequence import click -from chia.cmds.cmds_util import tx_config_args +from chia.cmds.cmds_util import tx_config_args, tx_out_cmd from chia.cmds.plotnft import validate_fee +from chia.wallet.transaction_record import TransactionRecord @click.group("dao", short_help="Create, manage or show state of DAOs", no_args_is_help=True) @@ -66,7 +67,6 @@ def dao_add_cmd( # CREATE -# MARK: tx_endpoint @dao_cmd.command("create", short_help="Create a new DAO wallet and treasury", no_args_is_help=True) @click.option( "-wp", @@ -156,6 +156,7 @@ def dao_add_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -176,7 +177,8 @@ def dao_create_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_dao_wallet if self_destruct == proposal_timelock: @@ -202,8 +204,9 @@ def dao_create_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_dao_wallet(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_dao_wallet(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- @@ -233,7 +236,6 @@ def dao_get_id_cmd( asyncio.run(get_treasury_id(extra_params, wallet_rpc_port, fingerprint)) -# MARK: tx_endpoint @dao_cmd.command("add_funds", short_help="Send funds to a DAO treasury", no_args_is_help=True) @click.option( "-wp", @@ -268,6 +270,7 @@ def dao_get_id_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_add_funds_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -280,7 +283,8 @@ def dao_add_funds_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import add_funds_to_treasury extra_params = { @@ -293,8 +297,9 @@ def dao_add_funds_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(add_funds_to_treasury(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(add_funds_to_treasury(extra_params, wallet_rpc_port, fingerprint)) @dao_cmd.command("balance", short_help="Get the asset balances for a DAO treasury", no_args_is_help=True) @@ -417,7 +422,6 @@ def dao_show_proposal_cmd( # VOTE -# MARK: tx_endpoint @dao_cmd.command("vote", short_help="Vote on a DAO proposal", no_args_is_help=True) @click.option( "-wp", @@ -458,6 +462,7 @@ def dao_show_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_vote_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -471,7 +476,8 @@ def dao_vote_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import vote_on_proposal is_yes_vote = False if vote_no else True @@ -487,15 +493,15 @@ def dao_vote_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(vote_on_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(vote_on_proposal(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- # CLOSE PROPOSALS -# MARK: tx_endpoint @dao_cmd.command("close_proposal", short_help="Close a DAO proposal", no_args_is_help=True) @click.option( "-wp", @@ -530,6 +536,7 @@ def dao_vote_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_close_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -542,7 +549,8 @@ def dao_close_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import close_proposal extra_params = { @@ -555,15 +563,15 @@ def dao_close_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(close_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(close_proposal(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- # LOCKUP COINS -# MARK: tx_endpoint @dao_cmd.command("lockup_coins", short_help="Lock DAO CATs for voting", no_args_is_help=True) @click.option( "-wp", @@ -591,6 +599,7 @@ def dao_close_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_lockup_coins_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -602,7 +611,8 @@ def dao_lockup_coins_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import lockup_coins extra_params = { @@ -614,11 +624,11 @@ def dao_lockup_coins_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(lockup_coins(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(lockup_coins(extra_params, wallet_rpc_port, fingerprint)) -# MARK: tx_endpoint @dao_cmd.command("release_coins", short_help="Release closed proposals from DAO CATs", no_args_is_help=True) @click.option( "-wp", @@ -639,6 +649,7 @@ def dao_lockup_coins_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_release_coins_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -649,7 +660,8 @@ def dao_release_coins_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: from .dao_funcs import release_coins extra_params = { @@ -660,11 +672,11 @@ def dao_release_coins_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(release_coins(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(release_coins(extra_params, wallet_rpc_port, fingerprint)) -# MARK: tx_endpoint @dao_cmd.command("exit_lockup", short_help="Release DAO CATs from voting mode", no_args_is_help=True) @click.option( "-wp", @@ -685,6 +697,7 @@ def dao_release_coins_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_exit_lockup_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -695,7 +708,8 @@ def dao_exit_lockup_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import exit_lockup extra_params = { @@ -706,8 +720,9 @@ def dao_exit_lockup_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(exit_lockup(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(exit_lockup(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- @@ -720,7 +735,6 @@ def dao_proposal(ctx: click.Context) -> None: pass -# MARK: tx_endpoint @dao_proposal.command("spend", short_help="Create a proposal to spend DAO funds", no_args_is_help=True) @click.option( "-wp", @@ -780,6 +794,7 @@ def dao_proposal(ctx: click.Context) -> None: callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_spend_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -795,7 +810,8 @@ def dao_create_spend_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_spend_proposal extra_params = { @@ -811,11 +827,11 @@ def dao_create_spend_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_spend_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_spend_proposal(extra_params, wallet_rpc_port, fingerprint)) -# MARK: tx_endpoint @dao_proposal.command("update", short_help="Create a proposal to change the DAO rules", no_args_is_help=True) @click.option( "-wp", @@ -886,6 +902,7 @@ def dao_create_spend_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_update_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -903,7 +920,8 @@ def dao_create_update_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_update_proposal extra_params = { @@ -921,11 +939,11 @@ def dao_create_update_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_update_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_update_proposal(extra_params, wallet_rpc_port, fingerprint)) -# MARK: tx_endpoint @dao_proposal.command("mint", short_help="Create a proposal to mint new DAO CATs", no_args_is_help=True) @click.option( "-wp", @@ -969,6 +987,7 @@ def dao_create_update_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_mint_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -982,7 +1001,8 @@ def dao_create_mint_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_mint_proposal extra_params = { @@ -996,8 +1016,9 @@ def dao_create_mint_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_mint_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_mint_proposal(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- diff --git a/chia/cmds/dao_funcs.py b/chia/cmds/dao_funcs.py index 357e2d790e45..c3e396e04c1f 100644 --- a/chia/cmds/dao_funcs.py +++ b/chia/cmds/dao_funcs.py @@ -4,7 +4,7 @@ import json import time from decimal import Decimal -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client, transaction_status_msg, transaction_submitted_msg from chia.cmds.units import units @@ -13,6 +13,7 @@ from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.config import selected_network_address_prefix from chia.util.ints import uint64 +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG from chia.wallet.util.wallet_types import WalletType @@ -45,7 +46,7 @@ async def add_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], f print(f"DAOCAT Wallet ID: {res.dao_cat_wallet_id}") -async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: proposal_minimum = uint64(int(Decimal(args["proposal_minimum_amount"]) * units["chia"])) if proposal_minimum % 2 == 0: @@ -95,13 +96,16 @@ async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int] "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - print("Successfully created DAO Wallet") + if args["push"]: + print("Successfully created DAO Wallet") print(f"DAO Treasury ID: {res.treasury_id.hex()}") print(f"DAO Wallet ID: {res.wallet_id}") print(f"CAT Wallet ID: {res.cat_wallet_id}") print(f"DAOCAT Wallet ID: {res.dao_cat_wallet_id}") + return res.transactions async def get_treasury_id(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -123,7 +127,9 @@ async def get_rules(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: in print(f"{rule}: {val}") -async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def add_funds_to_treasury( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] funding_wallet_id = args["funding_wallet_id"] amount = Decimal(args["amount"]) @@ -134,7 +140,7 @@ async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[ mojo_per_unit = get_mojo_per_unit(typ) except LookupError: # pragma: no cover print(f"Wallet id: {wallet_id} not found.") - return + return [] fee = Decimal(args["fee"]) final_fee: uint64 = uint64(int(fee * units["chia"])) @@ -154,18 +160,22 @@ async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[ "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover async def get_treasury_balance(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -283,7 +293,7 @@ async def show_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp print(f"Address: {address}") -async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] vote_amount = args["vote_amount"] fee = args["fee"] @@ -307,20 +317,24 @@ async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover - - -async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover + + +async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -341,20 +355,25 @@ async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], f "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover -async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] amount = args["amount"] final_amount: uint64 = uint64(int(Decimal(amount) * units["cat"])) @@ -374,20 +393,25 @@ async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover -async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + +async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -404,19 +428,24 @@ async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover - - -async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover + + +async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -434,19 +463,27 @@ async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover - - -async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover + + +async def create_spend_proposal( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -489,15 +526,20 @@ async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[ "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) asset_id_name = asset_id if asset_id else "XCH" print(f"Created spend proposal for asset: {asset_id_name}") - print("Successfully created proposal.") + if args["push"]: + print("Successfully created proposal.") print(f"Proposal ID: {res.proposal_id.hex()}") + return res.transactions -async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def create_update_proposal( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = Decimal(args["fee"]) final_fee: uint64 = uint64(int(fee * units["chia"])) @@ -532,13 +574,18 @@ async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - print("Successfully created proposal.") + if args["push"]: + print("Successfully created proposal.") print(f"Proposal ID: {res.proposal_id.hex()}") + return res.transactions -async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def create_mint_proposal( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -562,7 +609,10 @@ async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[i "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - print("Successfully created proposal.") + if args["push"]: + print("Successfully created proposal.") print(f"Proposal ID: {res.proposal_id.hex()}") + return res.transactions diff --git a/chia/cmds/data.py b/chia/cmds/data.py index 65dc32c9c2c7..5b80f2b3f66c 100644 --- a/chia/cmds/data.py +++ b/chia/cmds/data.py @@ -107,7 +107,6 @@ def create_fee_option() -> Callable[[FC], FC]: ) -# MARK: tx_endpoint def create_page_option() -> Callable[[FC], FC]: return click.option( "-p", @@ -127,6 +126,9 @@ def create_max_page_size_option() -> Callable[[FC], FC]: ) +# Functions with this mark in this file are not being ported to @tx_out_cmd due to API peculiarities +# They will therefore not work with observer-only functionality +# MARK: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @data_cmd.command("create_data_store", help="Create a new data store") @create_rpc_port_option() @create_fee_option() diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py index 7d9e1c733033..5c3acdda7f1e 100644 --- a/chia/cmds/plotnft.py +++ b/chia/cmds/plotnft.py @@ -53,7 +53,9 @@ def get_login_link_cmd(launcher_id: str) -> None: asyncio.run(get_login_link(launcher_id)) -# MARK: tx_endpoint +# Functions with this mark in this file are not being ported to @tx_out_cmd due to lack of observer key support +# They will therefore not work with observer-only functionality +# MARK: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @plotnft_cmd.command("create", help="Create a plot NFT") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @options.create_fingerprint() diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 89c517c2dc75..f2cd4f8724b2 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -134,7 +134,6 @@ def get_transactions_cmd( ) -# MARK: tx_endpoint @wallet_cmd.command("send", help="Send chia to another wallet") @click.option( "-wp", @@ -282,7 +281,6 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id: int, fingerprint: int, n asyncio.run(get_address(wallet_rpc_port, fingerprint, id, new_address)) -# MARK: tx_endpoint @wallet_cmd.command( "clawback", help="Claim or revert a Clawback transaction." @@ -315,14 +313,15 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id: int, fingerprint: int, n is_flag=True, default=False, ) +@tx_out_cmd def clawback( - wallet_rpc_port: Optional[int], id: int, fingerprint: int, tx_ids: str, fee: str, force: bool -) -> None: # pragma: no cover + wallet_rpc_port: Optional[int], id: int, fingerprint: int, tx_ids: str, fee: str, force: bool, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import spend_clawback - asyncio.run( + return asyncio.run( spend_clawback( - wallet_rpc_port=wallet_rpc_port, fp=fingerprint, fee=Decimal(fee), tx_ids_str=tx_ids, force=force + wallet_rpc_port=wallet_rpc_port, fp=fingerprint, fee=Decimal(fee), tx_ids_str=tx_ids, force=force, push=push ) ) @@ -429,7 +428,6 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str asyncio.run(add_token(wallet_rpc_port, fingerprint, asset_id, token_name)) -# MARK: tx_endpoint @wallet_cmd.command("make_offer", help="Create an offer of XCH/CATs/NFTs for XCH/CATs/NFTs") @click.option( "-wp", @@ -469,6 +467,8 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str default=False, ) @click.option("--override", help="Creates offer without checking for unusual values", is_flag=True, default=False) +# This command looks like a good candidate for @tx_out_cmd however, pushing an incomplete tx is nonsensical and +# we already have a canonical offer file format which the idea of exporting a different transaction conflicts with def make_offer_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -546,7 +546,6 @@ def get_offers_cmd( ) -# MARK: tx_endpoint @wallet_cmd.command("take_offer", help="Examine or take an offer") @click.argument("path_or_hex", type=str, nargs=1, required=True) @click.option( @@ -567,20 +566,21 @@ def get_offers_cmd( is_flag=True, default=False, ) +@tx_out_cmd def take_offer_cmd( path_or_hex: str, wallet_rpc_port: Optional[int], fingerprint: int, examine_only: bool, fee: str, - reuse: bool, -) -> None: + reuse: bool, # reuse is not used + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import take_offer - asyncio.run(take_offer(wallet_rpc_port, fingerprint, Decimal(fee), path_or_hex, examine_only)) # reuse is not used + return asyncio.run(take_offer(wallet_rpc_port, fingerprint, Decimal(fee), path_or_hex, examine_only, push=push)) -# MARK: tx_endpoint @wallet_cmd.command("cancel_offer", help="Cancel an existing offer") @click.option( "-wp", @@ -595,10 +595,13 @@ def take_offer_cmd( @click.option( "-m", "--fee", help="The fee to use when cancelling the offer securely, in XCH", default="0", show_default=True ) -def cancel_offer_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: str, insecure: bool, fee: str) -> None: +@tx_out_cmd +def cancel_offer_cmd( + wallet_rpc_port: Optional[int], fingerprint: int, id: str, insecure: bool, fee: str, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import cancel_offer - asyncio.run(cancel_offer(wallet_rpc_port, fingerprint, Decimal(fee), id, not insecure)) + return asyncio.run(cancel_offer(wallet_rpc_port, fingerprint, Decimal(fee), id, not insecure, push=push)) @wallet_cmd.command("check", short_help="Check wallet DB integrity", help=check_help_text) @@ -620,7 +623,6 @@ def did_cmd() -> None: pass -# MARK: tx_endpoint @did_cmd.command("create", help="Create DID wallet") @click.option( "-wp", @@ -648,12 +650,13 @@ def did_cmd() -> None: show_default=True, callback=validate_fee, ) +@tx_out_cmd def did_create_wallet_cmd( - wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: int, fee: str -) -> None: + wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: int, fee: str, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import create_did_wallet - asyncio.run(create_did_wallet(wallet_rpc_port, fingerprint, Decimal(fee), name, amount)) + return asyncio.run(create_did_wallet(wallet_rpc_port, fingerprint, Decimal(fee), name, amount, push=push)) @did_cmd.command("sign_message", help="Sign a message by a DID") @@ -731,7 +734,6 @@ def did_get_details_cmd(wallet_rpc_port: Optional[int], fingerprint: int, coin_i asyncio.run(get_did_info(wallet_rpc_port, fingerprint, coin_id, latest)) -# MARK: tx_endpoint @did_cmd.command("update_metadata", help="Update the metadata of a DID") @click.option( "-wp", @@ -749,12 +751,13 @@ def did_get_details_cmd(wallet_rpc_port: Optional[int], fingerprint: int, coin_i is_flag=True, default=False, ) +@tx_out_cmd def did_update_metadata_cmd( - wallet_rpc_port: Optional[int], fingerprint: int, id: int, metadata: str, reuse: bool -) -> None: + wallet_rpc_port: Optional[int], fingerprint: int, id: int, metadata: str, reuse: bool, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import update_did_metadata - asyncio.run(update_did_metadata(wallet_rpc_port, fingerprint, id, metadata, reuse)) + return asyncio.run(update_did_metadata(wallet_rpc_port, fingerprint, id, metadata, reuse, push=push)) @did_cmd.command("find_lost", help="Find the did you should own and recovery the DID wallet") @@ -805,7 +808,6 @@ def did_find_lost_cmd( ) -# MARK: tx_endpoint @did_cmd.command("message_spend", help="Generate a DID spend bundle for announcements") @click.option( "-wp", @@ -830,13 +832,15 @@ def did_find_lost_cmd( type=str, required=False, ) +@tx_out_cmd def did_message_spend_cmd( wallet_rpc_port: Optional[int], fingerprint: int, id: int, puzzle_announcements: Optional[str], coin_announcements: Optional[str], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import did_message_spend puzzle_list: List[str] = [] @@ -849,7 +853,7 @@ def did_message_spend_cmd( bytes.fromhex(announcement) except ValueError: print("Invalid puzzle announcement format, should be a list of hex strings.") - return + return [] if coin_announcements is not None: try: coin_list = coin_announcements.split(",") @@ -858,12 +862,11 @@ def did_message_spend_cmd( bytes.fromhex(announcement) except ValueError: print("Invalid coin announcement format, should be a list of hex strings.") - return + return [] - asyncio.run(did_message_spend(wallet_rpc_port, fingerprint, id, puzzle_list, coin_list)) + return asyncio.run(did_message_spend(wallet_rpc_port, fingerprint, id, puzzle_list, coin_list, push=push)) -# MARK: tx_endpoint @did_cmd.command("transfer", help="Transfer a DID") @click.option( "-wp", @@ -893,6 +896,7 @@ def did_message_spend_cmd( is_flag=True, default=False, ) +@tx_out_cmd def did_transfer_did( wallet_rpc_port: Optional[int], fingerprint: int, @@ -901,10 +905,11 @@ def did_transfer_did( reset_recovery: bool, fee: str, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import transfer_did - asyncio.run( + return asyncio.run( transfer_did( wallet_rpc_port, fingerprint, @@ -913,6 +918,7 @@ def did_transfer_did( target_address, reset_recovery is False, True if reuse else None, + push=push, ) ) @@ -966,7 +972,6 @@ def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: s ) -# MARK: tx_endpoint @nft_cmd.command("mint", help="Mint an NFT") @click.option( "-wp", @@ -1011,6 +1016,7 @@ def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: s is_flag=True, default=False, ) +@tx_out_cmd def nft_mint_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1029,7 +1035,8 @@ def nft_mint_cmd( fee: str, royalty_percentage_fraction: int, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import mint_nft if metadata_uris is None: @@ -1042,7 +1049,7 @@ def nft_mint_cmd( else: license_uris_list = [lu.strip() for lu in license_uris.split(",")] - asyncio.run( + return asyncio.run( mint_nft( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1061,11 +1068,11 @@ def nft_mint_cmd( d_fee=Decimal(fee), royalty_percentage=royalty_percentage_fraction, reuse_puzhash=True if reuse else None, + push=push, ) ) -# MARK: tx_endpoint @nft_cmd.command("add_uri", help="Add an URI to an NFT") @click.option( "-wp", @@ -1095,6 +1102,7 @@ def nft_mint_cmd( is_flag=True, default=False, ) +@tx_out_cmd def nft_add_uri_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1105,10 +1113,11 @@ def nft_add_uri_cmd( license_uri: str, fee: str, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import add_uri_to_nft - asyncio.run( + return asyncio.run( add_uri_to_nft( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1119,11 +1128,11 @@ def nft_add_uri_cmd( metadata_uri=metadata_uri, license_uri=license_uri, reuse_puzhash=True if reuse else None, + push=push, ) ) -# MARK: tx_endpoint @nft_cmd.command("transfer", help="Transfer an NFT") @click.option( "-wp", @@ -1151,6 +1160,7 @@ def nft_add_uri_cmd( is_flag=True, default=False, ) +@tx_out_cmd def nft_transfer_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1159,10 +1169,11 @@ def nft_transfer_cmd( target_address: str, fee: str, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import transfer_nft - asyncio.run( + return asyncio.run( transfer_nft( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1171,6 +1182,7 @@ def nft_transfer_cmd( nft_coin_id=nft_coin_id, target_address=target_address, reuse_puzhash=True if reuse else None, + push=push, ) ) @@ -1273,7 +1285,6 @@ def notification_cmd() -> None: pass -# MARK: tx_endpoint @notification_cmd.command("send", help="Send a notification to the owner of an address") @click.option( "-wp", @@ -1295,6 +1306,7 @@ def notification_cmd() -> None: ) @click.option("-n", "--message", help="The message of the notification", type=str) @click.option("-m", "--fee", help="The fee for the transaction, in XCH", type=str) +@tx_out_cmd def send_notification_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1302,10 +1314,13 @@ def send_notification_cmd( amount: str, message: str, fee: str, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import send_notification - asyncio.run(send_notification(wallet_rpc_port, fingerprint, Decimal(fee), to_address, message, Decimal(amount))) + return asyncio.run( + send_notification(wallet_rpc_port, fingerprint, Decimal(fee), to_address, message, Decimal(amount), push=push) + ) @notification_cmd.command("get", help="Get notification(s) that are in your wallet") @@ -1359,7 +1374,6 @@ def vcs_cmd() -> None: # pragma: no cover pass -# MARK: tx_endpoint @vcs_cmd.command("mint", short_help="Mint a VC") @click.option( "-wp", @@ -1372,16 +1386,18 @@ def vcs_cmd() -> None: # pragma: no cover @click.option("-d", "--did", help="The DID of the VC's proof provider", type=str, required=True) @click.option("-t", "--target-address", help="The address to send the VC to once it's minted", type=str, required=False) @click.option("-m", "--fee", help="Blockchain fee for mint transaction, in XCH", type=str, required=False, default="0") +@tx_out_cmd def mint_vc_cmd( wallet_rpc_port: Optional[int], fingerprint: int, did: str, target_address: Optional[str], fee: str, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import mint_vc - asyncio.run(mint_vc(wallet_rpc_port, fingerprint, did, Decimal(fee), target_address)) + return asyncio.run(mint_vc(wallet_rpc_port, fingerprint, did, Decimal(fee), target_address, push=push)) @vcs_cmd.command("get", short_help="Get a list of existing VCs") @@ -1410,7 +1426,6 @@ def get_vcs_cmd( asyncio.run(get_vcs(wallet_rpc_port, fingerprint, start, count)) -# MARK: tx_endpoint @vcs_cmd.command("update_proofs", short_help="Update a VC's proofs if you have the provider DID") @click.option( "-wp", @@ -1438,6 +1453,7 @@ def get_vcs_cmd( default=False, show_default=True, ) +@tx_out_cmd def spend_vc_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1446,10 +1462,11 @@ def spend_vc_cmd( new_proof_hash: str, fee: str, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import spend_vc - asyncio.run( + return asyncio.run( spend_vc( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1458,6 +1475,7 @@ def spend_vc_cmd( new_puzhash=new_puzhash, new_proof_hash=new_proof_hash, reuse_puzhash=reuse_puzhash, + push=push, ) ) @@ -1504,7 +1522,6 @@ def get_proofs_for_root_cmd( asyncio.run(get_proofs_for_root(wallet_rpc_port, fingerprint, proof_hash)) -# MARK: tx_endpoint @vcs_cmd.command("revoke", short_help="Revoke any VC if you have the proper DID and the VCs parent coin") @click.option( "-wp", @@ -1537,6 +1554,7 @@ def get_proofs_for_root_cmd( default=False, show_default=True, ) +@tx_out_cmd def revoke_vc_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1544,13 +1562,15 @@ def revoke_vc_cmd( vc_id: Optional[str], fee: str, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import revoke_vc - asyncio.run(revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, Decimal(fee), reuse_puzhash)) + return asyncio.run( + revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, Decimal(fee), reuse_puzhash, push=push) + ) -# MARK: tx_endpoint @vcs_cmd.command("approve_r_cats", help="Claim any R-CATs that are currently pending VC approval") @click.option( "-wp", @@ -1575,6 +1595,7 @@ def revoke_vc_cmd( is_flag=True, default=False, ) +@tx_out_cmd def approve_r_cats_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1584,11 +1605,20 @@ def approve_r_cats_cmd( min_coin_amount: Optional[Decimal], max_coin_amount: Optional[Decimal], reuse: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import approve_r_cats - asyncio.run( + return asyncio.run( approve_r_cats( - wallet_rpc_port, fingerprint, id, min_amount_to_claim, Decimal(fee), min_coin_amount, max_coin_amount, reuse + wallet_rpc_port, + fingerprint, + id, + min_amount_to_claim, + Decimal(fee), + min_coin_amount, + max_coin_amount, + reuse, + push, ) ) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index aec281030c56..a601c42f58e2 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -285,19 +285,19 @@ async def send( f"A transaction of amount {amount} and fee {fee} is unusual.\n" f"Pass in --override if you are sure you mean to do this." ) - return [] # pragma: no cover + return [] if amount == 0: print("You can not send an empty transaction") - return [] # pragma: no cover + return [] if clawback_time_lock < 0: print("Clawback time lock seconds cannot be negative.") - return [] # pragma: no cover + return [] try: typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(typ) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return [] # pragma: no cover + return [] final_fee: uint64 = uint64(int(fee * units["chia"])) # fees are always in XCH mojos final_amount: uint64 = uint64(int(amount * mojo_per_unit)) @@ -340,7 +340,7 @@ async def send( ) else: print("Only standard wallet and CAT wallets are supported") - return [] # pragma: no cover + return [] tx_id = res.transaction.name if push: @@ -695,7 +695,8 @@ async def take_offer( d_fee: Decimal, file: str, examine_only: bool, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if os.path.exists(file): filepath = pathlib.Path(file) @@ -711,7 +712,7 @@ async def take_offer( offer = Offer.from_bech32(offer_hex) except ValueError: print("Please enter a valid offer file or hex blob") - return + return [] offered, requested, _, _ = offer.summary() cat_name_resolver = wallet_client.cat_asset_id_to_name @@ -776,15 +777,21 @@ async def take_offer( if not examine_only: print() cli_confirm("Would you like to take this offer? (y/n): ") - trade_record = ( - await wallet_client.take_offer( - offer, - fee=fee, - tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + res = await wallet_client.take_offer( + offer, + fee=fee, + tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + push=push, + ) + if push: + print(f"Accepted offer with ID {res.trade_record.trade_id}") + print( + f"Use chia wallet get_offers --id {res.trade_record.trade_id} -f {fingerprint} to view its status" ) - ).trade_record - print(f"Accepted offer with ID {trade_record.trade_id}") - print(f"Use chia wallet get_offers --id {trade_record.trade_id} -f {fingerprint} to view its status") + + return res.transactions + else: + return [] async def cancel_offer( @@ -793,7 +800,8 @@ async def cancel_offer( d_fee: Decimal, offer_id_hex: str, secure: bool, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): offer_id = bytes32.from_hexstr(offer_id_hex) fee: int = int(d_fee * units["chia"]) @@ -802,13 +810,20 @@ async def cancel_offer( await print_trade_record(trade_record, wallet_client, summaries=True) cli_confirm(f"Are you sure you wish to cancel offer with ID: {trade_record.trade_id}? (y/n): ") - await wallet_client.cancel_offer( - offer_id, CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), secure=secure, fee=fee + res = await wallet_client.cancel_offer( + offer_id, + CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + secure=secure, + fee=fee, + push=push, ) - print(f"Cancelled offer with ID {trade_record.trade_id}") - if secure: + if push or not secure: + print(f"Cancelled offer with ID {trade_record.trade_id}") + if secure and push: print(f"Use chia wallet get_offers --id {trade_record.trade_id} -f {fingerprint} to view cancel status") + return res.transactions + def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]: # pragma: no cover if typ in {WalletType.CAT, WalletType.CRCAT}: @@ -908,18 +923,20 @@ async def print_balances( async def create_did_wallet( - wallet_rpc_port: Optional[int], fp: Optional[int], d_fee: Decimal, name: Optional[str], amount: int -) -> None: + wallet_rpc_port: Optional[int], fp: Optional[int], d_fee: Decimal, name: Optional[str], amount: int, push: bool +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): fee: int = int(d_fee * units["chia"]) try: - response = await wallet_client.create_new_did_wallet(amount, fee, name) + response = await wallet_client.create_new_did_wallet(amount, fee, name, push=push) wallet_id = response["wallet_id"] my_did = response["my_did"] print(f"Successfully created a DID wallet with name {name} and id {wallet_id} on key {fingerprint}") print(f"Successfully created a DID {my_did} in the newly created DID wallet") + return [] # TODO: fix this endpoint to return transactions except Exception as e: print(f"Failed to create DID wallet: {e}") + return [] async def did_set_wallet_name(wallet_rpc_port: Optional[int], fp: Optional[int], wallet_id: int, name: str) -> None: @@ -970,7 +987,8 @@ async def update_did_metadata( did_wallet_id: int, metadata: str, reuse_puzhash: bool, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: response = await wallet_client.update_did_metadata( @@ -980,12 +998,15 @@ async def update_did_metadata( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), ) - print( - f"Successfully updated DID wallet ID: {response.wallet_id}, " - f"Spend Bundle: {response.spend_bundle.to_json_dict()}" - ) + if push: + print( + f"Successfully updated DID wallet ID: {response.wallet_id}, " + f"Spend Bundle: {response.spend_bundle.to_json_dict()}" + ) + return response.transactions except Exception as e: print(f"Failed to update DID metadata: {e}") + return [] async def did_message_spend( @@ -994,7 +1015,8 @@ async def did_message_spend( did_wallet_id: int, puzzle_announcements: List[str], coin_announcements: List[str], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: response = await wallet_client.did_message_spend( @@ -1004,10 +1026,13 @@ async def did_message_spend( *(CreateCoinAnnouncement(hexstr_to_bytes(ca)) for ca in coin_announcements), *(CreatePuzzleAnnouncement(hexstr_to_bytes(pa)) for pa in puzzle_announcements), ), + push=push, ) print(f"Message Spend Bundle: {response.spend_bundle.to_json_dict()}") + return response.transactions except Exception as e: print(f"Failed to update DID metadata: {e}") + return [] async def transfer_did( @@ -1018,7 +1043,8 @@ async def transfer_did( target_address: str, with_recovery: bool, reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: fee: int = int(d_fee * units["chia"]) async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): @@ -1031,12 +1057,16 @@ async def transfer_did( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) - print(f"Successfully transferred DID to {target_address}") + if push: + print(f"Successfully transferred DID to {target_address}") print(f"Transaction ID: {response.transaction_id.hex()}") print(f"Transaction: {response.transaction.to_json_dict_convenience(config)}") + return response.transactions except Exception as e: print(f"Failed to transfer DID: {e}") + return [] async def find_lost_did( @@ -1095,7 +1125,8 @@ async def mint_nft( d_fee: Decimal, royalty_percentage: int, reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): royalty_address = ( None @@ -1141,11 +1172,15 @@ async def mint_nft( fee, royalty_percentage, did_id, + push=push, ) spend_bundle = mint_response.spend_bundle - print(f"NFT minted Successfully with spend bundle: {spend_bundle}") + if push: + print(f"NFT minted Successfully with spend bundle: {spend_bundle}") + return mint_response.transactions except Exception as e: print(f"Failed to mint NFT: {e}") + return [] async def add_uri_to_nft( @@ -1159,7 +1194,8 @@ async def add_uri_to_nft( metadata_uri: Optional[str], license_uri: Optional[str], reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: if len([x for x in (uri, metadata_uri, license_uri) if x is not None]) > 1: @@ -1185,11 +1221,15 @@ async def add_uri_to_nft( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) spend_bundle = response.spend_bundle.to_json_dict() - print(f"URI added successfully with spend bundle: {spend_bundle}") + if push: + print(f"URI added successfully with spend bundle: {spend_bundle}") + return response.transactions except Exception as e: print(f"Failed to add URI to NFT: {e}") + return [] async def transfer_nft( @@ -1201,7 +1241,8 @@ async def transfer_nft( nft_coin_id: str, target_address: str, reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: target_address = ensure_valid_address(target_address, allowed_types={AddressType.XCH}, config=config) @@ -1214,11 +1255,16 @@ async def transfer_nft( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) spend_bundle = response.spend_bundle.to_json_dict() - print(f"NFT transferred successfully with spend bundle: {spend_bundle}") + if push: + print("NFT transferred successfully") + print(f"spend bundle: {spend_bundle}") + return response.transactions except Exception as e: print(f"Failed to transfer NFT: {e}") + return [] def print_nft_info(nft: NFTInfo, *, config: Dict[str, Any]) -> None: @@ -1377,17 +1423,20 @@ async def send_notification( address_str: str, message_hex: str, d_amount: Decimal, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): address: bytes32 = decode_puzzle_hash(address_str) amount: uint64 = uint64(d_amount * units["chia"]) message: bytes = bytes(message_hex, "utf8") fee: uint64 = uint64(d_fee * units["chia"]) - tx = await wallet_client.send_notification(address, message, amount, fee) + tx = await wallet_client.send_notification(address, message, amount, fee, push=push) - print("Notification sent successfully.") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}") + if push: + print("Notification sent successfully.") + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}") + return [tx] async def get_notifications( @@ -1457,25 +1506,37 @@ async def sign_message( async def spend_clawback( - *, wallet_rpc_port: Optional[int], fp: Optional[int], fee: Decimal, tx_ids_str: str, force: bool = False -) -> None: # pragma: no cover + *, + wallet_rpc_port: Optional[int], + fp: Optional[int], + fee: Decimal, + tx_ids_str: str, + force: bool = False, + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _): tx_ids = [] for tid in tx_ids_str.split(","): tx_ids.append(bytes32.from_hexstr(tid)) if len(tx_ids) == 0: print("Transaction ID is required.") - return + return [] if fee < 0: print("Batch fee cannot be negative.") - return - response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"]), force) + return [] + response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"]), force, push=push) print(str(response)) + return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] async def mint_vc( - wallet_rpc_port: Optional[int], fp: Optional[int], did: str, d_fee: Decimal, target_address: Optional[str] -) -> None: # pragma: no cover + wallet_rpc_port: Optional[int], + fp: Optional[int], + did: str, + d_fee: Decimal, + target_address: Optional[str], + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): res = await wallet_client.vc_mint( decode_puzzle_hash(ensure_valid_address(did, allowed_types={AddressType.DID}, config=config)), @@ -1488,9 +1549,11 @@ async def mint_vc( ) ), uint64(int(d_fee * units["chia"])), + push=push, ) - print(f"New VC with launcher ID minted: {res.vc_record.vc.launcher_id.hex()}") + if push: + print(f"New VC with launcher ID minted: {res.vc_record.vc.launcher_id.hex()}") print("Relevant TX records:") print("") for tx in res.transactions: @@ -1501,6 +1564,7 @@ async def mint_vc( address_prefix=selected_network_address_prefix(config), mojo_per_unit=get_mojo_per_unit(wallet_type=WalletType.STANDARD_WALLET), ) + return res.transactions async def get_vcs( @@ -1537,7 +1601,8 @@ async def spend_vc( new_puzhash: Optional[str], new_proof_hash: str, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): txs = ( await wallet_client.vc_spend( @@ -1548,10 +1613,12 @@ async def spend_vc( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) ).transactions - print("Proofs successfully updated!") + if push: + print("Proofs successfully updated!") print("Relevant TX records:") print("") for tx in txs: @@ -1562,6 +1629,7 @@ async def spend_vc( address_prefix=selected_network_address_prefix(config), mojo_per_unit=get_mojo_per_unit(wallet_type=WalletType.STANDARD_WALLET), ) + return txs async def add_proof_reveal( @@ -1599,16 +1667,17 @@ async def revoke_vc( vc_id: Optional[str], fee: Decimal, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if parent_coin_id is None: if vc_id is None: print("Must specify either --parent-coin-id or --vc-id") - return + return [] record = await wallet_client.vc_get(bytes32.from_hexstr(vc_id)) if record is None: print(f"Cannot find a VC with ID {vc_id}") - return + return [] parent_id: bytes32 = bytes32(record.vc.coin.parent_coin_info) else: parent_id = bytes32.from_hexstr(parent_coin_id) @@ -1619,10 +1688,12 @@ async def revoke_vc( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) ).transactions - print("VC successfully revoked!") + if push: + print("VC successfully revoked!") print("Relevant TX records:") print("") for tx in txs: @@ -1633,6 +1704,7 @@ async def revoke_vc( address_prefix=selected_network_address_prefix(config), mojo_per_unit=get_mojo_per_unit(wallet_type=WalletType.STANDARD_WALLET), ) + return txs async def approve_r_cats( @@ -1644,7 +1716,8 @@ async def approve_r_cats( min_coin_amount: Optional[Decimal], max_coin_amount: Optional[Decimal], reuse: bool, -) -> None: # pragma: no cover + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): if wallet_client is None: return @@ -1657,9 +1730,11 @@ async def approve_r_cats( max_coin_amount=None if max_coin_amount is None else str(max_coin_amount), reuse_puzhash=reuse, ).to_tx_config(units["cat"], config, fingerprint), + push=push, ) - print("VC successfully approved R-CATs!") + if push: + print("VC successfully approved R-CATs!") print("Relevant TX records:") print("") for tx in txs: @@ -1674,7 +1749,7 @@ async def approve_r_cats( ) except LookupError as e: print(e.args[0]) - return + return txs print_transaction( tx, @@ -1683,3 +1758,4 @@ async def approve_r_cats( address_prefix=selected_network_address_prefix(config), mojo_per_unit=mojo_per_unit, ) + return txs diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 6f449ae0f01f..0c8c3c69c7d9 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -772,6 +772,9 @@ async def create_new_wallet( if type(request["metadata"]) is dict: metadata = request["metadata"] + if not push: + raise ValueError("Creation of DID wallet must be automatically pushed for now.") + async with self.service.wallet_state_manager.lock: did_wallet_name: str = request.get("wallet_name", None) if did_wallet_name is not None: diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index fec4d3ed5426..4211bc0f9f00 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -298,6 +298,7 @@ async def spend_clawback_coins( coin_ids: List[bytes32], fee: int = 0, force: bool = False, + push: bool = True, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), ) -> Dict[str, Any]: @@ -306,6 +307,7 @@ async def spend_clawback_coins( "fee": fee, "force": force, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **timelock_info.to_json_dict(), } response = await self.fetch("spend_clawback_coins", request) @@ -410,6 +412,7 @@ async def create_new_did_wallet( name: Optional[str] = "DID Wallet", backup_ids: List[str] = [], required_num: int = 0, + push: bool = True, ) -> Dict[str, Any]: request = { "wallet_type": "did_wallet", @@ -419,6 +422,7 @@ async def create_new_did_wallet( "amount": amount, "fee": fee, "wallet_name": name, + "push": push, } response = await self.fetch("create_new_wallet", request) return response @@ -1276,6 +1280,7 @@ async def send_notification( fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> TransactionRecord: response = await self.fetch( "send_notification", @@ -1285,6 +1290,7 @@ async def send_notification( "amount": amount, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **timelock_info.to_json_dict(), }, ) @@ -1311,6 +1317,7 @@ async def create_new_dao_wallet( fee: uint64 = uint64(0), fee_for_cat: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> CreateNewDAOWalletResponse: request: Dict[str, Any] = { "wallet_type": "dao_wallet", @@ -1323,6 +1330,7 @@ async def create_new_dao_wallet( "fee": fee, "fee_for_cat": fee_for_cat, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("create_new_wallet", request) @@ -1352,6 +1360,7 @@ async def dao_create_proposal( new_dao_rules: Optional[Dict[str, Optional[uint64]]] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOCreateProposalResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1390,6 +1399,7 @@ async def dao_vote_on_proposal( is_yes_vote: bool = True, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOVoteOnProposalResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1398,6 +1408,7 @@ async def dao_vote_on_proposal( "is_yes_vote": is_yes_vote, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_vote_on_proposal", request) @@ -1416,6 +1427,7 @@ async def dao_close_proposal( self_destruct: Optional[bool] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOCloseProposalResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1423,6 +1435,7 @@ async def dao_close_proposal( "self_destruct": self_destruct, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_close_proposal", request) @@ -1434,6 +1447,7 @@ async def dao_free_coins_from_finished_proposals( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOFreeCoinsFromFinishedProposalsResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1457,6 +1471,7 @@ async def dao_add_funds_to_treasury( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOAddFundsToTreasuryResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1464,6 +1479,7 @@ async def dao_add_funds_to_treasury( "amount": amount, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_add_funds_to_treasury", request) @@ -1476,12 +1492,14 @@ async def dao_send_to_lockup( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOSendToLockupResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "amount": amount, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_send_to_lockup", request) @@ -1494,12 +1512,14 @@ async def dao_exit_lockup( coins: Optional[List[Dict[str, Any]]] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOExitLockupResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "coins": coins, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_exit_lockup", request) From 98dda39f07c8c70c7dbc8b15cdaae6ef53a96a5e Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Thu, 28 Mar 2024 17:21:37 +0100 Subject: [PATCH 178/274] These are no longer dictionaries. --- .../wallet/dao_wallet/test_dao_wallets.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/chia/_tests/wallet/dao_wallet/test_dao_wallets.py b/chia/_tests/wallet/dao_wallet/test_dao_wallets.py index 216ff4bb5960..3bdd375904e8 100644 --- a/chia/_tests/wallet/dao_wallet/test_dao_wallets.py +++ b/chia/_tests/wallet/dao_wallet/test_dao_wallets.py @@ -1668,7 +1668,7 @@ async def test_dao_rpc_client( fee = uint64(10000) # create new dao - dao_wallet_dict_0 = await client_0.create_new_dao_wallet( + dao_wallet_res_0 = await client_0.create_new_dao_wallet( mode="new", tx_config=DEFAULT_TX_CONFIG, dao_rules=dao_rules.to_json_dict(), @@ -1676,8 +1676,8 @@ async def test_dao_rpc_client( filter_amount=filter_amount, name="DAO WALLET 0", ) - dao_id_0 = dao_wallet_dict_0.wallet_id - cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.cat_wallet_id] + dao_id_0 = dao_wallet_res_0.wallet_id + cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_res_0.cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) @@ -1698,15 +1698,15 @@ async def test_dao_rpc_client( await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) # join dao - dao_wallet_dict_1 = await client_1.create_new_dao_wallet( + dao_wallet_res_1 = await client_1.create_new_dao_wallet( mode="existing", tx_config=DEFAULT_TX_CONFIG, - treasury_id=dao_wallet_dict_0.treasury_id, + treasury_id=dao_wallet_res_0.treasury_id, filter_amount=filter_amount, name="DAO WALLET 1", ) - dao_id_1 = dao_wallet_dict_1.wallet_id - cat_wallet_1 = wallet_node_1.wallet_state_manager.wallets[dao_wallet_dict_1.cat_wallet_id] + dao_id_1 = dao_wallet_res_1.wallet_id + cat_wallet_1 = wallet_node_1.wallet_state_manager.wallets[dao_wallet_res_1.cat_wallet_id] # fund treasury xch_funds = uint64(10000000000) @@ -1737,7 +1737,7 @@ async def test_dao_rpc_client( # send cats to wallet 1 await client_0.cat_spend( - wallet_id=dao_wallet_dict_0.cat_wallet_id, + wallet_id=dao_wallet_res_0.cat_wallet_id, tx_config=DEFAULT_TX_CONFIG, amount=cat_amt, inner_address=encode_puzzle_hash(ph_1, "xch"), @@ -1916,7 +1916,7 @@ async def test_dao_rpc_client( await rpc_state( 20, client_1.get_wallet_balance, - [dao_wallet_dict_1.cat_wallet_id], + [dao_wallet_res_1.cat_wallet_id], lambda x: x["confirmed_wallet_balance"], 100, ) @@ -1953,7 +1953,7 @@ async def test_dao_rpc_client( await full_node_api.process_all_wallet_transactions(wallet_0, timeout=60) await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) - bal = await client_0.get_wallet_balance(dao_wallet_dict_0.dao_cat_wallet_id) + bal = await client_0.get_wallet_balance(dao_wallet_res_0.dao_cat_wallet_id) assert bal["confirmed_wallet_balance"] == cat_amt exit = await client_0.dao_exit_lockup(dao_id_0, tx_config=DEFAULT_TX_CONFIG) @@ -1965,14 +1965,14 @@ async def test_dao_rpc_client( await rpc_state( 20, client_0.get_wallet_balance, - [dao_wallet_dict_0.cat_wallet_id], + [dao_wallet_res_0.cat_wallet_id], lambda x: x["confirmed_wallet_balance"], cat_amt, ) # coverage tests for filter amount and get treasury id treasury_id_resp = await client_0.dao_get_treasury_id(wallet_id=dao_id_0) - assert treasury_id_resp["treasury_id"] == "0x" + dao_wallet_dict_0.treasury_id.hex() + assert treasury_id_resp["treasury_id"] == "0x" + dao_wallet_res_0.treasury_id.hex() filter_amount_resp = await client_0.dao_adjust_filter_level(wallet_id=dao_id_0, filter_level=30) assert filter_amount_resp["dao_info"]["filter_below_vote_amount"] == 30 @@ -2056,7 +2056,7 @@ async def test_dao_complex_spends( filter_amount = uint64(1) # create new dao - dao_wallet_dict_0 = await client_0.create_new_dao_wallet( + dao_wallet_res_0 = await client_0.create_new_dao_wallet( mode="new", tx_config=DEFAULT_TX_CONFIG, dao_rules=dao_rules.to_json_dict(), @@ -2064,9 +2064,9 @@ async def test_dao_complex_spends( filter_amount=filter_amount, name="DAO WALLET 0", ) - dao_id_0 = dao_wallet_dict_0.wallet_id - treasury_id = dao_wallet_dict_0.treasury_id - cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.cat_wallet_id] + dao_id_0 = dao_wallet_res_0.wallet_id + treasury_id = dao_wallet_res_0.treasury_id + cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_res_0.cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() await full_node_api.wait_transaction_records_entered_mempool(records=txs, timeout=60) @@ -2097,14 +2097,14 @@ async def test_dao_complex_spends( await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) # join dao - dao_wallet_dict_1 = await client_1.create_new_dao_wallet( + dao_wallet_res_1 = await client_1.create_new_dao_wallet( mode="existing", tx_config=DEFAULT_TX_CONFIG, treasury_id=treasury_id, filter_amount=filter_amount, name="DAO WALLET 1", ) - dao_id_1 = dao_wallet_dict_1.wallet_id + dao_id_1 = dao_wallet_res_1.wallet_id # fund treasury so there are multiple coins for each asset xch_funds = uint64(10000000000) @@ -2645,7 +2645,7 @@ async def test_dao_cat_exits( # create new dao await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_0, wallet_node_1], timeout=30) - dao_wallet_dict_0 = await client_0.create_new_dao_wallet( + dao_wallet_res_0 = await client_0.create_new_dao_wallet( mode="new", tx_config=DEFAULT_TX_CONFIG, dao_rules=dao_rules.to_json_dict(), @@ -2653,9 +2653,9 @@ async def test_dao_cat_exits( filter_amount=filter_amount, name="DAO WALLET 0", ) - dao_id_0 = dao_wallet_dict_0.wallet_id - cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.cat_wallet_id] - dao_cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_dict_0.dao_cat_wallet_id] + dao_id_0 = dao_wallet_res_0.wallet_id + cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_res_0.cat_wallet_id] + dao_cat_wallet_0 = wallet_node_0.wallet_state_manager.wallets[dao_wallet_res_0.dao_cat_wallet_id] txs = await wallet_0.wallet_state_manager.tx_store.get_all_unconfirmed() for tx in txs: await full_node_api.wait_transaction_records_entered_mempool(records=[tx], timeout=60) From 57b9eecfe6e80be918139e71fe53c002e58f9438 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 28 Mar 2024 11:40:58 -0700 Subject: [PATCH 179/274] Use new clvm_streamable pattern --- chia/rpc/wallet_rpc_client.py | 61 ++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index fec4d3ed5426..a8ffe94a22e9 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -263,7 +263,7 @@ async def send_transaction( if memos is not None: request["memos"] = memos response = await self.fetch("send_transaction", request) - return SendTransactionResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, SendTransactionResponse) async def send_transaction_multi( self, @@ -291,7 +291,7 @@ async def send_transaction_multi( coins_json = [c.to_json_dict() for c in coins] request["coins"] = coins_json response = await self.fetch("send_transaction_multi", request) - return SendTransactionMultiResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, SendTransactionMultiResponse) async def spend_clawback_coins( self, @@ -362,7 +362,7 @@ async def create_signed_transactions( request["wallet_id"] = wallet_id response = await self.fetch("create_signed_transaction", request) - return CreateSignedTransactionsResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, CreateSignedTransactionsResponse) async def select_coins(self, amount: int, wallet_id: int, coin_selection_config: CoinSelectionConfig) -> List[Coin]: request = {"amount": amount, "wallet_id": wallet_id, **coin_selection_config.to_json_dict()} @@ -458,7 +458,7 @@ async def update_did_recovery_list( **timelock_info.to_json_dict(), } response = await self.fetch("did_update_recovery_ids", request) - return DIDUpdateRecoveryIDsResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DIDUpdateRecoveryIDsResponse) async def get_did_recovery_list(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -481,7 +481,7 @@ async def did_message_spend( **timelock_info.to_json_dict(), } response = await self.fetch("did_message_spend", request) - return DIDMessageSpendResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DIDMessageSpendResponse) async def update_did_metadata( self, @@ -501,7 +501,7 @@ async def update_did_metadata( **timelock_info.to_json_dict(), } response = await self.fetch("did_update_metadata", request) - return DIDUpdateMetadataResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DIDUpdateMetadataResponse) async def get_did_metadata(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -579,7 +579,7 @@ async def did_transfer_did( **timelock_info.to_json_dict(), } response = await self.fetch("did_transfer_did", request) - return DIDTransferDIDResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DIDTransferDIDResponse) async def did_set_wallet_name(self, wallet_id: int, name: str) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "name": name} @@ -748,7 +748,7 @@ async def cat_spend( send_dict["tail_reveal"] = bytes(cat_discrepancy[1]).hex() send_dict["tail_solution"] = bytes(cat_discrepancy[2]).hex() res = await self.fetch("cat_spend", send_dict) - return CATSpendResponse.from_json_dict(res) + return json_deserialize_with_clvm_streamable(res, CATSpendResponse) # Offers async def create_offer_for_ids( @@ -777,7 +777,7 @@ async def create_offer_for_ids( if solver is not None: req["solver"] = solver res = await self.fetch("create_offer_for_ids", req) - return CreateOfferForIDsResponse.from_json_dict(res) + return json_deserialize_with_clvm_streamable(res, CreateOfferForIDsResponse) async def get_offer_summary( self, offer: Offer, advanced: bool = False @@ -810,7 +810,7 @@ async def take_offer( if solver is not None: req["solver"] = solver res = await self.fetch("take_offer", req) - return TakeOfferResponse.from_json_dict(res) + return json_deserialize_with_clvm_streamable(res, TakeOfferResponse) async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> TradeRecord: res = await self.fetch("get_offer", {"trade_id": trade_id.hex(), "file_contents": file_contents}) @@ -875,7 +875,7 @@ async def cancel_offer( }, ) - return CancelOfferResponse.from_json_dict(res) + return json_deserialize_with_clvm_streamable(res, CancelOfferResponse) async def cancel_offers( self, @@ -905,7 +905,7 @@ async def cancel_offers( }, ) - return CancelOffersResponse.from_json_dict(res) + return json_deserialize_with_clvm_streamable(res, CancelOffersResponse) # NFT wallet async def create_new_nft_wallet(self, did_id: Optional[str], name: Optional[str] = None) -> Dict[str, Any]: @@ -955,7 +955,7 @@ async def mint_nft( **timelock_info.to_json_dict(), } response = await self.fetch("nft_mint_nft", request) - return NFTMintNFTResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, NFTMintNFTResponse) # TODO: add a test for this async def add_uri_to_nft( @@ -982,7 +982,7 @@ async def add_uri_to_nft( **timelock_info.to_json_dict(), } response = await self.fetch("nft_add_uri", request) - return NFTAddURIResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, NFTAddURIResponse) async def nft_calculate_royalties( self, @@ -1027,7 +1027,7 @@ async def transfer_nft( **timelock_info.to_json_dict(), } response = await self.fetch("nft_transfer_nft", request) - return NFTTransferNFTResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, NFTTransferNFTResponse) async def count_nfts(self, wallet_id: Optional[int]) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1062,7 +1062,7 @@ async def set_nft_did( **timelock_info.to_json_dict(), } response = await self.fetch("nft_set_nft_did", request) - return NFTSetNFTDIDResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, NFTSetNFTDIDResponse) async def get_nft_wallet_did(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1111,7 +1111,7 @@ async def nft_mint_bulk( **timelock_info.to_json_dict(), } response = await self.fetch("nft_mint_bulk", request) - return NFTMintBulkResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, NFTMintBulkResponse) # DataLayer async def create_new_dl( @@ -1254,10 +1254,11 @@ async def dl_delete_mirror( async def dl_verify_proof(self, request: DLProof) -> VerifyProofResponse: response = await self.fetch(path="dl_verify_proof", request_json=request.to_json_dict()) - return VerifyProofResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, VerifyProofResponse) async def get_notifications(self, request: GetNotifications) -> GetNotificationsResponse: - return GetNotificationsResponse.from_json_dict(await self.fetch("get_notifications", request.to_json_dict())) + response = await self.fetch("get_notifications", request.to_json_dict()) + return json_deserialize_with_clvm_streamable(response, GetNotificationsResponse) async def delete_notifications(self, ids: Optional[List[bytes32]] = None) -> bool: request = {} @@ -1326,7 +1327,7 @@ async def create_new_dao_wallet( **tx_config.to_json_dict(), } response = await self.fetch("create_new_wallet", request) - return CreateNewDAOWalletResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, CreateNewDAOWalletResponse) async def dao_get_treasury_id(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1369,7 +1370,7 @@ async def dao_create_proposal( } response = await self.fetch("dao_create_proposal", request) - return DAOCreateProposalResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOCreateProposalResponse) async def dao_get_proposal_state(self, wallet_id: int, proposal_id: str) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "proposal_id": proposal_id} @@ -1401,7 +1402,7 @@ async def dao_vote_on_proposal( **tx_config.to_json_dict(), } response = await self.fetch("dao_vote_on_proposal", request) - return DAOVoteOnProposalResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOVoteOnProposalResponse) async def dao_get_proposals(self, wallet_id: int, include_closed: bool = True) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "include_closed": include_closed} @@ -1426,7 +1427,7 @@ async def dao_close_proposal( **tx_config.to_json_dict(), } response = await self.fetch("dao_close_proposal", request) - return DAOCloseProposalResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOCloseProposalResponse) async def dao_free_coins_from_finished_proposals( self, @@ -1442,7 +1443,7 @@ async def dao_free_coins_from_finished_proposals( **tx_config.to_json_dict(), } response = await self.fetch("dao_free_coins_from_finished_proposals", request) - return DAOFreeCoinsFromFinishedProposalsResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOFreeCoinsFromFinishedProposalsResponse) async def dao_get_treasury_balance(self, wallet_id: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id} @@ -1467,7 +1468,7 @@ async def dao_add_funds_to_treasury( **tx_config.to_json_dict(), } response = await self.fetch("dao_add_funds_to_treasury", request) - return DAOAddFundsToTreasuryResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOAddFundsToTreasuryResponse) async def dao_send_to_lockup( self, @@ -1485,7 +1486,7 @@ async def dao_send_to_lockup( **tx_config.to_json_dict(), } response = await self.fetch("dao_send_to_lockup", request) - return DAOSendToLockupResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOSendToLockupResponse) async def dao_exit_lockup( self, @@ -1503,7 +1504,7 @@ async def dao_exit_lockup( **tx_config.to_json_dict(), } response = await self.fetch("dao_exit_lockup", request) - return DAOExitLockupResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, DAOExitLockupResponse) async def dao_adjust_filter_level(self, wallet_id: int, filter_level: int) -> Dict[str, Any]: request = {"wallet_id": wallet_id, "filter_level": filter_level} @@ -1532,7 +1533,7 @@ async def vc_mint( **timelock_info.to_json_dict(), }, ) - return VCMintResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, VCMintResponse) async def vc_get(self, vc_id: bytes32) -> Optional[VCRecord]: response = await self.fetch("vc_get", {"vc_id": vc_id.hex()}) @@ -1570,7 +1571,7 @@ async def vc_spend( **timelock_info.to_json_dict(), }, ) - return VCSpendResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, VCSpendResponse) async def vc_add_proofs(self, proofs: Dict[str, Any]) -> None: await self.fetch("vc_add_proofs", {"proofs": proofs}) @@ -1599,7 +1600,7 @@ async def vc_revoke( **timelock_info.to_json_dict(), }, ) - return VCRevokeResponse.from_json_dict(response) + return json_deserialize_with_clvm_streamable(response, VCRevokeResponse) async def crcat_approve_pending( self, From 5e13d4c39a7648027943f8a793fded7c7293e331 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 29 Mar 2024 11:28:08 -0700 Subject: [PATCH 180/274] Fix offer endpoint --- chia/rpc/wallet_request_types.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index d222dc4f49a4..26e7f01af791 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -18,6 +18,7 @@ from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable from chia.wallet.vc_wallet.vc_store import VCRecord _T_OfferEndpointResponse = TypeVar("_T_OfferEndpointResponse", bound="_OfferEndpointResponse") @@ -145,7 +146,9 @@ class _OfferEndpointResponse(TransactionEndpointResponse): @classmethod def from_json_dict(cls: Type[_T_OfferEndpointResponse], json_dict: Dict[str, Any]) -> _T_OfferEndpointResponse: - tx_endpoint: TransactionEndpointResponse = TransactionEndpointResponse.from_json_dict(json_dict) + tx_endpoint: TransactionEndpointResponse = json_deserialize_with_clvm_streamable( + json_dict, TransactionEndpointResponse + ) offer: Offer = Offer.from_bech32(json_dict["offer"]) return cls( From c5da8ce414c04ddad696efca3480006bdb3ad3fd Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Fri, 5 Apr 2024 20:16:29 +0300 Subject: [PATCH 181/274] add recovery endpoint and cli --- chia/cmds/chia.py | 2 + chia/cmds/vault.py | 133 ++++++++++++++++++++++++ chia/cmds/vault_funcs.py | 55 ++++++++++ chia/rpc/wallet_rpc_api.py | 11 ++ chia/rpc/wallet_rpc_client.py | 7 ++ chia/wallet/vault/vault_wallet.py | 5 +- tests/cmds/wallet/test_vault.py | 123 ++++++++++++++++++++++ tests/wallet/vault/test_vault_wallet.py | 64 +++++++++++- 8 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 chia/cmds/vault.py create mode 100644 chia/cmds/vault_funcs.py create mode 100644 tests/cmds/wallet/test_vault.py diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 71b6fce10cbb..9aeeea0d22ab 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -26,6 +26,7 @@ from chia.cmds.show import show_cmd from chia.cmds.start import start_cmd from chia.cmds.stop import stop_cmd +from chia.cmds.vault import vault_cmd from chia.cmds.wallet import wallet_cmd from chia.util.default_root import DEFAULT_KEYS_ROOT_PATH, DEFAULT_ROOT_PATH from chia.util.errors import KeychainCurrentPassphraseIsInvalid @@ -131,6 +132,7 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: cli.add_command(completion) cli.add_command(dao_cmd) cli.add_command(dev_cmd) +cli.add_command(vault_cmd) def main() -> None: diff --git a/chia/cmds/vault.py b/chia/cmds/vault.py new file mode 100644 index 000000000000..fcd1b4f98363 --- /dev/null +++ b/chia/cmds/vault.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import asyncio +from decimal import Decimal +from typing import Optional + +import click + +from chia.cmds import options +from chia.cmds.plotnft import validate_fee + + +@click.group("vault", help="Manage your vault") +@click.pass_context +def vault_cmd(ctx: click.Context) -> None: + pass + + +@vault_cmd.command("create", help="Create a new vault") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@options.create_fingerprint() +@click.option( + "-pk", + "--public-key", + help="SECP public key", + type=str, + required=True, +) +@click.option( + "-rk", + "--recovery-public-key", + help="BLS public key for vault recovery", + type=str, + required=False, + default=None, +) +@click.option( + "-rt", + "--recovery-timelock", + help="Timelock for vault recovery (in seconds)", + type=int, + required=False, + default=None, +) +@click.option( + "-i", + "--hidden-puzzle-index", + help="Starting index for hidden puzzle", + type=int, + required=False, + default=0, +) +@click.option( + "-m", + "--fee", + help="Set the fees per transaction, in XCH.", + type=str, + default="0", + show_default=True, + callback=validate_fee, +) +@click.option("-n", "--name", help="Set the vault name", type=str) +def vault_create_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + public_key: str, + recovery_public_key: Optional[str], + recovery_timelock: Optional[int], + hidden_puzzle_index: Optional[int], + fee: str, + name: Optional[str], +) -> None: + from .vault_funcs import create_vault + + if hidden_puzzle_index is None: + hidden_puzzle_index = 0 + + asyncio.run( + create_vault( + wallet_rpc_port, + fingerprint, + public_key, + recovery_public_key, + recovery_timelock, + hidden_puzzle_index, + Decimal(fee), + name, + ) + ) + + +@vault_cmd.command("recover", help="Generate transactions for vault recovery") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@options.create_fingerprint() +@click.option("-i", "--wallet-id", help="Vault Wallet ID", type=int, required=True, default=1) +@click.option( + "-ri", + "--recovery-initiate-file", + help="Provide a filename to store the recovery transactions", + type=str, + required=True, + default="initiate_recovery.json", +) +@click.option( + "-rf", + "--recovery-finish-file", + help="Provide a filename to store the recovery transactions", + type=str, + required=True, + default="finish_recovery.json", +) +def vault_recover_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + wallet_id: int, + recovery_initiate_file: str, + recovery_finish_file: str, +) -> None: + from .vault_funcs import recover_vault + + asyncio.run(recover_vault(wallet_rpc_port, fingerprint, wallet_id, recovery_initiate_file, recovery_finish_file)) diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py new file mode 100644 index 000000000000..956184902b20 --- /dev/null +++ b/chia/cmds/vault_funcs.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import json +from decimal import Decimal +from typing import Optional + +from chia.cmds.cmds_util import get_wallet_client +from chia.cmds.units import units +from chia.util.ints import uint32, uint64 + + +async def create_vault( + wallet_rpc_port: Optional[int], + fingerprint: Optional[int], + public_key: str, + recovery_public_key: Optional[str], + timelock: Optional[int], + hidden_puzzle_index: int, + d_fee: Decimal, + name: Optional[str], +) -> None: + async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): + fee: int = int(d_fee * units["chia"]) + assert hidden_puzzle_index >= 0 + if timelock is not None: + assert timelock > 0 + try: + await wallet_client.vault_create( + bytes.fromhex(public_key), + uint32(hidden_puzzle_index), + config, + bytes.fromhex(recovery_public_key) if recovery_public_key else None, + uint64(timelock) if timelock else None, + uint64(fee), + push=True, + ) + print("Successfully created a Vault wallet") + except Exception as e: + print(f"Failed to create a new Vault: {e}") + + +async def recover_vault( + wallet_rpc_port: Optional[int], fingerprint: Optional[int], wallet_id: int, initiate_file: str, finish_file: str +) -> None: + async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): + try: + response = await wallet_client.vault_recovery(uint32(wallet_id)) + with open(initiate_file, "w") as f: + json.dump(response[0].to_json_dict(), f, indent=4) + print(f"Initiate Recovery transaction written to: {initiate_file}") + with open(finish_file, "w") as f: + json.dump(response[1].to_json_dict(), f, indent=4) + print(f"Finish Recovery transaction written to: {finish_file}") + except Exception as e: + print(f"Error creating recovery transactions: {e}") diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 1ea962a431ca..64d127fd3ece 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -119,6 +119,7 @@ from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import CoinType, WalletType from chia.wallet.vault.vault_drivers import get_vault_hidden_puzzle_with_index +from chia.wallet.vault.vault_wallet import Vault from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet from chia.wallet.vc_wallet.vc_store import VCProofs @@ -303,6 +304,7 @@ def get_routes(self) -> Dict[str, Endpoint]: "/submit_transactions": self.submit_transactions, # VAULT "/vault_create": self.vault_create, + "/vault_recovery": self.vault_recovery, } def get_connections(self, request_node_type: Optional[NodeType]) -> List[Dict[str, Any]]: @@ -4617,3 +4619,12 @@ async def vault_create( return { "transactions": [vault_record.to_json_dict_convenience(self.service.config)], } + + async def vault_recovery(self, request: Dict[str, Any]) -> EndpointResult: + """ + Initiate Vault Recovery + """ + wallet_id = uint32(request["wallet_id"]) + wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=Vault) + recovery_txs = await wallet.create_recovery_spends() + return {"transactions": [tx.to_json_dict_convenience(self.service.config) for tx in recovery_txs]} diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index ef723516c6e9..df1bc025f1ab 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1673,3 +1673,10 @@ async def vault_create( }, ) return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + + async def vault_recovery(self, wallet_id: uint32) -> List[TransactionRecord]: + response = await self.fetch( + "vault_recovery", + {"wallet_id": wallet_id}, + ) + return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 956e8a5cef91..8a9d2c6fc041 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -140,7 +140,6 @@ async def generate_signed_transaction( coins=coins, primaries_input=primaries, memos=memos, - negative_change_allowed=kwargs.get("negative_change_allowed", False), puzzle_decorator_override=puzzle_decorator_override, extra_conditions=extra_conditions, ) @@ -542,7 +541,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: sent_to=[], memos=[], trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), + type=uint32(TransactionType.INCOMING_TX.value), name=recovery_spend.name(), valid_times=parse_timelock_info(tuple()), ) @@ -562,7 +561,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: sent_to=[], memos=[], trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), + type=uint32(TransactionType.INCOMING_TX.value), name=finish_spend.name(), valid_times=parse_timelock_info(tuple()), ) diff --git a/tests/cmds/wallet/test_vault.py b/tests/cmds/wallet/test_vault.py new file mode 100644 index 000000000000..92cadf121249 --- /dev/null +++ b/tests/cmds/wallet/test_vault.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from pathlib import Path +from typing import List, Optional, Tuple + +from chia_rs import Coin, G2Element + +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint8, uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.tx_config import TXConfig +from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert +from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, WALLET_ID_ARG, get_bytes32 + + +def test_vault_create(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: + test_rpc_clients, root_dir = get_test_cli_clients + + # set RPC clients + class CreateVaultRpcClient(TestWalletRpcClient): + async def vault_create( + self, + secp_pk: bytes, + hp_index: uint32, + tx_config: TXConfig, + bls_pk: Optional[bytes] = None, + timelock: Optional[uint64] = None, + fee: uint64 = uint64(0), + push: bool = True, + ) -> List[TransactionRecord]: + tx_rec = TransactionRecord( + confirmed_at_height=uint32(1), + created_at_time=uint64(1234), + to_puzzle_hash=get_bytes32(1), + amount=uint64(12345678), + fee_amount=uint64(1234567), + confirmed=False, + sent=uint32(0), + spend_bundle=SpendBundle([], G2Element()), + additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], + removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], + wallet_id=uint32(1), + sent_to=[("aaaaa", uint8(1), None)], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=get_bytes32(2), + memos=[(get_bytes32(3), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), + ) + return [tx_rec] + + inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter + test_rpc_clients.wallet_rpc_client = inst_rpc_client + pk = get_bytes32(0).hex() + recovery_pk = get_bytes32(1).hex() + timelock = 100 + hidden_puzzle_index = 10 + fee = 0.1 + command_args = [ + "vault", + "create", + "-pk", + pk, + "-rk", + recovery_pk, + "-rt", + timelock, + "-i", + hidden_puzzle_index, + "-m", + fee, + ] + assert_list = ["Successfully created a Vault wallet"] + run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG], assert_list) + + +def test_vault_recovery(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: + test_rpc_clients, root_dir = get_test_cli_clients + + # set RPC clients + class CreateVaultRpcClient(TestWalletRpcClient): + async def vault_recovery( + self, + wallet_id: uint32, + ) -> List[TransactionRecord]: + tx_rec = TransactionRecord( + confirmed_at_height=uint32(1), + created_at_time=uint64(1234), + to_puzzle_hash=get_bytes32(1), + amount=uint64(12345678), + fee_amount=uint64(1234567), + confirmed=False, + sent=uint32(0), + spend_bundle=SpendBundle([], G2Element()), + additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], + removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], + wallet_id=uint32(1), + sent_to=[("aaaaa", uint8(1), None)], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=get_bytes32(2), + memos=[(get_bytes32(3), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), + ) + return [tx_rec, tx_rec] + + inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter + test_rpc_clients.wallet_rpc_client = inst_rpc_client + command_args = [ + "vault", + "recover", + "-ri", + "recovery_init.json", + "-rf", + "recovery_finish.json", + ] + assert_list = [ + "Initiate Recovery transaction written to: recovery_init.json", + "Finish Recovery transaction written to: recovery_finish.json", + ] + run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG, WALLET_ID_ARG], assert_list) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 1bed32fbd9fc..25e0a548cef0 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -31,7 +31,7 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b # Temporary hack so execute_signing_instructions can access the key env.wallet_state_manager.config["test_sk"] = SECP_SK - client = env.rpc_client + client = wallet_environments.environments[1].rpc_client fingerprint = (await client.get_public_keys())[0] bls_pk = None timelock = None @@ -60,7 +60,7 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b }, post_block_balance_updates={ 1: { - "confirmed_wallet_balance": -1, + # "confirmed_wallet_balance": -1, "set_remainder": True, } }, @@ -231,3 +231,63 @@ async def test_vault_creation( # test match_hinted_coin matched = await wallet.match_hinted_coin(wallet.vault_info.coin, wallet.vault_info.inner_puzzle_hash) assert matched + + +@pytest.mark.parametrize( + "wallet_environments", + [{"num_environments": 2, "blocks_needed": [1, 1]}], + indirect=True, +) +@pytest.mark.parametrize("setup_function", [vault_setup]) +@pytest.mark.parametrize("with_recovery", [True]) +@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") +@pytest.mark.anyio +async def test_vault_recovery( + wallet_environments: WalletTestFramework, + setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], + with_recovery: bool, +) -> None: + await setup_function(wallet_environments, with_recovery) + env = wallet_environments.environments[0] + assert isinstance(env.xch_wallet, Vault) + + wallet: Vault = env.xch_wallet + await wallet.sync_vault_launcher() + assert wallet.vault_info + + p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() + await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) + + coins_to_create = 2 + funding_amount = uint64(1000000000) + funding_wallet = wallet_environments.environments[1].xch_wallet + for _ in range(coins_to_create): + funding_tx = await funding_wallet.generate_signed_transaction( + funding_amount, + p2_singleton_puzzle_hash, + DEFAULT_TX_CONFIG, + memos=[wallet.vault_info.pubkey], + ) + await funding_wallet.wallet_state_manager.add_pending_transactions(funding_tx) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": funding_amount * 2, + "set_remainder": True, + } + }, + ), + ], + ) + + recovery_txs = await env.rpc_client.vault_recovery(wallet_id=wallet.id()) + assert recovery_txs From 9791a37545ea4ebdc75dca1ed730752ff54138f4 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 8 Apr 2024 09:12:10 +0300 Subject: [PATCH 182/274] use p2_singleton_via_delegated --- .../puzzles/deployed_puzzle_hashes.json | 2 +- .../puzzles/p2_delegated_or_hidden_secp.clsp | 26 +++++++- .../p2_delegated_or_hidden_secp.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 6 +- chia/wallet/vault/vault_wallet.py | 59 ++++++++++++------- tests/wallet/vault/test_vault_clsp.py | 20 ++++--- tests/wallet/vault/test_vault_lifecycle.py | 1 + tests/wallet/vault/test_vault_wallet.py | 3 + 8 files changed, 85 insertions(+), 34 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index a4f95708f981..95d15b34e899 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -43,7 +43,7 @@ "p2_announced_delegated_puzzle": "c4d24c3c5349376f3e8f3aba202972091713b4ec4915f0f26192ae4ace0bd04d", "p2_conditions": "1c77d7d5efde60a7a1d2d27db6d746bc8e568aea1ef8586ca967a0d60b83cc36", "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", - "p2_delegated_or_hidden_secp": "007b927c693b5594c777ce0a433d5bce0c1c3a101140c1897301f74b0ae103f5", + "p2_delegated_or_hidden_secp": "88bb2f500407842240e91830c9252371c1445140bd9a41e37ba23e6e9545651a", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp index c5b66d264e33..266d3b8d9c07 100644 --- a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp @@ -1,11 +1,24 @@ ; p2_delegated with SECP256-R1 signature ; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) -(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id) +(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id next_vault_inner p2_ids) (include *standard-cl-21*) (include condition_codes.clib) (include sha256tree.clib) + (defun create_p2_announcements (p2_ids zero_puzhash) + (if p2_ids + (c + (list CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list (f p2_ids) zero_puzhash))) + (c + (list ASSERT_COIN_ANNOUNCEMENT (sha256 (f p2_ids) "$")) + (create_p2_announcements (r p2_ids) zero_puzhash) + ) + ) + () + ) + ) + (let ((delegated_puzzle_hash (sha256tree delegated_puzzle))) (if (= delegated_puzzle_hash HIDDEN_PUZZLE_HASH) (a delegated_puzzle delegated_solution) @@ -13,7 +26,16 @@ (x) ; this doesn't actually run because secp256_verify will raise on failure (c (list ASSERT_MY_COIN_ID coin_id) - (a delegated_puzzle delegated_solution) + (c + (list CREATE_COIN next_vault_inner 1 (list next_vault_inner)) + (c + (list CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list (f p2_ids) delegated_puzzle_hash))) + (c + (list ASSERT_COIN_ANNOUNCEMENT (sha256 (f p2_ids) "$")) + (create_p2_announcements (r p2_ids) (sha256tree 0)) + ) + ) + ) ) ) ) diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex index d89e050ccc47..2833dbca3558 100644 --- a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ffff09ffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ff018080808080ffff01ff02ffff01ff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff0180808080808080ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ffff05ffff06ffff06ff01808080ffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff05ffff06ff018080ffff05ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01808080ffff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff018080808080808080ff018080ff0180ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 +ff02ffff01ff02ffff03ffff09ffff02ff04ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ff018080808080ffff01ff02ffff01ff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff0180808080808080ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ffff05ffff06ffff06ff01808080ffff0bffff02ff04ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff05ffff06ff018080ffff05ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01808080ffff04ffff04ffff0133ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff01808080808080808080ffff04ffff0101ffff04ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff01808080808080808080ffff018080ffff018080808080ffff04ffff04ffff013effff04ffff02ff04ffff04ff02ffff04ffff04ffff05ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080808080ffff04ffff02ff04ffff04ff02ffff04ff2fff80808080ffff01808080ff80808080ffff01808080ffff04ffff04ffff013dffff04ffff0bffff05ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080808080ffff012480ffff01808080ffff02ff06ffff04ff02ffff04ffff06ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080808080ffff04ffff02ff04ffff04ff02ffff04ffff0180ff80808080ff808080808080808080ff018080ff0180ff018080ff0180ffff04ffff01ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff04ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff04ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff02ffff03ff05ffff01ff02ffff01ff04ffff04ffff013effff04ffff02ff04ffff04ff02ffff04ffff04ffff05ff0580ffff04ff0bffff01808080ff80808080ffff01808080ffff04ffff04ffff013dffff04ffff0bffff05ff0580ffff012480ffff01808080ffff02ff06ffff04ff02ffff04ffff06ff0580ffff04ff0bff80808080808080ff0180ffff01ff02ffff01ff0180ff018080ff0180ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 58bf27ce38dd..c0cab29158c1 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -29,8 +29,9 @@ P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() -P2_SINGLETON_MOD: Program = load_clvm("p2_singleton.clsp") +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp") P2_SINGLETON_MOD_HASH = P2_SINGLETON_MOD.get_tree_hash() +P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp") # PUZZLES @@ -111,7 +112,8 @@ def get_recovery_finish_puzzle(bls_pk: G1Element, timelock: uint64, amount: uint def get_p2_singleton_puzzle(launcher_id: bytes32) -> Program: - puzzle = P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, SINGLETON_LAUNCHER_HASH) + singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH))) + puzzle = P2_SINGLETON_MOD.curry(singleton_struct, P2_SINGLETON_AGGREGATOR_MOD) return puzzle diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 8a9d2c6fc041..9dd5bd5f3f35 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -20,7 +20,7 @@ from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import Condition, CreateCoin, CreatePuzzleAnnouncement, parse_timelock_info +from chia.wallet.conditions import Condition, CreateCoin, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment @@ -169,6 +169,8 @@ async def generate_signed_transaction( async def generate_p2_singleton_spends( self, amount: uint64, + delegated_puzzle: Program, + delegated_solution: Program, tx_config: TXConfig, coins: Optional[Set[Coin]] = None, ) -> List[CoinSpend]: @@ -187,6 +189,8 @@ async def generate_p2_singleton_spends( p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.launcher_id) spends: List[CoinSpend] = [] + # create the first spend which generates the conditions + for coin in list(coins): p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin.name()]) spends.append(make_spend(coin, p2_singleton_puzzle, p2_singleton_solution)) @@ -216,31 +220,42 @@ async def _generate_unsigned_transaction( + fee + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) ) - - p2_singleton_spends = await self.generate_p2_singleton_spends(uint64(total_amount), tx_config, coins=coins) - - coins = {spend.coin for spend in p2_singleton_spends} - spend_value = sum([coin.amount for coin in coins]) - change = spend_value - total_amount - assert change >= 0 - if change > 0: - change_puzzle_hash: bytes32 = get_p2_singleton_puzzle_hash(self.launcher_id) - primaries.append(Payment(change_puzzle_hash, uint64(change))) + # Select p2_singleton coins to spend + if coins is None: + total_balance = await self.get_spendable_balance() + if total_amount > total_balance: + raise ValueError( + f"Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {amount} mojos" + ) + coins = await self.select_coins( + uint64(total_amount), + tx_config.coin_selection_config, + ) + assert len(coins) > 0 + selected_amount = sum([coin.amount for coin in coins]) + assert selected_amount >= amount conditions = [primary.as_condition() for primary in primaries] - next_puzzle_hash = ( - self.vault_info.coin.puzzle_hash if tx_config.reuse_puzhash else (await self.get_new_puzzlehash()) - ) - recreate_vault_condition = CreateCoin( - next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] - ).to_program() - conditions.append(recreate_vault_condition) - announcements = [CreatePuzzleAnnouncement(spend.coin.name()).to_program() for spend in p2_singleton_spends] - conditions.extend(announcements) - + conditions.append(Payment(newpuzzlehash, amount, memos if memos else []).as_condition()) + p2_singleton_puzzle = get_p2_singleton_puzzle(self.launcher_id) + p2_singleton_puzhash = p2_singleton_puzzle.get_tree_hash() + # add the change condition + if selected_amount > amount: + conditions.append(Payment(p2_singleton_puzhash, uint64(selected_amount - total_amount)).as_condition()) + # create the p2_singleton spends delegated_puzzle = puzzle_for_conditions(conditions) delegated_solution = solution_for_conditions(conditions) + p2_singleton_spends: List[CoinSpend] = [] + for coin in coins: + if not p2_singleton_spends: + p2_solution = Program.to( + [0, self.vault_info.inner_puzzle_hash, delegated_puzzle, delegated_solution, coin.name()] + ) + else: + p2_solution = Program.to([0, self.vault_info.inner_puzzle_hash, 0, 0, coin.name()]) + p2_singleton_spends.append(make_spend(coin, p2_singleton_puzzle, p2_solution)) + # create the vault spend secp_puzzle = construct_p2_delegated_secp( self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, @@ -260,6 +275,8 @@ async def _generate_unsigned_transaction( delegated_solution, None, # Slot for signed message self.vault_info.coin.name(), + vault_inner_puzzle.get_tree_hash(), + [p2_coin.name() for p2_coin in coins], ] ) if self.vault_info.is_recoverable: diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 0005be1f1165..48b883812b50 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -95,12 +95,13 @@ def test_recovery_puzzles() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) + next_vault_inner_puzhash = bytes32(bytes([1] * 32)) secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, next_vault_inner_puzhash] ) escape_solution = Program.to([escape_proof, escape_puzzle, secp_solution]) escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) - assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH + assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == next_vault_inner_puzhash def test_p2_delegated_secp() -> None: @@ -112,11 +113,13 @@ def test_p2_delegated_secp() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) - - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) + next_vault_inner_puzhash = bytes32(bytes([1] * 32)) + secp_solution = Program.to( + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, next_vault_inner_puzhash] + ) conds = secp_puzzle.run(secp_solution) - assert conds.at("rfrf").as_atom() == ACS_PH + assert conds.at("rfrf").as_atom() == next_vault_inner_puzhash # test that a bad secp sig fails sig_bytes = bytearray(signed_delegated_puzzle) @@ -158,12 +161,15 @@ def test_vault_root_puzzle() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) + next_vault_inner_puzhash = bytes32(bytes([1] * 32)) + secp_solution = Program.to( + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, next_vault_inner_puzhash] + ) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) secp_proof = Program.to((proof[0], proof[1][0])) vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) secp_conds = conditions_dict_for_solution(vault_puzzle, vault_solution, INFINITE_COST) - assert secp_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH + assert secp_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == next_vault_inner_puzhash # recovery spend path recovery_conditions = Program.to([[51, ACS_PH, amount]]) diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index dfb2ba180aa2..aad91129f246 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -82,6 +82,7 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_solution, secp_signature, vault_coin.name(), + vault_puzzlehash, ] ) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 25e0a548cef0..03844aa65f92 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -181,13 +181,16 @@ async def test_vault_creation( await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) assert unsigned_txs[0].spend_bundle is not None + assert len(unsigned_txs) == 1 spends = [Spend.from_coin_spend(spend) for spend in unsigned_txs[0].spend_bundle.coin_spends] signing_info = await env.rpc_client.gather_signing_info(GatherSigningInfo(spends)) signing_responses = await wallet.execute_signing_instructions(signing_info.signing_instructions) signed_response = await wallet.apply_signatures(spends, signing_responses) + await env.wallet_state_manager.submit_transactions([signed_response]) + vault_eve_id = wallet.vault_info.coin.name() await wallet_environments.process_pending_states( From bcc7ebe2109ef3948e65c81989348284d7755dac Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 07:25:21 -0700 Subject: [PATCH 183/274] Add @tx_out_cmd decorator --- chia/_tests/cmds/wallet/test_tx_decorators.py | 27 +++++++++++++++++++ chia/cmds/cmds_util.py | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 chia/_tests/cmds/wallet/test_tx_decorators.py diff --git a/chia/_tests/cmds/wallet/test_tx_decorators.py b/chia/_tests/cmds/wallet/test_tx_decorators.py new file mode 100644 index 000000000000..8a79e9507828 --- /dev/null +++ b/chia/_tests/cmds/wallet/test_tx_decorators.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import Any, List + +import click +from click.testing import CliRunner + +from chia._tests.cmds.wallet.test_consts import STD_TX +from chia.cmds.cmds_util import TransactionBundle, tx_out_cmd +from chia.wallet.transaction_record import TransactionRecord + + +def test_tx_out_cmd() -> None: + @click.command() + @tx_out_cmd + def test_cmd(**kwargs: Any) -> List[TransactionRecord]: + with open("./temp.push", "w") as file: + file.write(str(kwargs["push"])) + return [STD_TX, STD_TX] + + runner: CliRunner = CliRunner() + with runner.isolated_filesystem(): + runner.invoke(test_cmd, ["--transaction-file", "./temp.transaction"]) + with open("./temp.transaction", "rb") as file: + assert TransactionBundle.from_bytes(file.read()) == TransactionBundle([STD_TX, STD_TX]) + with open("./temp.push") as file2: + assert file2.read() == "True" diff --git a/chia/cmds/cmds_util.py b/chia/cmds/cmds_util.py index 2badd8d8d1ef..a3fe1625239b 100644 --- a/chia/cmds/cmds_util.py +++ b/chia/cmds/cmds_util.py @@ -327,6 +327,33 @@ def timelock_args(func: Callable[..., None]) -> Callable[..., None]: ) +@streamable +@dataclasses.dataclass(frozen=True) +class TransactionBundle(Streamable): + txs: List[TransactionRecord] + + +def tx_out_cmd(func: Callable[..., List[TransactionRecord]]) -> Callable[..., None]: + def original_cmd(transaction_file: Optional[str] = None, **kwargs: Any) -> None: + txs: List[TransactionRecord] = func(**kwargs) + if transaction_file is not None: + print(f"Writing transactions to file {transaction_file}:") + with open(Path(transaction_file), "wb") as file: + file.write(bytes(TransactionBundle(txs))) + + return click.option( + "--push/--no-push", help="Push the transaction to the network", type=bool, is_flag=True, default=True + )( + click.option( + "--transaction-file", + help="A file to write relevant transactions to", + type=str, + required=False, + default=None, + )(original_cmd) + ) + + @streamable @dataclasses.dataclass(frozen=True) class CMDCoinSelectionConfigLoader(Streamable): From 2edabcb76c187bbde0278b55e7054084e1a5a694 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 8 Apr 2024 13:44:39 -0700 Subject: [PATCH 184/274] Bump ecdsa version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8773f4364f11..3b03e707d011 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ "packaging==23.2", "psutil==5.9.4", "hsms==0.3.1", - "ecdsa==0.18.0", # For SECP + "ecdsa==0.19.0", # For SECP ] upnp_dependencies = [ From 87d472cf0580a385a6be82f75651fe18c715e0a4 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 9 Apr 2024 12:58:46 +0300 Subject: [PATCH 185/274] Revert to original p2_delegated_or_hidden_secp --- .../puzzles/deployed_puzzle_hashes.json | 2 +- .../puzzles/p2_delegated_or_hidden_secp.clsp | 26 +---------- .../p2_delegated_or_hidden_secp.clsp.hex | 2 +- chia/wallet/vault/vault_wallet.py | 44 ++++++++++++++----- tests/wallet/vault/test_vault_clsp.py | 20 +++------ tests/wallet/vault/test_vault_lifecycle.py | 1 - tests/wallet/vault/test_vault_wallet.py | 3 -- 7 files changed, 45 insertions(+), 53 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 95d15b34e899..a4f95708f981 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -43,7 +43,7 @@ "p2_announced_delegated_puzzle": "c4d24c3c5349376f3e8f3aba202972091713b4ec4915f0f26192ae4ace0bd04d", "p2_conditions": "1c77d7d5efde60a7a1d2d27db6d746bc8e568aea1ef8586ca967a0d60b83cc36", "p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341", - "p2_delegated_or_hidden_secp": "88bb2f500407842240e91830c9252371c1445140bd9a41e37ba23e6e9545651a", + "p2_delegated_or_hidden_secp": "007b927c693b5594c777ce0a433d5bce0c1c3a101140c1897301f74b0ae103f5", "p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c", "p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52", "p2_m_of_n_delegate_direct": "0f199d5263ac1a62b077c159404a71abd3f9691cc57520bf1d4c5cb501504457", diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp index 266d3b8d9c07..c5b66d264e33 100644 --- a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp @@ -1,24 +1,11 @@ ; p2_delegated with SECP256-R1 signature ; this is the "standard puzzle" for spending coins with SECP keys (ie secure enclave) -(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id next_vault_inner p2_ids) +(mod (GENESIS_CHALLENGE SECP_PK HIDDEN_PUZZLE_HASH delegated_puzzle delegated_solution signature coin_id) (include *standard-cl-21*) (include condition_codes.clib) (include sha256tree.clib) - (defun create_p2_announcements (p2_ids zero_puzhash) - (if p2_ids - (c - (list CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list (f p2_ids) zero_puzhash))) - (c - (list ASSERT_COIN_ANNOUNCEMENT (sha256 (f p2_ids) "$")) - (create_p2_announcements (r p2_ids) zero_puzhash) - ) - ) - () - ) - ) - (let ((delegated_puzzle_hash (sha256tree delegated_puzzle))) (if (= delegated_puzzle_hash HIDDEN_PUZZLE_HASH) (a delegated_puzzle delegated_solution) @@ -26,16 +13,7 @@ (x) ; this doesn't actually run because secp256_verify will raise on failure (c (list ASSERT_MY_COIN_ID coin_id) - (c - (list CREATE_COIN next_vault_inner 1 (list next_vault_inner)) - (c - (list CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list (f p2_ids) delegated_puzzle_hash))) - (c - (list ASSERT_COIN_ANNOUNCEMENT (sha256 (f p2_ids) "$")) - (create_p2_announcements (r p2_ids) (sha256tree 0)) - ) - ) - ) + (a delegated_puzzle delegated_solution) ) ) ) diff --git a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex index 2833dbca3558..d89e050ccc47 100644 --- a/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex +++ b/chia/wallet/puzzles/p2_delegated_or_hidden_secp.clsp.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ffff09ffff02ff04ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ff018080808080ffff01ff02ffff01ff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff0180808080808080ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ffff05ffff06ffff06ff01808080ffff0bffff02ff04ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff05ffff06ff018080ffff05ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01808080ffff04ffff04ffff0133ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff01808080808080808080ffff04ffff0101ffff04ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff01808080808080808080ffff018080ffff018080808080ffff04ffff04ffff013effff04ffff02ff04ffff04ff02ffff04ffff04ffff05ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080808080ffff04ffff02ff04ffff04ff02ffff04ff2fff80808080ffff01808080ff80808080ffff01808080ffff04ffff04ffff013dffff04ffff0bffff05ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080808080ffff012480ffff01808080ffff02ff06ffff04ff02ffff04ffff06ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080808080ffff04ffff02ff04ffff04ff02ffff04ffff0180ff80808080ff808080808080808080ff018080ff0180ff018080ff0180ffff04ffff01ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff04ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff04ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff02ffff03ff05ffff01ff02ffff01ff04ffff04ffff013effff04ffff02ff04ffff04ff02ffff04ffff04ffff05ff0580ffff04ff0bffff01808080ff80808080ffff01808080ffff04ffff04ffff013dffff04ffff0bffff05ff0580ffff012480ffff01808080ffff02ff06ffff04ff02ffff04ffff06ff0580ffff04ff0bff80808080808080ff0180ffff01ff02ffff01ff0180ff018080ff0180ff018080 +ff02ffff01ff02ffff03ffff09ffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ff018080808080ffff01ff02ffff01ff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff0180808080808080ff0180ffff01ff02ffff01ff02ffff03ffff841c3a8f00ffff05ffff06ffff06ff01808080ffff0bffff02ff02ffff04ff02ffff04ff2fff80808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff05ffff06ff018080ffff05ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01ff02ffff01ff0880ff0180ffff01ff02ffff01ff04ffff04ffff0146ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ffff01808080ffff02ffff05ffff06ffff06ffff06ffff06ff018080808080ffff05ffff06ffff06ffff06ffff06ffff06ff018080808080808080ff018080ff0180ff018080ff0180ffff04ffff01ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff02ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080 diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 9dd5bd5f3f35..615185204513 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -20,7 +20,13 @@ from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import Condition, CreateCoin, parse_timelock_info +from chia.wallet.conditions import ( + AssertCoinAnnouncement, + Condition, + CreateCoin, + CreatePuzzleAnnouncement, + parse_timelock_info, +) from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment @@ -169,8 +175,6 @@ async def generate_signed_transaction( async def generate_p2_singleton_spends( self, amount: uint64, - delegated_puzzle: Program, - delegated_solution: Program, tx_config: TXConfig, coins: Optional[Set[Coin]] = None, ) -> List[CoinSpend]: @@ -189,8 +193,6 @@ async def generate_p2_singleton_spends( p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.launcher_id) spends: List[CoinSpend] = [] - # create the first spend which generates the conditions - for coin in list(coins): p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin.name()]) spends.append(make_spend(coin, p2_singleton_puzzle, p2_singleton_solution)) @@ -220,6 +222,7 @@ async def _generate_unsigned_transaction( + fee + sum(c.amount for c in extra_conditions if isinstance(c, CreateCoin)) ) + # Select p2_singleton coins to spend if coins is None: total_balance = await self.get_spendable_balance() @@ -245,6 +248,7 @@ async def _generate_unsigned_transaction( # create the p2_singleton spends delegated_puzzle = puzzle_for_conditions(conditions) delegated_solution = solution_for_conditions(conditions) + p2_singleton_spends: List[CoinSpend] = [] for coin in coins: if not p2_singleton_spends: @@ -255,7 +259,29 @@ async def _generate_unsigned_transaction( p2_solution = Program.to([0, self.vault_info.inner_puzzle_hash, 0, 0, coin.name()]) p2_singleton_spends.append(make_spend(coin, p2_singleton_puzzle, p2_solution)) - # create the vault spend + next_puzzle_hash = ( + self.vault_info.coin.puzzle_hash if tx_config.reuse_puzhash else (await self.get_new_puzzlehash()) + ) + vault_conditions: List[Condition] = [] + recreate_vault_condition = CreateCoin( + next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] + ).to_program() + vault_conditions.append(recreate_vault_condition) + for i, spend in enumerate(p2_singleton_spends): + puzzle_to_assert = delegated_puzzle if i == 0 else Program.to(0) + vault_conditions.extend( + [ + CreatePuzzleAnnouncement( + Program.to([spend.coin.name(), puzzle_to_assert.get_tree_hash()]).get_tree_hash(), + # self.vault_info.coin.puzzle_hash + ).to_program(), + AssertCoinAnnouncement(asserted_id=spend.coin.name(), asserted_msg=b"$").to_program(), + ] + ) + + vault_delegated_puzzle = puzzle_for_conditions(vault_conditions) + vault_delegated_solution = solution_for_conditions(vault_conditions) + secp_puzzle = construct_p2_delegated_secp( self.vault_info.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, @@ -271,12 +297,10 @@ async def _generate_unsigned_transaction( secp_solution = Program.to( [ - delegated_puzzle, - delegated_solution, + vault_delegated_puzzle, + vault_delegated_solution, None, # Slot for signed message self.vault_info.coin.name(), - vault_inner_puzzle.get_tree_hash(), - [p2_coin.name() for p2_coin in coins], ] ) if self.vault_info.is_recoverable: diff --git a/tests/wallet/vault/test_vault_clsp.py b/tests/wallet/vault/test_vault_clsp.py index 48b883812b50..0005be1f1165 100644 --- a/tests/wallet/vault/test_vault_clsp.py +++ b/tests/wallet/vault/test_vault_clsp.py @@ -95,13 +95,12 @@ def test_recovery_puzzles() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) - next_vault_inner_puzhash = bytes32(bytes([1] * 32)) secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, next_vault_inner_puzhash] + [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] ) escape_solution = Program.to([escape_proof, escape_puzzle, secp_solution]) escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) - assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == next_vault_inner_puzhash + assert escape_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH def test_p2_delegated_secp() -> None: @@ -113,13 +112,11 @@ def test_p2_delegated_secp() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) - next_vault_inner_puzhash = bytes32(bytes([1] * 32)) - secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, next_vault_inner_puzhash] - ) + + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) conds = secp_puzzle.run(secp_solution) - assert conds.at("rfrf").as_atom() == next_vault_inner_puzhash + assert conds.at("rfrf").as_atom() == ACS_PH # test that a bad secp sig fails sig_bytes = bytearray(signed_delegated_puzzle) @@ -161,15 +158,12 @@ def test_vault_root_puzzle() -> None: signed_delegated_puzzle = secp_sk.sign_deterministic( delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH ) - next_vault_inner_puzhash = bytes32(bytes([1] * 32)) - secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, next_vault_inner_puzhash] - ) + secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) secp_proof = Program.to((proof[0], proof[1][0])) vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) secp_conds = conditions_dict_for_solution(vault_puzzle, vault_solution, INFINITE_COST) - assert secp_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == next_vault_inner_puzhash + assert secp_conds[ConditionOpcode.CREATE_COIN][0].vars[0] == ACS_PH # recovery spend path recovery_conditions = Program.to([[51, ACS_PH, amount]]) diff --git a/tests/wallet/vault/test_vault_lifecycle.py b/tests/wallet/vault/test_vault_lifecycle.py index aad91129f246..dfb2ba180aa2 100644 --- a/tests/wallet/vault/test_vault_lifecycle.py +++ b/tests/wallet/vault/test_vault_lifecycle.py @@ -82,7 +82,6 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_delegated_solution, secp_signature, vault_coin.name(), - vault_puzzlehash, ] ) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 03844aa65f92..25e0a548cef0 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -181,16 +181,13 @@ async def test_vault_creation( await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) assert unsigned_txs[0].spend_bundle is not None - assert len(unsigned_txs) == 1 spends = [Spend.from_coin_spend(spend) for spend in unsigned_txs[0].spend_bundle.coin_spends] signing_info = await env.rpc_client.gather_signing_info(GatherSigningInfo(spends)) signing_responses = await wallet.execute_signing_instructions(signing_info.signing_instructions) signed_response = await wallet.apply_signatures(spends, signing_responses) - await env.wallet_state_manager.submit_transactions([signed_response]) - vault_eve_id = wallet.vault_info.coin.name() await wallet_environments.process_pending_states( From ed66cc778cd2c40973b3aa9c49213932f0a29589 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 6 May 2024 09:53:39 -0700 Subject: [PATCH 186/274] Fix @marshal util to return proper transactions --- chia/rpc/util.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index f16dcdba1b0d..1a143ed4fd2c 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -158,18 +158,19 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s new_txs, signing_responses = await self.service.wallet_state_manager.sign_transactions( tx_records, response.get("signing_responses", []), "signing_responses" in response ) - response["transactions"] = [ - TransactionRecord.to_json_dict_convenience(tx, self.service.config) for tx in new_txs - ] response["signing_responses"] = [byte_serialize_clvm_streamable(r).hex() for r in signing_responses] else: new_txs = tx_records # pragma: no cover if request.get("push", push): - await self.service.wallet_state_manager.add_pending_transactions( + new_txs = await self.service.wallet_state_manager.add_pending_transactions( new_txs, merge_spends=merge_spends, sign=False ) + response["transactions"] = [ + TransactionRecord.to_json_dict_convenience(tx, self.service.config) for tx in new_txs + ] + # Some backwards compatibility code if "transaction" in response: if ( From 1abae415c5e7b16ace188c71b49ee2b4a1c7c5fd Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 8 May 2024 07:14:01 -0700 Subject: [PATCH 187/274] Add get_public_key --- chia/wallet/wallet.py | 4 ++-- chia/wallet/wallet_state_manager.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 9e7ae3c7eb12..a3bf86b62d07 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -169,8 +169,8 @@ 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()) + public_key = await self.wallet_state_manager.get_public_key(puzzle_hash) + return puzzle_for_pk(public_key) async def get_new_puzzle(self) -> Program: dr = await self.wallet_state_manager.get_unused_derivation_record(self.id()) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 1937716799fb..461e66586394 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -380,6 +380,14 @@ async def get_private_key(self, puzzle_hash: bytes32) -> PrivateKey: return master_sk_to_wallet_sk(self.get_master_private_key(), record.index) return master_sk_to_wallet_sk_unhardened(self.get_master_private_key(), record.index) + async def get_public_key(self, puzzle_hash: bytes32) -> G1Element: + 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: + raise ValueError(f"Key for puzzle hash: {puzzle_hash.hex()} is hardened") + return master_pk_to_wallet_pk_unhardened(self.root_pubkey, record.index) + def get_master_private_key(self) -> PrivateKey: if self.private_key is None: # pragma: no cover raise ValueError("Wallet is currently in observer mode and access to private key is denied") From 9fd0c862e0f122f381f5393f0e57ec143d88ab97 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 07:48:03 -0700 Subject: [PATCH 188/274] port `chia wallet send` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_wallet.py | 17 +++++++++--- chia/cmds/wallet.py | 9 +++++-- chia/cmds/wallet_funcs.py | 37 +++++++++++++++----------- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_wallet.py b/chia/_tests/cmds/wallet/test_wallet.py index d87742128436..cef2d09e28ec 100644 --- a/chia/_tests/cmds/wallet/test_wallet.py +++ b/chia/_tests/cmds/wallet/test_wallet.py @@ -6,6 +6,7 @@ import pkg_resources import pytest from chia_rs import Coin, G2Element +from click.testing import CliRunner from chia._tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, logType, run_cli_command_and_assert from chia._tests.cmds.wallet.test_consts import ( @@ -19,6 +20,7 @@ bytes32_hexstr, get_bytes32, ) +from chia.cmds.cmds_util import TransactionBundle from chia.rpc.wallet_request_types import ( CancelOfferResponse, CATSpendResponse, @@ -405,9 +407,18 @@ async def cat_spend( "Transaction submitted to nodes: [{'peer_id': 'aaaaa'", f"-f 789101 -tx 0x{get_bytes32(2).hex()}", ] - - run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG], assert_list) - run_cli_command_and_assert(capsys, root_dir, command_args + [CAT_FINGERPRINT_ARG], cat_assert_list) + with CliRunner().isolated_filesystem(): + run_cli_command_and_assert( + capsys, root_dir, command_args + [FINGERPRINT_ARG] + ["--transaction-file=temp"], assert_list + ) + run_cli_command_and_assert( + capsys, root_dir, command_args + [CAT_FINGERPRINT_ARG] + ["--transaction-file=temp2"], cat_assert_list + ) + + with open("temp", "rb") as file: + assert TransactionBundle.from_bytes(file.read()) == TransactionBundle([STD_TX]) + with open("temp2", "rb") as file: + assert TransactionBundle.from_bytes(file.read()) == TransactionBundle([STD_TX]) # these are various things that should be in the output expected_calls: logType = { diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 968b567cdd32..1e07819f6d0c 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -9,8 +9,10 @@ from chia.cmds import options from chia.cmds.check_wallet_db import help_text as check_help_text +from chia.cmds.cmds_util import tx_out_cmd from chia.cmds.coins import coins_cmd from chia.cmds.plotnft import validate_fee +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.address_type import AddressType from chia.wallet.util.wallet_types import WalletType @@ -192,6 +194,7 @@ def get_transactions_cmd( type=int, default=0, ) +@tx_out_cmd def send_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -206,10 +209,11 @@ def send_cmd( coins_to_exclude: Sequence[str], reuse: bool, clawback_time: int, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import send - asyncio.run( + return asyncio.run( send( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -224,6 +228,7 @@ def send_cmd( excluded_coin_ids=coins_to_exclude, reuse_puzhash=True if reuse else None, clawback_time_lock=clawback_time, + push=push, ) ) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 01856eeb562c..12508caebacf 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -272,7 +272,8 @@ async def send( excluded_coin_ids: Sequence[str], reuse_puzhash: Optional[bool], clawback_time_lock: int, -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if memo is None: memos = None @@ -284,19 +285,19 @@ async def send( f"A transaction of amount {amount} and fee {fee} is unusual.\n" f"Pass in --override if you are sure you mean to do this." ) - return + return [] if amount == 0: print("You can not send an empty transaction") - return + return [] if clawback_time_lock < 0: print("Clawback time lock seconds cannot be negative.") - return + return [] try: typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(typ) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return + return [] final_fee: uint64 = uint64(int(fee * units["chia"])) # fees are always in XCH mojos final_amount: uint64 = uint64(int(amount * mojo_per_unit)) @@ -319,6 +320,7 @@ async def send( if clawback_time_lock > 0 else None ), + push=push, ) elif typ in {WalletType.CAT, WalletType.CRCAT}: print("Submitting transaction...") @@ -334,23 +336,28 @@ async def send( address, final_fee, memos, + push=push, ) else: print("Only standard wallet and CAT wallets are supported") - return + return [] tx_id = res.transaction.name - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, tx_id)) - return None + if push: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, tx_id)) + return res.transactions print("Transaction not yet submitted to nodes") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + if push: # pragma: no cover + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + + return res.transactions # pragma: no cover async def get_address(wallet_rpc_port: Optional[int], fp: Optional[int], wallet_id: int, new_address: bool) -> None: From 618e4f9fca621ea263c489d8f953337c0eab3e53 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 15 May 2024 08:32:38 -0700 Subject: [PATCH 189/274] merge fix of puzzle hash derivation --- chia/wallet/wallet_state_manager.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 03ca29029ebc..51e7d452b13a 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -536,12 +536,13 @@ async def create_more_puzzle_hashes( await self.wallet_node.new_peak_queue.subscribe_to_puzzle_hashes( [record.puzzle_hash for record in derivation_paths] ) - if len(unhardened_keys) > 0: - self.state_changed("new_derivation_index", data_object={"index": last_index - 1}) - # 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 len(unhardened_keys) > 0: - self.log.info(f"Updating last used derivation index: {unused - 1}") - await self.puzzle_store.set_used_up_to(uint32(unused - 1)) + if not target_wallet.handle_own_derivation(): + if len(unhardened_keys) > 0: + self.state_changed("new_derivation_index", data_object={"index": last_index - 1}) + # 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 len(unhardened_keys) > 0: + 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] = [] From d423e1846d8e87f6d3e2aaf685bbc46ccdfa63ff Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 08:48:52 -0700 Subject: [PATCH 190/274] Port `chia wallet coins` to @tx_out_cmd --- chia/_tests/cmds/cmd_test_utils.py | 3 +- chia/_tests/cmds/wallet/test_coins.py | 3 ++ chia/cmds/coin_funcs.py | 41 ++++++++++++++++----------- chia/cmds/coins.py | 18 ++++++++---- 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/chia/_tests/cmds/cmd_test_utils.py b/chia/_tests/cmds/cmd_test_utils.py index 7c2a6ae7c542..c46ad38792be 100644 --- a/chia/_tests/cmds/cmd_test_utils.py +++ b/chia/_tests/cmds/cmd_test_utils.py @@ -254,8 +254,9 @@ async def send_transaction_multi( tx_config: TXConfig, coins: Optional[List[Coin]] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> SendTransactionMultiResponse: - self.add_to_log("send_transaction_multi", (wallet_id, additions, tx_config, coins, fee)) + self.add_to_log("send_transaction_multi", (wallet_id, additions, tx_config, coins, fee, push)) name = bytes32([2] * 32) return SendTransactionMultiResponse( [STD_UTX], diff --git a/chia/_tests/cmds/wallet/test_coins.py b/chia/_tests/cmds/wallet/test_coins.py index ad2c1ae35ea7..12b88b187e61 100644 --- a/chia/_tests/cmds/wallet/test_coins.py +++ b/chia/_tests/cmds/wallet/test_coins.py @@ -135,6 +135,7 @@ async def select_coins( Coin(get_bytes32(3), get_bytes32(4), uint64(1234560000)), ], 1000000000, + True, ), ( 1, @@ -152,6 +153,7 @@ async def select_coins( Coin(get_bytes32(5), get_bytes32(6), uint64(300000000000)), ], 1000000000, + True, ), ], } @@ -213,6 +215,7 @@ async def get_coin_records_by_names( DEFAULT_TX_CONFIG, [Coin(get_bytes32(1), get_bytes32(2), uint64(100000000000))], 1000000000, + True, ) ], } diff --git a/chia/cmds/coin_funcs.py b/chia/cmds/coin_funcs.py index f7893518bbe5..8aeff605686c 100644 --- a/chia/cmds/coin_funcs.py +++ b/chia/cmds/coin_funcs.py @@ -124,7 +124,8 @@ async def async_combine( target_coin_amount: Decimal, target_coin_ids_str: Sequence[str], largest_first: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): target_coin_ids: List[bytes32] = [bytes32.from_hexstr(coin_id) for coin_id in target_coin_ids_str] final_fee = uint64(int(fee * units["chia"])) @@ -135,10 +136,10 @@ async def async_combine( mojo_per_unit = get_mojo_per_unit(wallet_type) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return + return [] if not await wallet_client.get_synced(): print("Wallet not synced. Please wait.") - return + return [] is_xch: bool = wallet_type == WalletType.STANDARD_WALLET # this lets us know if we are directly combining Chia tx_config = CMDTXConfigLoader( @@ -165,10 +166,10 @@ async def async_combine( conf_coins = [cr for cr in conf_coins if cr.name in target_coin_ids] if len(conf_coins) == 0: print("No coins to combine.") - return + return [] if len(conf_coins) == 1: print("Only one coin found, you need at least two coins to combine.") - return + return [] if largest_first: conf_coins.sort(key=lambda r: r.coin.amount, reverse=True) else: @@ -181,15 +182,18 @@ async def async_combine( total_amount: uint128 = uint128(sum(coin.amount for coin in removals)) if is_xch and total_amount - final_fee <= 0: print("Total amount is less than 0 after fee, exiting.") - return + return [] target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, False)) additions = [{"amount": (total_amount - final_fee) if is_xch else total_amount, "puzzle_hash": target_ph}] transaction: TransactionRecord = ( - await wallet_client.send_transaction_multi(wallet_id, additions, tx_config, removals, final_fee) + await wallet_client.send_transaction_multi(wallet_id, additions, tx_config, removals, final_fee, push=push) ).transaction tx_id = transaction.name.hex() - print(f"Transaction sent: {tx_id}") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + if push: + print(f"Transaction sent: {tx_id}") + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + + return [transaction] async def async_split( @@ -202,22 +206,23 @@ async def async_split( amount_per_coin: Decimal, target_coin_id_str: str, # TODO: [add TXConfig args] -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): final_fee = uint64(int(fee * units["chia"])) target_coin_id: bytes32 = bytes32.from_hexstr(target_coin_id_str) if number_of_coins > 500: print(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.") - return + return [] try: wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client) mojo_per_unit = get_mojo_per_unit(wallet_type) except LookupError: print(f"Wallet id: {wallet_id} not found.") - return + return [] if not await wallet_client.get_synced(): print("Wallet not synced. Please wait.") - return + return [] is_xch: bool = wallet_type == WalletType.STANDARD_WALLET # this lets us know if we are directly spitting Chia final_amount_per_coin = uint64(int(amount_per_coin * mojo_per_unit)) total_amount = final_amount_per_coin * number_of_coins @@ -231,7 +236,7 @@ async def async_split( f"is less than the total amount of the split: {total_amount / mojo_per_unit}, exiting." ) print("Try using a smaller fee or amount.") - return + return [] additions: List[Dict[str, Union[uint64, bytes32]]] = [] for i in range(number_of_coins): # for readability. # we always use new addresses @@ -244,12 +249,13 @@ async def async_split( transaction: TransactionRecord = ( await wallet_client.send_transaction_multi( - wallet_id, additions, tx_config, [removal_coin_record.coin], final_fee + wallet_id, additions, tx_config, [removal_coin_record.coin], final_fee, push=push ) ).transaction tx_id = transaction.name.hex() - print(f"Transaction sent: {tx_id}") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") + if push: + print(f"Transaction sent: {tx_id}") + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}") dust_threshold = config.get("xch_spam_amount", 1000000) # min amount per coin in mojo spam_filter_after_n_txs = config.get("spam_filter_after_n_txs", 200) # how many txs to wait before filtering if final_amount_per_coin < dust_threshold and wallet_type == WalletType.STANDARD_WALLET: @@ -259,3 +265,4 @@ async def async_split( f"{'will' if number_of_coins > spam_filter_after_n_txs else 'may'} not show up in your wallet unless " f"you decrease the dust limit to below {final_amount_per_coin} mojos or disable it by setting it to 0." ) + return [transaction] diff --git a/chia/cmds/coins.py b/chia/cmds/coins.py index d3d4f1e4d8dd..a2c789e75928 100644 --- a/chia/cmds/coins.py +++ b/chia/cmds/coins.py @@ -2,11 +2,13 @@ import asyncio from decimal import Decimal -from typing import Optional, Sequence +from typing import List, Optional, Sequence import click from chia.cmds import options +from chia.cmds.cmds_util import tx_out_cmd +from chia.wallet.transaction_record import TransactionRecord @click.group("coins", help="Manage your wallets coins") @@ -150,6 +152,7 @@ def list_cmd( default=False, help="Sort coins from largest to smallest or smallest to largest.", ) +@tx_out_cmd def combine_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -162,10 +165,11 @@ def combine_cmd( fee: str, input_coins: Sequence[str], largest_first: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .coin_funcs import async_combine - asyncio.run( + return asyncio.run( async_combine( wallet_rpc_port=wallet_rpc_port, fingerprint=fingerprint, @@ -178,6 +182,7 @@ def combine_cmd( target_coin_amount=Decimal(target_amount), target_coin_ids_str=input_coins, largest_first=largest_first, + push=push, ) ) @@ -216,6 +221,7 @@ def combine_cmd( required=True, ) @click.option("-t", "--target-coin-id", type=str, required=True, help="The coin id of the coin we are splitting.") +@tx_out_cmd def split_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -224,10 +230,11 @@ def split_cmd( fee: str, amount_per_coin: str, target_coin_id: str, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .coin_funcs import async_split - asyncio.run( + return asyncio.run( async_split( wallet_rpc_port=wallet_rpc_port, fingerprint=fingerprint, @@ -236,5 +243,6 @@ def split_cmd( number_of_coins=number_of_coins, amount_per_coin=Decimal(amount_per_coin), target_coin_id_str=target_coin_id, + push=push, ) ) From a1bccab3f88bcb898285d9a8585bc10b8cf21e3a Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 10:46:02 -0700 Subject: [PATCH 191/274] Port chia wallet clawback to @tx_out_cmd --- chia/_tests/cmds/wallet/test_wallet.py | 17 ++++++++++++++--- chia/cmds/wallet.py | 9 +++++---- chia/cmds/wallet_funcs.py | 17 ++++++++++++----- chia/rpc/wallet_rpc_client.py | 2 ++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_wallet.py b/chia/_tests/cmds/wallet/test_wallet.py index cef2d09e28ec..3f8c5023b413 100644 --- a/chia/_tests/cmds/wallet/test_wallet.py +++ b/chia/_tests/cmds/wallet/test_wallet.py @@ -507,10 +507,21 @@ async def spend_clawback_coins( coin_ids: List[bytes32], fee: int = 0, force: bool = False, + push: bool = True, ) -> Dict[str, Any]: - self.add_to_log("spend_clawback_coins", (coin_ids, fee, force)) + self.add_to_log("spend_clawback_coins", (coin_ids, fee, force, push)) tx_hex_list = [get_bytes32(6).hex(), get_bytes32(7).hex(), get_bytes32(8).hex()] - return {"transaction_ids": tx_hex_list} + return { + "transaction_ids": tx_hex_list, + "transactions": [ + STD_TX.to_json_dict_convenience( + { + "selected_network": "mainnet", + "network_overrides": {"config": {"mainnet": {"address_prefix": "xch"}}}, + } + ) + ], + } inst_rpc_client = ClawbackWalletRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client @@ -528,7 +539,7 @@ async def spend_clawback_coins( run_cli_command_and_assert(capsys, root_dir, command_args, ["transaction_ids", str(r_tx_ids_hex)]) # these are various things that should be in the output expected_calls: logType = { - "spend_clawback_coins": [(tx_ids, 1000000000000, False)], + "spend_clawback_coins": [(tx_ids, 1000000000000, False, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 1e07819f6d0c..28e2943b85f4 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -313,14 +313,15 @@ def get_address_cmd(wallet_rpc_port: Optional[int], id: int, fingerprint: int, n is_flag=True, default=False, ) +@tx_out_cmd def clawback( - wallet_rpc_port: Optional[int], id: int, fingerprint: int, tx_ids: str, fee: str, force: bool -) -> None: # pragma: no cover + wallet_rpc_port: Optional[int], id: int, fingerprint: int, tx_ids: str, fee: str, force: bool, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import spend_clawback - asyncio.run( + return asyncio.run( spend_clawback( - wallet_rpc_port=wallet_rpc_port, fp=fingerprint, fee=Decimal(fee), tx_ids_str=tx_ids, force=force + wallet_rpc_port=wallet_rpc_port, fp=fingerprint, fee=Decimal(fee), tx_ids_str=tx_ids, force=force, push=push ) ) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 12508caebacf..b4ff534c49aa 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -1457,20 +1457,27 @@ async def sign_message( async def spend_clawback( - *, wallet_rpc_port: Optional[int], fp: Optional[int], fee: Decimal, tx_ids_str: str, force: bool = False -) -> None: # pragma: no cover + *, + wallet_rpc_port: Optional[int], + fp: Optional[int], + fee: Decimal, + tx_ids_str: str, + force: bool = False, + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _): tx_ids = [] for tid in tx_ids_str.split(","): tx_ids.append(bytes32.from_hexstr(tid)) if len(tx_ids) == 0: print("Transaction ID is required.") - return + return [] if fee < 0: print("Batch fee cannot be negative.") - return - response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"]), force) + return [] + response = await wallet_client.spend_clawback_coins(tx_ids, int(fee * units["chia"]), force, push=push) print(str(response)) + return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] async def mint_vc( diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 8415c7304720..79ac3cb78b7a 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -299,6 +299,7 @@ async def spend_clawback_coins( coin_ids: List[bytes32], fee: int = 0, force: bool = False, + push: bool = True, extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), ) -> Dict[str, Any]: @@ -307,6 +308,7 @@ async def spend_clawback_coins( "fee": fee, "force": force, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **timelock_info.to_json_dict(), } response = await self.fetch("spend_clawback_coins", request) From c598f70a2a22a381432b7eaf70845369cad9c357 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 11:53:08 -0700 Subject: [PATCH 192/274] Port `chia wallet take/cancel_offer` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_wallet.py | 16 ++++++--- chia/cmds/wallet.py | 17 +++++++--- chia/cmds/wallet_funcs.py | 45 +++++++++++++++++--------- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_wallet.py b/chia/_tests/cmds/wallet/test_wallet.py index 3f8c5023b413..f3e3a7151365 100644 --- a/chia/_tests/cmds/wallet/test_wallet.py +++ b/chia/_tests/cmds/wallet/test_wallet.py @@ -937,8 +937,9 @@ async def take_offer( tx_config: TXConfig, solver: Optional[Dict[str, Any]] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> TakeOfferResponse: - self.add_to_log("take_offer", (offer, tx_config, solver, fee)) + self.add_to_log("take_offer", (offer, tx_config, solver, fee, push)) return TakeOfferResponse( [STD_UTX], [STD_TX], @@ -981,7 +982,7 @@ async def take_offer( (cat2,), (bytes32.from_hexstr("accce8e1c71b56624f2ecaeff5af57eac41365080449904d0717bd333c04806d"),), ], - "take_offer": [(Offer.from_bech32(test_offer_file_bech32), DEFAULT_TX_CONFIG, None, 1000000000000)], + "take_offer": [(Offer.from_bech32(test_offer_file_bech32), DEFAULT_TX_CONFIG, None, 1000000000000, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -1010,9 +1011,14 @@ async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> Tra ) async def cancel_offer( - self, trade_id: bytes32, tx_config: TXConfig, fee: uint64 = uint64(0), secure: bool = True + self, + trade_id: bytes32, + tx_config: TXConfig, + fee: uint64 = uint64(0), + secure: bool = True, + push: bool = True, ) -> CancelOfferResponse: - self.add_to_log("cancel_offer", (trade_id, tx_config, fee, secure)) + self.add_to_log("cancel_offer", (trade_id, tx_config, fee, secure, push)) return CancelOfferResponse([STD_UTX], [STD_TX]) inst_rpc_client = CancelOfferRpcClient() # pylint: disable=no-value-for-parameter @@ -1033,7 +1039,7 @@ async def cancel_offer( run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "get_offer": [(test_offer_id_bytes, True)], - "cancel_offer": [(test_offer_id_bytes, DEFAULT_TX_CONFIG, 1000000000000, True)], + "cancel_offer": [(test_offer_id_bytes, DEFAULT_TX_CONFIG, 1000000000000, True, True)], "cat_asset_id_to_name": [ (cat1,), (cat2,), diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 28e2943b85f4..7edbc1fe56e8 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -467,6 +467,8 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str default=False, ) @click.option("--override", help="Creates offer without checking for unusual values", is_flag=True, default=False) +# This command looks like a good candidate for @tx_out_cmd however, pushing an incomplete tx is nonsensical and +# we already have a canonical offer file format which the idea of exporting a different transaction conflicts with def make_offer_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -564,17 +566,19 @@ def get_offers_cmd( is_flag=True, default=False, ) +@tx_out_cmd def take_offer_cmd( path_or_hex: str, wallet_rpc_port: Optional[int], fingerprint: int, examine_only: bool, fee: str, - reuse: bool, -) -> None: + reuse: bool, # reuse is not used + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import take_offer - asyncio.run(take_offer(wallet_rpc_port, fingerprint, Decimal(fee), path_or_hex, examine_only)) # reuse is not used + return asyncio.run(take_offer(wallet_rpc_port, fingerprint, Decimal(fee), path_or_hex, examine_only, push=push)) @wallet_cmd.command("cancel_offer", help="Cancel an existing offer") @@ -591,10 +595,13 @@ def take_offer_cmd( @click.option( "-m", "--fee", help="The fee to use when cancelling the offer securely, in XCH", default="0", show_default=True ) -def cancel_offer_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: str, insecure: bool, fee: str) -> None: +@tx_out_cmd +def cancel_offer_cmd( + wallet_rpc_port: Optional[int], fingerprint: int, id: str, insecure: bool, fee: str, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import cancel_offer - asyncio.run(cancel_offer(wallet_rpc_port, fingerprint, Decimal(fee), id, not insecure)) + return asyncio.run(cancel_offer(wallet_rpc_port, fingerprint, Decimal(fee), id, not insecure, push=push)) @wallet_cmd.command("check", short_help="Check wallet DB integrity", help=check_help_text) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index b4ff534c49aa..61aeb5a3f775 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -695,7 +695,8 @@ async def take_offer( d_fee: Decimal, file: str, examine_only: bool, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if os.path.exists(file): filepath = pathlib.Path(file) @@ -711,7 +712,7 @@ async def take_offer( offer = Offer.from_bech32(offer_hex) except ValueError: print("Please enter a valid offer file or hex blob") - return + return [] offered, requested, _, _ = offer.summary() cat_name_resolver = wallet_client.cat_asset_id_to_name @@ -776,15 +777,21 @@ async def take_offer( if not examine_only: print() cli_confirm("Would you like to take this offer? (y/n): ") - trade_record = ( - await wallet_client.take_offer( - offer, - fee=fee, - tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + res = await wallet_client.take_offer( + offer, + fee=fee, + tx_config=CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + push=push, + ) + if push: + print(f"Accepted offer with ID {res.trade_record.trade_id}") + print( + f"Use chia wallet get_offers --id {res.trade_record.trade_id} -f {fingerprint} to view its status" ) - ).trade_record - print(f"Accepted offer with ID {trade_record.trade_id}") - print(f"Use chia wallet get_offers --id {trade_record.trade_id} -f {fingerprint} to view its status") + + return res.transactions + else: + return [] async def cancel_offer( @@ -793,7 +800,8 @@ async def cancel_offer( d_fee: Decimal, offer_id_hex: str, secure: bool, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): offer_id = bytes32.from_hexstr(offer_id_hex) fee: int = int(d_fee * units["chia"]) @@ -802,13 +810,20 @@ async def cancel_offer( await print_trade_record(trade_record, wallet_client, summaries=True) cli_confirm(f"Are you sure you wish to cancel offer with ID: {trade_record.trade_id}? (y/n): ") - await wallet_client.cancel_offer( - offer_id, CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), secure=secure, fee=fee + res = await wallet_client.cancel_offer( + offer_id, + CMDTXConfigLoader().to_tx_config(units["chia"], config, fingerprint), + secure=secure, + fee=fee, + push=push, ) - print(f"Cancelled offer with ID {trade_record.trade_id}") - if secure: + if push or not secure: + print(f"Cancelled offer with ID {trade_record.trade_id}") + if secure and push: print(f"Use chia wallet get_offers --id {trade_record.trade_id} -f {fingerprint} to view cancel status") + return res.transactions + def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]: # pragma: no cover if typ in {WalletType.CAT, WalletType.CRCAT}: From 52ad57c3517f1dc3cde5d76491b85ae8723f169a Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 07:45:16 -0700 Subject: [PATCH 193/274] Port `chia wallet did ...` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_did.py | 22 ++++++++++------- chia/cmds/wallet.py | 31 ++++++++++++++---------- chia/cmds/wallet_funcs.py | 37 ++++++++++++++++++++--------- chia/rpc/wallet_rpc_api.py | 3 +++ chia/rpc/wallet_rpc_client.py | 2 ++ 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_did.py b/chia/_tests/cmds/wallet/test_did.py index 190cff16f268..63072fba841d 100644 --- a/chia/_tests/cmds/wallet/test_did.py +++ b/chia/_tests/cmds/wallet/test_did.py @@ -32,10 +32,11 @@ async def create_new_did_wallet( name: Optional[str] = "DID Wallet", backup_ids: Optional[List[str]] = None, required_num: int = 0, + push: bool = True, ) -> Dict[str, Union[str, int]]: if backup_ids is None: backup_ids = [] - self.add_to_log("create_new_did_wallet", (amount, fee, name, backup_ids, required_num)) + self.add_to_log("create_new_did_wallet", (amount, fee, name, backup_ids, required_num, push)) return {"wallet_id": 3, "my_did": "did:chia:testdid123456"} inst_rpc_client = DidCreateRpcClient() # pylint: disable=no-value-for-parameter @@ -48,7 +49,7 @@ async def create_new_did_wallet( ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "create_new_did_wallet": [(3, 100000000000, "test", [], 0)], + "create_new_did_wallet": [(3, 100000000000, "test", [], 0, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -180,8 +181,9 @@ async def update_did_metadata( wallet_id: int, metadata: Dict[str, object], tx_config: TXConfig, + push: bool = True, ) -> DIDUpdateMetadataResponse: - self.add_to_log("update_did_metadata", (wallet_id, metadata, tx_config)) + self.add_to_log("update_did_metadata", (wallet_id, metadata, tx_config, push)) return DIDUpdateMetadataResponse([STD_UTX], [STD_TX], SpendBundle([], G2Element()), uint32(wallet_id)) inst_rpc_client = DidUpdateMetadataRpcClient() # pylint: disable=no-value-for-parameter @@ -203,7 +205,7 @@ async def update_did_metadata( assert_list = [f"Successfully updated DID wallet ID: {w_id}, Spend Bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "update_did_metadata": [(w_id, {"test": True}, DEFAULT_TX_CONFIG.override(reuse_puzhash=True))], + "update_did_metadata": [(w_id, {"test": True}, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -252,9 +254,9 @@ def test_did_message_spend(capsys: object, get_test_cli_clients: Tuple[TestRpcCl # set RPC Client class DidMessageSpendRpcClient(TestWalletRpcClient): async def did_message_spend( - self, wallet_id: int, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...] + self, wallet_id: int, tx_config: TXConfig, extra_conditions: Tuple[Condition, ...], push: bool ) -> DIDMessageSpendResponse: - self.add_to_log("did_message_spend", (wallet_id, tx_config, extra_conditions)) + self.add_to_log("did_message_spend", (wallet_id, tx_config, extra_conditions, True)) return DIDMessageSpendResponse([STD_UTX], [STD_TX], SpendBundle([], G2Element())) inst_rpc_client = DidMessageSpendRpcClient() # pylint: disable=no-value-for-parameter @@ -286,6 +288,7 @@ async def did_message_spend( *(CreateCoinAnnouncement(ann) for ann in c_announcements), *(CreatePuzzleAnnouncement(ann) for ann in puz_announcements), ), + True, ) ], } @@ -304,8 +307,9 @@ async def did_transfer_did( fee: int, with_recovery: bool, tx_config: TXConfig, + push: bool, ) -> DIDTransferDIDResponse: - self.add_to_log("did_transfer_did", (wallet_id, address, fee, with_recovery, tx_config)) + self.add_to_log("did_transfer_did", (wallet_id, address, fee, with_recovery, tx_config, push)) return DIDTransferDIDResponse( [STD_UTX], [STD_TX], @@ -340,6 +344,8 @@ async def did_transfer_did( ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "did_transfer_did": [(w_id, t_address, 500000000000, True, DEFAULT_TX_CONFIG.override(reuse_puzhash=True))], + "did_transfer_did": [ + (w_id, t_address, 500000000000, True, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), True) + ], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 7edbc1fe56e8..20bd8480db73 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -650,12 +650,13 @@ def did_cmd() -> None: show_default=True, callback=validate_fee, ) +@tx_out_cmd def did_create_wallet_cmd( - wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: int, fee: str -) -> None: + wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: int, fee: str, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import create_did_wallet - asyncio.run(create_did_wallet(wallet_rpc_port, fingerprint, Decimal(fee), name, amount)) + return asyncio.run(create_did_wallet(wallet_rpc_port, fingerprint, Decimal(fee), name, amount, push=push)) @did_cmd.command("sign_message", help="Sign a message by a DID") @@ -750,12 +751,13 @@ def did_get_details_cmd(wallet_rpc_port: Optional[int], fingerprint: int, coin_i is_flag=True, default=False, ) +@tx_out_cmd def did_update_metadata_cmd( - wallet_rpc_port: Optional[int], fingerprint: int, id: int, metadata: str, reuse: bool -) -> None: + wallet_rpc_port: Optional[int], fingerprint: int, id: int, metadata: str, reuse: bool, push: bool +) -> List[TransactionRecord]: from .wallet_funcs import update_did_metadata - asyncio.run(update_did_metadata(wallet_rpc_port, fingerprint, id, metadata, reuse)) + return asyncio.run(update_did_metadata(wallet_rpc_port, fingerprint, id, metadata, reuse, push=push)) @did_cmd.command("find_lost", help="Find the did you should own and recovery the DID wallet") @@ -830,13 +832,15 @@ def did_find_lost_cmd( type=str, required=False, ) +@tx_out_cmd def did_message_spend_cmd( wallet_rpc_port: Optional[int], fingerprint: int, id: int, puzzle_announcements: Optional[str], coin_announcements: Optional[str], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import did_message_spend puzzle_list: List[str] = [] @@ -849,7 +853,7 @@ def did_message_spend_cmd( bytes.fromhex(announcement) except ValueError: print("Invalid puzzle announcement format, should be a list of hex strings.") - return + return [] if coin_announcements is not None: try: coin_list = coin_announcements.split(",") @@ -858,9 +862,9 @@ def did_message_spend_cmd( bytes.fromhex(announcement) except ValueError: print("Invalid coin announcement format, should be a list of hex strings.") - return + return [] - asyncio.run(did_message_spend(wallet_rpc_port, fingerprint, id, puzzle_list, coin_list)) + return asyncio.run(did_message_spend(wallet_rpc_port, fingerprint, id, puzzle_list, coin_list, push=push)) @did_cmd.command("transfer", help="Transfer a DID") @@ -892,6 +896,7 @@ def did_message_spend_cmd( is_flag=True, default=False, ) +@tx_out_cmd def did_transfer_did( wallet_rpc_port: Optional[int], fingerprint: int, @@ -900,10 +905,11 @@ def did_transfer_did( reset_recovery: bool, fee: str, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import transfer_did - asyncio.run( + return asyncio.run( transfer_did( wallet_rpc_port, fingerprint, @@ -912,6 +918,7 @@ def did_transfer_did( target_address, reset_recovery is False, True if reuse else None, + push=push, ) ) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 61aeb5a3f775..74111714580d 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -923,18 +923,20 @@ async def print_balances( async def create_did_wallet( - wallet_rpc_port: Optional[int], fp: Optional[int], d_fee: Decimal, name: Optional[str], amount: int -) -> None: + wallet_rpc_port: Optional[int], fp: Optional[int], d_fee: Decimal, name: Optional[str], amount: int, push: bool +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): fee: int = int(d_fee * units["chia"]) try: - response = await wallet_client.create_new_did_wallet(amount, fee, name) + response = await wallet_client.create_new_did_wallet(amount, fee, name, push=push) wallet_id = response["wallet_id"] my_did = response["my_did"] print(f"Successfully created a DID wallet with name {name} and id {wallet_id} on key {fingerprint}") print(f"Successfully created a DID {my_did} in the newly created DID wallet") + return [] # TODO: fix this endpoint to return transactions except Exception as e: print(f"Failed to create DID wallet: {e}") + return [] async def did_set_wallet_name(wallet_rpc_port: Optional[int], fp: Optional[int], wallet_id: int, name: str) -> None: @@ -985,7 +987,8 @@ async def update_did_metadata( did_wallet_id: int, metadata: str, reuse_puzhash: bool, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: response = await wallet_client.update_did_metadata( @@ -995,12 +998,15 @@ async def update_did_metadata( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), ) - print( - f"Successfully updated DID wallet ID: {response.wallet_id}, " - f"Spend Bundle: {response.spend_bundle.to_json_dict()}" - ) + if push: + print( + f"Successfully updated DID wallet ID: {response.wallet_id}, " + f"Spend Bundle: {response.spend_bundle.to_json_dict()}" + ) + return response.transactions except Exception as e: print(f"Failed to update DID metadata: {e}") + return [] async def did_message_spend( @@ -1009,7 +1015,8 @@ async def did_message_spend( did_wallet_id: int, puzzle_announcements: List[str], coin_announcements: List[str], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: response = await wallet_client.did_message_spend( @@ -1019,10 +1026,13 @@ async def did_message_spend( *(CreateCoinAnnouncement(hexstr_to_bytes(ca)) for ca in coin_announcements), *(CreatePuzzleAnnouncement(hexstr_to_bytes(pa)) for pa in puzzle_announcements), ), + push=push, ) print(f"Message Spend Bundle: {response.spend_bundle.to_json_dict()}") + return response.transactions except Exception as e: print(f"Failed to update DID metadata: {e}") + return [] async def transfer_did( @@ -1033,7 +1043,8 @@ async def transfer_did( target_address: str, with_recovery: bool, reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: fee: int = int(d_fee * units["chia"]) async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): @@ -1046,12 +1057,16 @@ async def transfer_did( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) - print(f"Successfully transferred DID to {target_address}") + if push: + print(f"Successfully transferred DID to {target_address}") print(f"Transaction ID: {response.transaction_id.hex()}") print(f"Transaction: {response.transaction.to_json_dict_convenience(config)}") + return response.transactions except Exception as e: print(f"Failed to transfer DID: {e}") + return [] async def find_lost_did( diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index f115189e6b48..e1d16aa97d59 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -763,6 +763,9 @@ async def create_new_wallet( if type(request["metadata"]) is dict: metadata = request["metadata"] + if not push: + raise ValueError("Creation of DID wallet must be automatically pushed for now.") + async with self.service.wallet_state_manager.lock: did_wallet_name: str = request.get("wallet_name", None) if did_wallet_name is not None: diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 79ac3cb78b7a..8ebb0dc74c79 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -413,6 +413,7 @@ async def create_new_did_wallet( name: Optional[str] = "DID Wallet", backup_ids: List[str] = [], required_num: int = 0, + push: bool = True, ) -> Dict[str, Any]: request = { "wallet_type": "did_wallet", @@ -422,6 +423,7 @@ async def create_new_did_wallet( "amount": amount, "fee": fee, "wallet_name": name, + "push": push, } response = await self.fetch("create_new_wallet", request) return response From 2a7e0c87b6c8d97d5d0b37e9ac6bff32e5bac6f5 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 20 May 2024 20:46:46 +0100 Subject: [PATCH 194/274] update vault_p2_recovery.clsp and drivers --- .../puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/vault_p2_recovery.clsp | 26 ++++--- .../wallet/puzzles/vault_p2_recovery.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 74 +++++++++++++++---- 4 files changed, 78 insertions(+), 26 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index a4f95708f981..a657ec1091d1 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -62,7 +62,7 @@ "singleton_top_layer_v1_1": "7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f", "standard_vc_backdoor_puzzle": "fbce76408ebaf9b3d0b8cd90cc68607755eeca67cd7432d5eea85f3f498cc002", "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", - "vault_p2_recovery": "5bcfac8571e7464ab8852794b37b428ce23c55976d0a6b092227fd0a6b0e07c5", + "vault_p2_recovery": "59d20b29c583990dca222583cf6fa6b03189841b18ad866e1271aeeec9774828", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp index 847a2f8e9291..046faeb2bfd4 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp @@ -47,19 +47,23 @@ ) ) - (list - (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) - (list CREATE_COIN - (create_recovery_puzzlehash - P2_1_OF_N_MOD_HASH - FINISH_RECOVERY_MOD_HASH - P2_SECP_PUZZLEHASH - TIMELOCK - recovery_conditions + (let + ( + (recovery_puzzlehash + (create_recovery_puzzlehash + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + TIMELOCK + recovery_conditions + ) ) - my_amount ) - (list ASSERT_MY_AMOUNT my_amount) + (list + (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) + (list CREATE_COIN recovery_puzzlehash my_amount (list recovery_puzzlehash)) + (list ASSERT_MY_AMOUNT my_amount) + ) ) ) diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex index 895752c239de..dbd0bee1d90f 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ffff0132ffff04ff2fffff04ffff02ff08ffff04ff02ffff04ff82017fff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ff8200bfffff0180808080ffff04ffff04ffff0149ffff04ff8200bfffff01808080ffff0180808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 +ff02ffff01ff04ffff04ffff0132ffff04ffff05ffff06ffff06ffff06ffff06ff018080808080ffff04ffff02ff08ffff04ff02ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff0180808080808080ffff04ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff018080ffff018080808080ffff04ffff04ffff0149ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff0180808080808080ffff01808080ffff0180808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index c0cab29158c1..341b5070c190 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -1,15 +1,16 @@ from __future__ import annotations -from typing import Optional +from typing import List, Optional, Tuple from chia_rs import G1Element from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_spend import CoinSpend from chia.util.ints import uint32, uint64 from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.load_clvm import load_clvm -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE, puzzle_hash_for_pk +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE from chia.wallet.puzzles.singleton_top_layer_v1_1 import ( SINGLETON_LAUNCHER_HASH, SINGLETON_MOD, @@ -100,14 +101,10 @@ def get_vault_full_puzzle_hash(launcher_id: bytes32, inner_puzzle_hash: bytes32) return puzzle_hash -def get_recovery_conditions(bls_pk: G1Element, amount: uint64) -> Program: - puzzle_hash = puzzle_hash_for_pk(bls_pk) - recovery_conditions: Program = Program.to([[51, puzzle_hash, amount]]) - return recovery_conditions - - -def get_recovery_finish_puzzle(bls_pk: G1Element, timelock: uint64, amount: uint64) -> Program: - recovery_condition = get_recovery_conditions(bls_pk, amount) +def get_recovery_finish_puzzle( + new_vault_inner_puzhash: bytes32, timelock: uint64, amount: uint64, memos: List[bytes] +) -> Program: + recovery_condition = Program.to([[51, new_vault_inner_puzhash, amount, memos]]) return RECOVERY_FINISH_MOD.curry(timelock, recovery_condition) @@ -132,10 +129,61 @@ def match_vault_puzzle(mod: Program, curried_args: Program) -> bool: return False +def match_recovery_puzzle(mod: Program, curried_args: Program, solution: Program) -> bool: + if match_vault_puzzle(mod, curried_args): + try: + delegated_puz = solution.at("rrfrf") + if delegated_puz.uncurry()[0] == P2_RECOVERY_MOD: + return True + except ValueError: + pass + return False + + +def get_recovery_puzzle_from_spend(spend: CoinSpend) -> Program: + solution = spend.solution.to_program() + delegated_puz = solution.at("rrfrf") + recovery_args = delegated_puz.uncurry()[1] + secp_puzzle_hash = bytes32(recovery_args.at("rrf").as_atom()) + timelock = uint64(recovery_args.at("rrrrf").as_int()) + new_vault_condition = solution.at("rrfrrfrff") + new_vault_inner_puzhash = bytes32(new_vault_condition.at("rf").as_atom()) + memos = new_vault_condition.at("rrrf") + recovery_finish_puzzle = get_recovery_finish_puzzle(new_vault_inner_puzhash, timelock, spend.coin.amount, memos) + recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) + return recovery_inner_puzzle + + +def get_new_vault_info_from_spend(spend: CoinSpend) -> Tuple[bytes, bytes32, Optional[G1Element], Optional[uint64]]: + solution = spend.solution.to_program() + delegated_puz = solution.at("rrfrf") + conds = delegated_puz.at("rrfrrfrfr") + for cond in conds.as_iter(): + if (cond.at("f").as_int() == 51) and (cond.at("rrf").as_int() == 1): + memos = cond.at("rrrf") if cond.list_len() == 4 else None + assert memos is not None + secp_pk = memos.at("f").as_atom() + hidden_puzzle_hash = memos.at("rf").as_atom() + if memos.list_len() > 2: + bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) + timelock = uint64(memos.at("rrrf").as_int()) + else: + bls_pk = None + timelock = None + break + return secp_pk, hidden_puzzle_hash, bls_pk, timelock + + +def match_finish_spend(spend: CoinSpend) -> bool: + solution = spend.solution.to_program() + delegated_puz = solution.at("rrfrf") + return delegated_puz.uncurry()[0] == RECOVERY_FINISH_MOD + + # SOLUTIONS -def get_recovery_solution(amount: uint64, bls_pk: G1Element) -> Program: - recovery_conditions = get_recovery_conditions(bls_pk, amount) - recovery_solution: Program = Program.to([amount, recovery_conditions]) +def get_recovery_solution(new_vault_inner_puzhash: bytes32, amount: uint64, memos: List[bytes]) -> Program: + recovery_condition = Program.to([[51, new_vault_inner_puzhash, amount, memos]]) + recovery_solution: Program = Program.to([amount, recovery_condition]) return recovery_solution From d4edcca60a4296b6b1cf0fcc967057d299a2d892 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 20 May 2024 20:47:27 +0100 Subject: [PATCH 195/274] update recovery process in wallet --- chia/wallet/vault/vault_wallet.py | 100 +++++++++++++++++++----- tests/wallet/vault/test_vault_wallet.py | 89 ++++++++++++++++++--- 2 files changed, 158 insertions(+), 31 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 615185204513..347688f15da5 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element +from clvm.casts import int_to_bytes from ecdsa.keys import SigningKey from typing_extensions import Unpack @@ -50,11 +51,13 @@ from chia.wallet.vault.vault_drivers import ( construct_p2_delegated_secp, construct_vault_merkle_tree, + get_new_vault_info_from_spend, get_p2_singleton_puzzle, get_p2_singleton_puzzle_hash, get_recovery_finish_puzzle, get_recovery_inner_puzzle, get_recovery_puzzle, + get_recovery_puzzle_from_spend, get_recovery_solution, get_vault_full_puzzle, get_vault_full_solution, @@ -63,6 +66,8 @@ get_vault_inner_puzzle_hash, get_vault_inner_solution, get_vault_proof, + match_finish_spend, + match_recovery_puzzle, match_vault_puzzle, ) from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo @@ -500,7 +505,15 @@ def derivation_for_index(self, index: int) -> List[DerivationRecord]: ) return [record] - async def create_recovery_spends(self) -> List[TransactionRecord]: + async def create_recovery_spends( + self, + secp_pk: bytes, + hidden_puzzle_hash: bytes32, + genesis_challenge: bytes32, + tx_config: TXConfig, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, + ) -> List[TransactionRecord]: """ Returns two tx records 1. Recover the vault which can be taken to the appropriate BLS wallet for signing @@ -516,6 +529,15 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: amount = uint64(self.vault_info.coin.amount) vault_coin_state = (await wallet_node.get_coin_state([vault_coin.name()], peer))[0] assert vault_coin_state.spent_height is None + + # get the new vault puzhash we'll recover to + new_vault_inner_puzhash = get_vault_inner_puzzle_hash( + secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock + ) + memos = [secp_pk, hidden_puzzle_hash] + if bls_pk: + memos.extend([bls_pk.to_bytes(), int_to_bytes(timelock)]) + # Generate the current inner puzzle inner_puzzle = get_vault_inner_puzzle( self.vault_info.pubkey, @@ -537,7 +559,7 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: ) recovery_puzzle_hash = recovery_puzzle.get_tree_hash() assert isinstance(self.vault_info.recovery_info.bls_pk, G1Element) - recovery_solution = get_recovery_solution(amount, self.vault_info.recovery_info.bls_pk) + recovery_solution = get_recovery_solution(new_vault_inner_puzhash, amount, memos) merkle_tree = construct_vault_merkle_tree(secp_puzzle_hash, recovery_puzzle_hash) proof = get_vault_proof(merkle_tree, recovery_puzzle_hash) @@ -552,19 +574,24 @@ async def create_recovery_spends(self) -> List[TransactionRecord]: # 2. Generate the Finish Recovery Spend assert isinstance(self.vault_info.recovery_info.bls_pk, G1Element) assert isinstance(self.vault_info.recovery_info.timelock, uint64) + recovery_finish_puzzle = get_recovery_finish_puzzle( - self.vault_info.recovery_info.bls_pk, self.vault_info.recovery_info.timelock, amount + new_vault_inner_puzhash, self.vault_info.recovery_info.timelock, amount, memos ) recovery_finish_solution = Program.to([]) recovery_inner_puzzle = get_recovery_inner_puzzle(secp_puzzle_hash, recovery_finish_puzzle.get_tree_hash()) full_recovery_puzzle = get_vault_full_puzzle(self.launcher_id, recovery_inner_puzzle) - recovery_coin = Coin(self.vault_info.coin.name(), full_recovery_puzzle.get_tree_hash(), amount) + recovery_coin = Coin(vault_coin.name(), full_recovery_puzzle.get_tree_hash(), amount) recovery_solution = get_vault_inner_solution(recovery_finish_puzzle, recovery_finish_solution, proof) - lineage = LineageProof(self.vault_info.coin.name(), inner_puzzle.get_tree_hash(), amount) + lineage = LineageProof(vault_coin.parent_coin_info, inner_puzzle.get_tree_hash(), amount) full_recovery_solution = get_vault_full_solution(lineage, amount, recovery_solution) finish_spend = SpendBundle( [make_spend(recovery_coin, full_recovery_puzzle, full_recovery_solution)], G2Element() ) + new_vault_coin_id = finish_spend.additions()[0].name() + await self.wallet_state_manager.add_interested_coin_ids( + [recovery_coin.name(), new_vault_coin_id], [self.id(), self.id()] + ) # make the tx records recovery_tx = TransactionRecord( @@ -680,18 +707,50 @@ async def update_vault_singleton( ) -> None: hints, _ = compute_spend_hints_and_additions(coin_spend) inner_puzzle_hash = hints[coin_state.coin.name()].hint - assert inner_puzzle_hash - dr = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(inner_puzzle_hash) - assert dr is not None - hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(dr.index).get_tree_hash() - next_inner_puzzle = get_vault_inner_puzzle( - self.vault_info.pubkey, - self.wallet_state_manager.constants.GENESIS_CHALLENGE, - hidden_puzzle_hash, - self.vault_info.recovery_info.bls_pk, - self.vault_info.recovery_info.timelock, - ) + dr = None + replace_key = False + replace_recovery = False + if inner_puzzle_hash is not None: + dr = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(inner_puzzle_hash) + + if dr is None: + # We're in recovery mode + puzzle, curried_args = coin_spend.puzzle_reveal.to_program().uncurry() + solution = coin_spend.solution.to_program() + if match_recovery_puzzle(puzzle, curried_args, solution): + # recreate the vault inner puz with the recovery settings + # hidden_puzzle_hash = None + next_inner_puzzle = get_recovery_puzzle_from_spend(coin_spend) + elif match_finish_spend(coin_spend): + # We've finished the recovery and have a new key + ( + new_secp_pk, + hidden_puzzle_hash, + new_bls_pk, + new_timelock, + ) = get_new_vault_info_from_spend(coin_spend) + next_inner_puzzle = get_vault_inner_puzzle( + new_secp_pk, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + new_bls_pk, + new_timelock, + ) + if new_bls_pk: + new_recovery_info = RecoveryInfo(bls_pk=new_bls_pk, timelock=new_timelock) + replace_recovery = True + replace_key = True + else: + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(dr.index).get_tree_hash() + next_inner_puzzle = get_vault_inner_puzzle( + self.vault_info.pubkey, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + hidden_puzzle_hash, + self.vault_info.recovery_info.bls_pk, + self.vault_info.recovery_info.timelock, + ) + assert get_vault_full_puzzle(self.launcher_id, next_inner_puzzle).get_tree_hash() == coin_state.coin.puzzle_hash # get the parent state to create lineage proof wallet_node: Any = self.wallet_state_manager.wallet_node peer = wallet_node.get_full_node_peer() @@ -703,14 +762,15 @@ async def update_vault_singleton( lineage_proof = LineageProof( parent_state.coin.parent_coin_info, parent_inner_puzzle_hash, parent_state.coin.amount ) + # assert hidden_puzzle_hash is not None new_vault_info = VaultInfo( coin_state.coin, - self.vault_info.pubkey, - hidden_puzzle_hash, + self.vault_info.pubkey if not replace_key else new_secp_pk, + hidden_puzzle_hash if dr else self.vault_info.hidden_puzzle_hash, next_inner_puzzle.get_tree_hash(), lineage_proof, - self.vault_info.is_recoverable, - self.vault_info.recovery_info, + self.vault_info.is_recoverable if not replace_key else replace_recovery, + self.vault_info.recovery_info if not replace_recovery else new_recovery_info, ) await self.update_vault_store(new_vault_info, coin_spend) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 25e0a548cef0..8e53985bc774 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -38,7 +38,7 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b if with_recovery: bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] bls_pk = bytes.fromhex(bls_pk_hex) - timelock = uint64(1000) + timelock = uint64(10) hidden_puzzle_index = uint32(0) res = await client.vault_create( SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG @@ -119,14 +119,6 @@ async def test_vault_creation( p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) - if with_recovery: - assert wallet.vault_info.recovery_info is not None - [recovery_spend, finish_spend] = await wallet.create_recovery_spends() - assert recovery_spend - assert finish_spend - else: - assert not wallet.vault_info.is_recoverable - coins_to_create = 2 funding_amount = uint64(1000000000) funding_wallet = wallet_environments.environments[1].xch_wallet @@ -250,6 +242,14 @@ async def test_vault_recovery( await setup_function(wallet_environments, with_recovery) env = wallet_environments.environments[0] assert isinstance(env.xch_wallet, Vault) + recovery_seed = b"recovery_chia_secp" + RECOVERY_SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(recovery_seed), hashfunc=sha256) + RECOVERY_SECP_PK = RECOVERY_SECP_SK.verifying_key.to_string("compressed") + client = wallet_environments.environments[1].rpc_client + fingerprint = (await client.get_public_keys())[0] + bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] + bls_pk = bytes.fromhex(bls_pk_hex) + timelock = uint64(10) wallet: Vault = env.xch_wallet await wallet.sync_vault_launcher() @@ -289,5 +289,72 @@ async def test_vault_recovery( ], ) - recovery_txs = await env.rpc_client.vault_recovery(wallet_id=wallet.id()) - assert recovery_txs + [initiate_tx, finish_tx] = await env.rpc_client.vault_recovery( + wallet_id=wallet.id(), + secp_pk=RECOVERY_SECP_PK, + hp_index=uint32(0), + tx_config=DEFAULT_TX_CONFIG, + bls_pk=bls_pk, + timelock=timelock, + ) + assert initiate_tx.spend_bundle is not None + spends = [Spend.from_coin_spend(spend) for spend in initiate_tx.spend_bundle.coin_spends] + signing_info = await wallet_environments.environments[1].rpc_client.gather_signing_info(GatherSigningInfo(spends)) + signing_responses = await funding_wallet.execute_signing_instructions(signing_info.signing_instructions) + signed_response = await funding_wallet.apply_signatures(spends, signing_responses) + + await funding_wallet.wallet_state_manager.submit_transactions([signed_response]) + + vault_coin = wallet.vault_info.coin + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + ), + ], + ) + + recovery_coin = wallet.vault_info.coin + assert recovery_coin.parent_coin_info == vault_coin.name() + + wallet_environments.full_node.time_per_block = 100 + await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=2, guarantee_transaction_blocks=True) + + assert finish_tx.spend_bundle is not None + spends = [Spend.from_coin_spend(spend) for spend in finish_tx.spend_bundle.coin_spends] + signing_info = await wallet_environments.environments[1].rpc_client.gather_signing_info(GatherSigningInfo(spends)) + signing_responses = await funding_wallet.execute_signing_instructions(signing_info.signing_instructions) + signed_response = await funding_wallet.apply_signatures(spends, signing_responses) + await funding_wallet.wallet_state_manager.submit_transactions([signed_response]) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "init": True, + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + ), + ], + ) + + recovered_coin = wallet.vault_info.coin + assert recovered_coin.parent_coin_info == recovery_coin.name() From 7acf62450c08044422657104d03d59ea5f48194c Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 20 May 2024 20:48:40 +0100 Subject: [PATCH 196/274] add recovery API and CLI --- chia/cmds/vault.py | 123 +++++++++++++++++++++++++++++++- chia/cmds/vault_funcs.py | 48 +++++++++++-- chia/rpc/wallet_rpc_api.py | 14 +++- chia/rpc/wallet_rpc_client.py | 18 ++++- tests/cmds/wallet/test_vault.py | 25 +++++-- 5 files changed, 213 insertions(+), 15 deletions(-) diff --git a/chia/cmds/vault.py b/chia/cmds/vault.py index fcd1b4f98363..a6fcde48996e 100644 --- a/chia/cmds/vault.py +++ b/chia/cmds/vault.py @@ -2,7 +2,7 @@ import asyncio from decimal import Decimal -from typing import Optional +from typing import Optional, Sequence import click @@ -66,6 +66,34 @@ def vault_cmd(ctx: click.Context) -> None: callback=validate_fee, ) @click.option("-n", "--name", help="Set the vault name", type=str) +@click.option( + "-ma", + "--min-coin-amount", + help="Ignore coins worth less then this much XCH or CAT units", + type=str, + required=False, + default="0", +) +@click.option( + "-l", + "--max-coin-amount", + help="Ignore coins worth more then this much XCH or CAT units", + type=str, + required=False, + default=None, +) +@click.option( + "--exclude-coin", + "coins_to_exclude", + multiple=True, + help="Exclude this coin from being spent.", +) +@click.option( + "--reuse", + help="Reuse existing address for the change.", + is_flag=True, + default=False, +) def vault_create_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -75,6 +103,10 @@ def vault_create_cmd( hidden_puzzle_index: Optional[int], fee: str, name: Optional[str], + min_coin_amount: str, + max_coin_amount: Optional[str], + coins_to_exclude: Sequence[str], + reuse: bool, ) -> None: from .vault_funcs import create_vault @@ -91,6 +123,10 @@ def vault_create_cmd( hidden_puzzle_index, Decimal(fee), name, + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=coins_to_exclude, + reuse_puzhash=True if reuse else None, ) ) @@ -105,6 +141,37 @@ def vault_create_cmd( ) @options.create_fingerprint() @click.option("-i", "--wallet-id", help="Vault Wallet ID", type=int, required=True, default=1) +@click.option( + "-pk", + "--public-key", + help="SECP public key", + type=str, + required=True, +) +@click.option( + "-i", + "--hidden-puzzle-index", + help="Starting index for hidden puzzle", + type=int, + required=False, + default=0, +) +@click.option( + "-rk", + "--recovery-public-key", + help="BLS public key for vault recovery", + type=str, + required=False, + default=None, +) +@click.option( + "-rt", + "--recovery-timelock", + help="Timelock for vault recovery (in seconds)", + type=int, + required=False, + default=None, +) @click.option( "-ri", "--recovery-initiate-file", @@ -121,13 +188,65 @@ def vault_create_cmd( required=True, default="finish_recovery.json", ) +@click.option( + "-ma", + "--min-coin-amount", + help="Ignore coins worth less then this much XCH or CAT units", + type=str, + required=False, + default="0", +) +@click.option( + "-l", + "--max-coin-amount", + help="Ignore coins worth more then this much XCH or CAT units", + type=str, + required=False, + default=None, +) +@click.option( + "--exclude-coin", + "coins_to_exclude", + multiple=True, + help="Exclude this coin from being spent.", +) +@click.option( + "--reuse", + help="Reuse existing address for the change.", + is_flag=True, + default=False, +) def vault_recover_cmd( wallet_rpc_port: Optional[int], fingerprint: int, wallet_id: int, + public_key: str, + hidden_puzzle_index: int, + recovery_public_key: Optional[str], + recovery_timelock: Optional[int], recovery_initiate_file: str, recovery_finish_file: str, + min_coin_amount: str, + max_coin_amount: Optional[str], + coins_to_exclude: Sequence[str], + reuse: bool, ) -> None: from .vault_funcs import recover_vault - asyncio.run(recover_vault(wallet_rpc_port, fingerprint, wallet_id, recovery_initiate_file, recovery_finish_file)) + asyncio.run( + recover_vault( + wallet_rpc_port, + fingerprint, + wallet_id, + public_key, + hidden_puzzle_index, + recovery_public_key, + recovery_timelock, + recovery_initiate_file, + recovery_finish_file, + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=coins_to_exclude, + reuse_puzhash=True if reuse else None, + ) + ) diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index 956184902b20..ea1e191215ab 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -2,9 +2,9 @@ import json from decimal import Decimal -from typing import Optional +from typing import Optional, Sequence -from chia.cmds.cmds_util import get_wallet_client +from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client from chia.cmds.units import units from chia.util.ints import uint32, uint64 @@ -18,17 +18,27 @@ async def create_vault( hidden_puzzle_index: int, d_fee: Decimal, name: Optional[str], + min_coin_amount: Optional[str], + max_coin_amount: Optional[str], + excluded_coin_ids: Sequence[str], + reuse_puzhash: Optional[bool], ) -> None: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): fee: int = int(d_fee * units["chia"]) assert hidden_puzzle_index >= 0 + tx_config = CMDTXConfigLoader( + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=list(excluded_coin_ids), + reuse_puzhash=reuse_puzhash, + ).to_tx_config(units["chia"], config, fingerprint) if timelock is not None: assert timelock > 0 try: await wallet_client.vault_create( bytes.fromhex(public_key), uint32(hidden_puzzle_index), - config, + tx_config, bytes.fromhex(recovery_public_key) if recovery_public_key else None, uint64(timelock) if timelock else None, uint64(fee), @@ -40,11 +50,39 @@ async def create_vault( async def recover_vault( - wallet_rpc_port: Optional[int], fingerprint: Optional[int], wallet_id: int, initiate_file: str, finish_file: str + wallet_rpc_port: Optional[int], + fingerprint: Optional[int], + wallet_id: int, + public_key: str, + hidden_puzzle_index: int, + recovery_public_key: Optional[str], + timelock: Optional[int], + initiate_file: str, + finish_file: str, + min_coin_amount: Optional[str], + max_coin_amount: Optional[str], + excluded_coin_ids: Sequence[str], + reuse_puzhash: Optional[bool], ) -> None: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): + assert hidden_puzzle_index >= 0 + if timelock is not None: + assert timelock > 0 + tx_config = CMDTXConfigLoader( + min_coin_amount=min_coin_amount, + max_coin_amount=max_coin_amount, + excluded_coin_ids=list(excluded_coin_ids), + reuse_puzhash=reuse_puzhash, + ).to_tx_config(units["chia"], config, fingerprint) try: - response = await wallet_client.vault_recovery(uint32(wallet_id)) + response = await wallet_client.vault_recovery( + uint32(wallet_id), + bytes.fromhex(public_key), + uint32(hidden_puzzle_index), + tx_config, + bytes.fromhex(recovery_public_key) if recovery_public_key else None, + uint64(timelock) if timelock else None, + ) with open(initiate_file, "w") as f: json.dump(response[0].to_json_dict(), f, indent=4) print(f"Initiate Recovery transaction written to: {initiate_file}") diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 64d127fd3ece..16026037349c 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -4620,11 +4620,21 @@ async def vault_create( "transactions": [vault_record.to_json_dict_convenience(self.service.config)], } - async def vault_recovery(self, request: Dict[str, Any]) -> EndpointResult: + async def vault_recovery(self, request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG) -> EndpointResult: """ Initiate Vault Recovery """ wallet_id = uint32(request["wallet_id"]) wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=Vault) - recovery_txs = await wallet.create_recovery_spends() + secp_pk = bytes.fromhex(str(request.get("secp_pk"))) + hp_index = request.get("hp_index", 0) + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash() + bls_str = request.get("bls_pk") + bls_pk = G1Element.from_bytes(bytes.fromhex(str(bls_str))) if bls_str else None + timelock_int = request.get("timelock") + timelock = uint64(timelock_int) if timelock_int else None + genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE + recovery_txs = await wallet.create_recovery_spends( + secp_pk, hidden_puzzle_hash, genesis_challenge, tx_config, bls_pk=bls_pk, timelock=timelock + ) return {"transactions": [tx.to_json_dict_convenience(self.service.config) for tx in recovery_txs]} diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index df1bc025f1ab..7540cafbe50d 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1674,9 +1674,23 @@ async def vault_create( ) return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] - async def vault_recovery(self, wallet_id: uint32) -> List[TransactionRecord]: + async def vault_recovery( + self, + wallet_id: uint32, + secp_pk: bytes, + hp_index: uint32, + tx_config: TXConfig, + bls_pk: Optional[bytes] = None, + timelock: Optional[uint64] = None, + ) -> List[TransactionRecord]: response = await self.fetch( "vault_recovery", - {"wallet_id": wallet_id}, + { + "wallet_id": wallet_id, + "secp_pk": secp_pk.hex(), + "hp_index": hp_index, + "bls_pk": bls_pk.hex() if bls_pk else None, + "timelock": timelock, + }, ) return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] diff --git a/tests/cmds/wallet/test_vault.py b/tests/cmds/wallet/test_vault.py index 92cadf121249..9bc06792ac5a 100644 --- a/tests/cmds/wallet/test_vault.py +++ b/tests/cmds/wallet/test_vault.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import List, Optional, Tuple -from chia_rs import Coin, G2Element +from chia_rs import Coin, G1Element, G2Element from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint8, uint32, uint64 @@ -55,9 +55,9 @@ async def vault_create( test_rpc_clients.wallet_rpc_client = inst_rpc_client pk = get_bytes32(0).hex() recovery_pk = get_bytes32(1).hex() - timelock = 100 - hidden_puzzle_index = 10 - fee = 0.1 + timelock = "100" + hidden_puzzle_index = "10" + fee = "0.1" command_args = [ "vault", "create", @@ -84,6 +84,11 @@ class CreateVaultRpcClient(TestWalletRpcClient): async def vault_recovery( self, wallet_id: uint32, + secp_pk: bytes, + hp_index: uint32, + tx_config: TXConfig, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, ) -> List[TransactionRecord]: tx_rec = TransactionRecord( confirmed_at_height=uint32(1), @@ -108,9 +113,21 @@ async def vault_recovery( inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client + pk = get_bytes32(0).hex() + recovery_pk = get_bytes32(1).hex() + timelock = "100" + hidden_puzzle_index = "10" command_args = [ "vault", "recover", + "-pk", + pk, + "-rk", + recovery_pk, + "-rt", + timelock, + "-i", + hidden_puzzle_index, "-ri", "recovery_init.json", "-rf", From 19a70e7be2b3d62efabcf320d009c35c01be8ea6 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 08:21:45 -0700 Subject: [PATCH 197/274] Port `chia wallet nft ...` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_nft.py | 14 ++++++++++---- chia/cmds/wallet.py | 21 +++++++++++++++------ chia/cmds/wallet_funcs.py | 28 ++++++++++++++++++++++------ 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_nft.py b/chia/_tests/cmds/wallet/test_nft.py index b0056518c070..c40627601751 100644 --- a/chia/_tests/cmds/wallet/test_nft.py +++ b/chia/_tests/cmds/wallet/test_nft.py @@ -96,6 +96,7 @@ async def mint_nft( royalty_percentage: int = 0, did_id: Optional[str] = None, reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> NFTMintNFTResponse: self.add_to_log( "mint_nft", @@ -115,6 +116,7 @@ async def mint_nft( royalty_percentage, did_id, reuse_puzhash, + push, ), ) return NFTMintNFTResponse( @@ -171,6 +173,7 @@ async def mint_nft( 500000000000, 0, "0xcee228b8638c67cb66a55085be99fa3b457ae5b56915896f581990f600b2c652", + True, ) ], } @@ -190,8 +193,9 @@ async def add_uri_to_nft( uri: str, fee: int, tx_config: TXConfig, + push: bool, ) -> NFTAddURIResponse: - self.add_to_log("add_uri_to_nft", (wallet_id, nft_coin_id, key, uri, fee, tx_config)) + self.add_to_log("add_uri_to_nft", (wallet_id, nft_coin_id, key, uri, fee, tx_config, push)) return NFTAddURIResponse([STD_UTX], [STD_TX], uint32(wallet_id), SpendBundle([], G2Element())) inst_rpc_client = NFTAddUriRpcClient() # pylint: disable=no-value-for-parameter @@ -223,6 +227,7 @@ async def add_uri_to_nft( "https://example.com/nft", 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), + True, ) ], } @@ -241,8 +246,9 @@ async def transfer_nft( target_address: str, fee: int, tx_config: TXConfig, + push: bool, ) -> NFTTransferNFTResponse: - self.add_to_log("transfer_nft", (wallet_id, nft_coin_id, target_address, fee, tx_config)) + self.add_to_log("transfer_nft", (wallet_id, nft_coin_id, target_address, fee, tx_config, push)) return NFTTransferNFTResponse( [STD_UTX], [STD_TX], @@ -269,11 +275,11 @@ async def transfer_nft( ] # these are various things that should be in the output assert STD_TX.spend_bundle is not None - assert_list = [f"NFT transferred successfully with spend bundle: {STD_TX.spend_bundle.to_json_dict()}"] + assert_list = ["NFT transferred successfully", f"spend bundle: {STD_TX.spend_bundle.to_json_dict()}"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "transfer_nft": [ - (4, nft_coin_id, target_address, 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True)) + (4, nft_coin_id, target_address, 500000000000, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), True) ], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 20bd8480db73..2184752cf6d9 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -1016,6 +1016,7 @@ def nft_sign_message(wallet_rpc_port: Optional[int], fingerprint: int, nft_id: s is_flag=True, default=False, ) +@tx_out_cmd def nft_mint_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1034,7 +1035,8 @@ def nft_mint_cmd( fee: str, royalty_percentage_fraction: int, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import mint_nft if metadata_uris is None: @@ -1047,7 +1049,7 @@ def nft_mint_cmd( else: license_uris_list = [lu.strip() for lu in license_uris.split(",")] - asyncio.run( + return asyncio.run( mint_nft( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1066,6 +1068,7 @@ def nft_mint_cmd( d_fee=Decimal(fee), royalty_percentage=royalty_percentage_fraction, reuse_puzhash=True if reuse else None, + push=push, ) ) @@ -1099,6 +1102,7 @@ def nft_mint_cmd( is_flag=True, default=False, ) +@tx_out_cmd def nft_add_uri_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1109,10 +1113,11 @@ def nft_add_uri_cmd( license_uri: str, fee: str, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import add_uri_to_nft - asyncio.run( + return asyncio.run( add_uri_to_nft( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1123,6 +1128,7 @@ def nft_add_uri_cmd( metadata_uri=metadata_uri, license_uri=license_uri, reuse_puzhash=True if reuse else None, + push=push, ) ) @@ -1154,6 +1160,7 @@ def nft_add_uri_cmd( is_flag=True, default=False, ) +@tx_out_cmd def nft_transfer_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1162,10 +1169,11 @@ def nft_transfer_cmd( target_address: str, fee: str, reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import transfer_nft - asyncio.run( + return asyncio.run( transfer_nft( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1174,6 +1182,7 @@ def nft_transfer_cmd( nft_coin_id=nft_coin_id, target_address=target_address, reuse_puzhash=True if reuse else None, + push=push, ) ) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 74111714580d..af16c47f72f5 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -1125,7 +1125,8 @@ async def mint_nft( d_fee: Decimal, royalty_percentage: int, reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): royalty_address = ( None @@ -1171,11 +1172,15 @@ async def mint_nft( fee, royalty_percentage, did_id, + push=push, ) spend_bundle = mint_response.spend_bundle - print(f"NFT minted Successfully with spend bundle: {spend_bundle}") + if push: + print(f"NFT minted Successfully with spend bundle: {spend_bundle}") + return mint_response.transactions except Exception as e: print(f"Failed to mint NFT: {e}") + return [] async def add_uri_to_nft( @@ -1189,7 +1194,8 @@ async def add_uri_to_nft( metadata_uri: Optional[str], license_uri: Optional[str], reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: if len([x for x in (uri, metadata_uri, license_uri) if x is not None]) > 1: @@ -1215,11 +1221,15 @@ async def add_uri_to_nft( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) spend_bundle = response.spend_bundle.to_json_dict() - print(f"URI added successfully with spend bundle: {spend_bundle}") + if push: + print(f"URI added successfully with spend bundle: {spend_bundle}") + return response.transactions except Exception as e: print(f"Failed to add URI to NFT: {e}") + return [] async def transfer_nft( @@ -1231,7 +1241,8 @@ async def transfer_nft( nft_coin_id: str, target_address: str, reuse_puzhash: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): try: target_address = ensure_valid_address(target_address, allowed_types={AddressType.XCH}, config=config) @@ -1244,11 +1255,16 @@ async def transfer_nft( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) spend_bundle = response.spend_bundle.to_json_dict() - print(f"NFT transferred successfully with spend bundle: {spend_bundle}") + if push: + print("NFT transferred successfully") + print(f"spend bundle: {spend_bundle}") + return response.transactions except Exception as e: print(f"Failed to transfer NFT: {e}") + return [] def print_nft_info(nft: NFTInfo, *, config: Dict[str, Any]) -> None: From a6387733fe2df4335ce4500ccdfa133777440e62 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 20 May 2024 15:29:00 -0700 Subject: [PATCH 198/274] Simplify vault recovery test --- chia/rpc/wallet_rpc_client.py | 4 ++-- tests/wallet/vault/test_vault_wallet.py | 15 ++------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 7540cafbe50d..374fdae85ca4 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -116,9 +116,9 @@ async def get_height_info(self) -> uint32: async def push_tx(self, spend_bundle: SpendBundle) -> Dict[str, Any]: return await self.fetch("push_tx", {"spend_bundle": bytes(spend_bundle).hex()}) - async def push_transactions(self, txs: List[TransactionRecord]) -> Dict[str, Any]: + async def push_transactions(self, txs: List[TransactionRecord], sign: bool = False) -> Dict[str, Any]: transactions = [bytes(tx).hex() for tx in txs] - return await self.fetch("push_transactions", {"transactions": transactions}) + return await self.fetch("push_transactions", {"transactions": transactions, "sign": sign}) async def farm_block(self, address: str) -> Dict[str, Any]: return await self.fetch("farm_block", {"address": address}) diff --git a/tests/wallet/vault/test_vault_wallet.py b/tests/wallet/vault/test_vault_wallet.py index 8e53985bc774..c324facfa7dd 100644 --- a/tests/wallet/vault/test_vault_wallet.py +++ b/tests/wallet/vault/test_vault_wallet.py @@ -297,13 +297,7 @@ async def test_vault_recovery( bls_pk=bls_pk, timelock=timelock, ) - assert initiate_tx.spend_bundle is not None - spends = [Spend.from_coin_spend(spend) for spend in initiate_tx.spend_bundle.coin_spends] - signing_info = await wallet_environments.environments[1].rpc_client.gather_signing_info(GatherSigningInfo(spends)) - signing_responses = await funding_wallet.execute_signing_instructions(signing_info.signing_instructions) - signed_response = await funding_wallet.apply_signatures(spends, signing_responses) - - await funding_wallet.wallet_state_manager.submit_transactions([signed_response]) + await wallet_environments.environments[1].rpc_client.push_transactions([initiate_tx], sign=True) vault_coin = wallet.vault_info.coin @@ -331,12 +325,7 @@ async def test_vault_recovery( wallet_environments.full_node.time_per_block = 100 await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=2, guarantee_transaction_blocks=True) - assert finish_tx.spend_bundle is not None - spends = [Spend.from_coin_spend(spend) for spend in finish_tx.spend_bundle.coin_spends] - signing_info = await wallet_environments.environments[1].rpc_client.gather_signing_info(GatherSigningInfo(spends)) - signing_responses = await funding_wallet.execute_signing_instructions(signing_info.signing_instructions) - signed_response = await funding_wallet.apply_signatures(spends, signing_responses) - await funding_wallet.wallet_state_manager.submit_transactions([signed_response]) + await wallet_environments.environments[1].rpc_client.push_transactions([finish_tx]) await wallet_environments.process_pending_states( [ From 7adbf94fb1ef493d5dca64c4c1cfbf816dcf0312 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 08:37:52 -0700 Subject: [PATCH 199/274] Port `chia wallet notifications send` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_notifications.py | 6 +++--- chia/cmds/wallet.py | 8 ++++++-- chia/cmds/wallet_funcs.py | 11 +++++++---- chia/rpc/wallet_rpc_client.py | 2 ++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_notifications.py b/chia/_tests/cmds/wallet/test_notifications.py index 4e161357559a..d0080e5a5161 100644 --- a/chia/_tests/cmds/wallet/test_notifications.py +++ b/chia/_tests/cmds/wallet/test_notifications.py @@ -21,9 +21,9 @@ def test_notifications_send(capsys: object, get_test_cli_clients: Tuple[TestRpcC # set RPC Client class NotificationsSendRpcClient(TestWalletRpcClient): async def send_notification( - self, target: bytes32, msg: bytes, amount: uint64, fee: uint64 = uint64(0) + self, target: bytes32, msg: bytes, amount: uint64, fee: uint64 = uint64(0), push: bool = True ) -> TransactionRecord: - self.add_to_log("send_notification", (target, msg, amount, fee)) + self.add_to_log("send_notification", (target, msg, amount, fee, push)) class FakeTransactionRecord: def __init__(self, name: str) -> None: @@ -53,7 +53,7 @@ def __init__(self, name: str) -> None: ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { - "send_notification": [(target_ph, bytes(msg, "utf8"), 20000000, 1000000000)], + "send_notification": [(target_ph, bytes(msg, "utf8"), 20000000, 1000000000, True)], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 2184752cf6d9..d5db3becea4c 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -1306,6 +1306,7 @@ def notification_cmd() -> None: ) @click.option("-n", "--message", help="The message of the notification", type=str) @click.option("-m", "--fee", help="The fee for the transaction, in XCH", type=str) +@tx_out_cmd def send_notification_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1313,10 +1314,13 @@ def send_notification_cmd( amount: str, message: str, fee: str, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import send_notification - asyncio.run(send_notification(wallet_rpc_port, fingerprint, Decimal(fee), to_address, message, Decimal(amount))) + return asyncio.run( + send_notification(wallet_rpc_port, fingerprint, Decimal(fee), to_address, message, Decimal(amount), push=push) + ) @notification_cmd.command("get", help="Get notification(s) that are in your wallet") diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index af16c47f72f5..69723e10de16 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -1423,17 +1423,20 @@ async def send_notification( address_str: str, message_hex: str, d_amount: Decimal, -) -> None: + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): address: bytes32 = decode_puzzle_hash(address_str) amount: uint64 = uint64(d_amount * units["chia"]) message: bytes = bytes(message_hex, "utf8") fee: uint64 = uint64(d_fee * units["chia"]) - tx = await wallet_client.send_notification(address, message, amount, fee) + tx = await wallet_client.send_notification(address, message, amount, fee, push=push) - print("Notification sent successfully.") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}") + if push: + print("Notification sent successfully.") + print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}") + return [tx] async def get_notifications( diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 8ebb0dc74c79..197ccd22f845 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1284,6 +1284,7 @@ async def send_notification( fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), + push: bool = True, ) -> TransactionRecord: response = await self.fetch( "send_notification", @@ -1293,6 +1294,7 @@ async def send_notification( "amount": amount, "fee": fee, "extra_conditions": conditions_to_json_dicts(extra_conditions), + "push": push, **timelock_info.to_json_dict(), }, ) From c23b890c32fc41b6a39ea178d160331623ed3c7b Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 08:54:18 -0700 Subject: [PATCH 200/274] Port `chia wallet vcs ...` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_vcs.py | 30 +++++++++++++++----- chia/cmds/wallet.py | 37 ++++++++++++++++++------ chia/cmds/wallet_funcs.py | 44 +++++++++++++++++++++-------- 3 files changed, 83 insertions(+), 28 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_vcs.py b/chia/_tests/cmds/wallet/test_vcs.py index 398f2695472d..6f96b2f5bc1e 100644 --- a/chia/_tests/cmds/wallet/test_vcs.py +++ b/chia/_tests/cmds/wallet/test_vcs.py @@ -31,8 +31,9 @@ async def vc_mint( tx_config: TXConfig, target_address: Optional[bytes32] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> VCMintResponse: - self.add_to_log("vc_mint", (did_id, tx_config, target_address, fee)) + self.add_to_log("vc_mint", (did_id, tx_config, target_address, fee, push)) return VCMintResponse( [STD_UTX], @@ -64,7 +65,7 @@ async def vc_mint( f"Transaction {get_bytes32(2).hex()}", ] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) - expected_calls: logType = {"vc_mint": [(did_bytes, DEFAULT_TX_CONFIG, target_bytes, 500000000000)]} + expected_calls: logType = {"vc_mint": [(did_bytes, DEFAULT_TX_CONFIG, target_bytes, 500000000000, True)]} test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -117,8 +118,11 @@ async def vc_spend( new_proof_hash: Optional[bytes32] = None, provider_inner_puzhash: Optional[bytes32] = None, fee: uint64 = uint64(0), + push: bool = True, ) -> VCSpendResponse: - self.add_to_log("vc_spend", (vc_id, tx_config, new_puzhash, new_proof_hash, provider_inner_puzhash, fee)) + self.add_to_log( + "vc_spend", (vc_id, tx_config, new_puzhash, new_proof_hash, provider_inner_puzhash, fee, push) + ) return VCSpendResponse([STD_UTX], [STD_TX]) inst_rpc_client = VcsUpdateProofsRpcClient() # pylint: disable=no-value-for-parameter @@ -145,7 +149,15 @@ async def vc_spend( run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) expected_calls: logType = { "vc_spend": [ - (vc_bytes, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), target_ph, new_proof, None, uint64(500000000000)) + ( + vc_bytes, + DEFAULT_TX_CONFIG.override(reuse_puzhash=True), + target_ph, + new_proof, + None, + uint64(500000000000), + True, + ) ] } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -219,8 +231,9 @@ async def vc_revoke( vc_parent_id: bytes32, tx_config: TXConfig, fee: uint64 = uint64(0), + push: bool = True, ) -> VCRevokeResponse: - self.add_to_log("vc_revoke", (vc_parent_id, tx_config, fee)) + self.add_to_log("vc_revoke", (vc_parent_id, tx_config, fee, push)) return VCRevokeResponse([STD_UTX], [STD_TX]) inst_rpc_client = VcsRevokeRpcClient() # pylint: disable=no-value-for-parameter @@ -242,8 +255,8 @@ async def vc_revoke( expected_calls: logType = { "vc_get": [(vc_id,)], "vc_revoke": [ - (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000)), - (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000)), + (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000), True), + (parent_id, DEFAULT_TX_CONFIG.override(reuse_puzhash=True), uint64(500000000000), True), ], } test_rpc_clients.wallet_rpc_client.check_log(expected_calls) @@ -260,6 +273,7 @@ async def crcat_approve_pending( min_amount_to_claim: uint64, tx_config: TXConfig, fee: uint64 = uint64(0), + push: bool = True, ) -> List[TransactionRecord]: self.add_to_log( "crcat_approve_pending", @@ -268,6 +282,7 @@ async def crcat_approve_pending( min_amount_to_claim, tx_config, fee, + push, ), ) return [STD_TX] @@ -305,6 +320,7 @@ async def crcat_approve_pending( reuse_puzhash=True, ), uint64(500000000000), + True, ) ], "get_wallets": [(None,)], diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index d5db3becea4c..f2cd4f8724b2 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -1386,16 +1386,18 @@ def vcs_cmd() -> None: # pragma: no cover @click.option("-d", "--did", help="The DID of the VC's proof provider", type=str, required=True) @click.option("-t", "--target-address", help="The address to send the VC to once it's minted", type=str, required=False) @click.option("-m", "--fee", help="Blockchain fee for mint transaction, in XCH", type=str, required=False, default="0") +@tx_out_cmd def mint_vc_cmd( wallet_rpc_port: Optional[int], fingerprint: int, did: str, target_address: Optional[str], fee: str, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import mint_vc - asyncio.run(mint_vc(wallet_rpc_port, fingerprint, did, Decimal(fee), target_address)) + return asyncio.run(mint_vc(wallet_rpc_port, fingerprint, did, Decimal(fee), target_address, push=push)) @vcs_cmd.command("get", short_help="Get a list of existing VCs") @@ -1451,6 +1453,7 @@ def get_vcs_cmd( default=False, show_default=True, ) +@tx_out_cmd def spend_vc_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1459,10 +1462,11 @@ def spend_vc_cmd( new_proof_hash: str, fee: str, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import spend_vc - asyncio.run( + return asyncio.run( spend_vc( wallet_rpc_port=wallet_rpc_port, fp=fingerprint, @@ -1471,6 +1475,7 @@ def spend_vc_cmd( new_puzhash=new_puzhash, new_proof_hash=new_proof_hash, reuse_puzhash=reuse_puzhash, + push=push, ) ) @@ -1549,6 +1554,7 @@ def get_proofs_for_root_cmd( default=False, show_default=True, ) +@tx_out_cmd def revoke_vc_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1556,10 +1562,13 @@ def revoke_vc_cmd( vc_id: Optional[str], fee: str, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import revoke_vc - asyncio.run(revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, Decimal(fee), reuse_puzhash)) + return asyncio.run( + revoke_vc(wallet_rpc_port, fingerprint, parent_coin_id, vc_id, Decimal(fee), reuse_puzhash, push=push) + ) @vcs_cmd.command("approve_r_cats", help="Claim any R-CATs that are currently pending VC approval") @@ -1586,6 +1595,7 @@ def revoke_vc_cmd( is_flag=True, default=False, ) +@tx_out_cmd def approve_r_cats_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -1595,11 +1605,20 @@ def approve_r_cats_cmd( min_coin_amount: Optional[Decimal], max_coin_amount: Optional[Decimal], reuse: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: from .wallet_funcs import approve_r_cats - asyncio.run( + return asyncio.run( approve_r_cats( - wallet_rpc_port, fingerprint, id, min_amount_to_claim, Decimal(fee), min_coin_amount, max_coin_amount, reuse + wallet_rpc_port, + fingerprint, + id, + min_amount_to_claim, + Decimal(fee), + min_coin_amount, + max_coin_amount, + reuse, + push, ) ) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 69723e10de16..a601c42f58e2 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -1530,8 +1530,13 @@ async def spend_clawback( async def mint_vc( - wallet_rpc_port: Optional[int], fp: Optional[int], did: str, d_fee: Decimal, target_address: Optional[str] -) -> None: # pragma: no cover + wallet_rpc_port: Optional[int], + fp: Optional[int], + did: str, + d_fee: Decimal, + target_address: Optional[str], + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): res = await wallet_client.vc_mint( decode_puzzle_hash(ensure_valid_address(did, allowed_types={AddressType.DID}, config=config)), @@ -1544,9 +1549,11 @@ async def mint_vc( ) ), uint64(int(d_fee * units["chia"])), + push=push, ) - print(f"New VC with launcher ID minted: {res.vc_record.vc.launcher_id.hex()}") + if push: + print(f"New VC with launcher ID minted: {res.vc_record.vc.launcher_id.hex()}") print("Relevant TX records:") print("") for tx in res.transactions: @@ -1557,6 +1564,7 @@ async def mint_vc( address_prefix=selected_network_address_prefix(config), mojo_per_unit=get_mojo_per_unit(wallet_type=WalletType.STANDARD_WALLET), ) + return res.transactions async def get_vcs( @@ -1593,7 +1601,8 @@ async def spend_vc( new_puzhash: Optional[str], new_proof_hash: str, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): txs = ( await wallet_client.vc_spend( @@ -1604,10 +1613,12 @@ async def spend_vc( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) ).transactions - print("Proofs successfully updated!") + if push: + print("Proofs successfully updated!") print("Relevant TX records:") print("") for tx in txs: @@ -1618,6 +1629,7 @@ async def spend_vc( address_prefix=selected_network_address_prefix(config), mojo_per_unit=get_mojo_per_unit(wallet_type=WalletType.STANDARD_WALLET), ) + return txs async def add_proof_reveal( @@ -1655,16 +1667,17 @@ async def revoke_vc( vc_id: Optional[str], fee: Decimal, reuse_puzhash: bool, -) -> None: # pragma: no cover + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config): if parent_coin_id is None: if vc_id is None: print("Must specify either --parent-coin-id or --vc-id") - return + return [] record = await wallet_client.vc_get(bytes32.from_hexstr(vc_id)) if record is None: print(f"Cannot find a VC with ID {vc_id}") - return + return [] parent_id: bytes32 = bytes32(record.vc.coin.parent_coin_info) else: parent_id = bytes32.from_hexstr(parent_coin_id) @@ -1675,10 +1688,12 @@ async def revoke_vc( tx_config=CMDTXConfigLoader( reuse_puzhash=reuse_puzhash, ).to_tx_config(units["chia"], config, fingerprint), + push=push, ) ).transactions - print("VC successfully revoked!") + if push: + print("VC successfully revoked!") print("Relevant TX records:") print("") for tx in txs: @@ -1689,6 +1704,7 @@ async def revoke_vc( address_prefix=selected_network_address_prefix(config), mojo_per_unit=get_mojo_per_unit(wallet_type=WalletType.STANDARD_WALLET), ) + return txs async def approve_r_cats( @@ -1700,7 +1716,8 @@ async def approve_r_cats( min_coin_amount: Optional[Decimal], max_coin_amount: Optional[Decimal], reuse: bool, -) -> None: # pragma: no cover + push: bool = True, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): if wallet_client is None: return @@ -1713,9 +1730,11 @@ async def approve_r_cats( max_coin_amount=None if max_coin_amount is None else str(max_coin_amount), reuse_puzhash=reuse, ).to_tx_config(units["cat"], config, fingerprint), + push=push, ) - print("VC successfully approved R-CATs!") + if push: + print("VC successfully approved R-CATs!") print("Relevant TX records:") print("") for tx in txs: @@ -1730,7 +1749,7 @@ async def approve_r_cats( ) except LookupError as e: print(e.args[0]) - return + return txs print_transaction( tx, @@ -1739,3 +1758,4 @@ async def approve_r_cats( address_prefix=selected_network_address_prefix(config), mojo_per_unit=mojo_per_unit, ) + return txs From e9c7d32f6a90524ab268cb57a71c45bb561fb81f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 26 Mar 2024 13:44:25 -0700 Subject: [PATCH 201/274] Port `chia dao ...` to @tx_out_cmd --- chia/_tests/cmds/wallet/test_dao.py | 8 ++ chia/cmds/dao.py | 75 +++++++--- chia/cmds/dao_funcs.py | 204 +++++++++++++++++----------- chia/rpc/wallet_rpc_client.py | 14 ++ 4 files changed, 202 insertions(+), 99 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_dao.py b/chia/_tests/cmds/wallet/test_dao.py index 4e014ec0bae6..00b56f8121fa 100644 --- a/chia/_tests/cmds/wallet/test_dao.py +++ b/chia/_tests/cmds/wallet/test_dao.py @@ -48,6 +48,7 @@ async def create_new_dao_wallet( name: Optional[str] = None, fee: uint64 = uint64(0), fee_for_cat: uint64 = uint64(0), + push: bool = True, ) -> CreateNewDAOWalletResponse: if not treasury_id: treasury_id = bytes32(token_bytes(32)) @@ -140,6 +141,7 @@ async def dao_add_funds_to_treasury( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOAddFundsToTreasuryResponse: return DAOAddFundsToTreasuryResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -278,6 +280,7 @@ async def dao_vote_on_proposal( tx_config: TXConfig, is_yes_vote: bool, fee: uint64 = uint64(0), + push: bool = True, ) -> DAOVoteOnProposalResponse: return DAOVoteOnProposalResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -289,6 +292,7 @@ async def dao_close_proposal( fee: uint64 = uint64(0), self_destruct: bool = False, reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOCloseProposalResponse: return DAOCloseProposalResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -306,6 +310,7 @@ async def dao_create_proposal( new_dao_rules: Optional[Dict[str, uint64]] = None, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOCreateProposalResponse: return DAOCreateProposalResponse([STD_UTX], [STD_TX], bytes32([0] * 32), STD_TX.name, STD_TX) @@ -489,6 +494,7 @@ async def dao_send_to_lockup( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOSendToLockupResponse: return DAOSendToLockupResponse([STD_UTX], [STD_TX], STD_TX.name, [STD_TX]) @@ -498,6 +504,7 @@ async def dao_free_coins_from_finished_proposals( tx_config: TXConfig, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOFreeCoinsFromFinishedProposalsResponse: return DAOFreeCoinsFromFinishedProposalsResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) @@ -508,6 +515,7 @@ async def dao_exit_lockup( coins: Optional[List[Dict[str, Union[str, int]]]] = None, fee: uint64 = uint64(0), reuse_puzhash: Optional[bool] = None, + push: bool = True, ) -> DAOExitLockupResponse: return DAOExitLockupResponse([STD_UTX], [STD_TX], STD_TX.name, STD_TX) diff --git a/chia/cmds/dao.py b/chia/cmds/dao.py index b54d488be2d4..0ab570d7ad30 100644 --- a/chia/cmds/dao.py +++ b/chia/cmds/dao.py @@ -1,12 +1,13 @@ from __future__ import annotations import asyncio -from typing import Optional, Sequence +from typing import List, Optional, Sequence import click -from chia.cmds.cmds_util import tx_config_args +from chia.cmds.cmds_util import tx_config_args, tx_out_cmd from chia.cmds.plotnft import validate_fee +from chia.wallet.transaction_record import TransactionRecord @click.group("dao", short_help="Create, manage or show state of DAOs", no_args_is_help=True) @@ -155,6 +156,7 @@ def dao_add_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -175,7 +177,8 @@ def dao_create_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_dao_wallet if self_destruct == proposal_timelock: @@ -201,8 +204,9 @@ def dao_create_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_dao_wallet(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_dao_wallet(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- @@ -266,6 +270,7 @@ def dao_get_id_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_add_funds_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -278,7 +283,8 @@ def dao_add_funds_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import add_funds_to_treasury extra_params = { @@ -291,8 +297,9 @@ def dao_add_funds_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(add_funds_to_treasury(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(add_funds_to_treasury(extra_params, wallet_rpc_port, fingerprint)) @dao_cmd.command("balance", short_help="Get the asset balances for a DAO treasury", no_args_is_help=True) @@ -455,6 +462,7 @@ def dao_show_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_vote_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -468,7 +476,8 @@ def dao_vote_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import vote_on_proposal is_yes_vote = False if vote_no else True @@ -484,8 +493,9 @@ def dao_vote_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(vote_on_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(vote_on_proposal(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- @@ -526,6 +536,7 @@ def dao_vote_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_close_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -538,7 +549,8 @@ def dao_close_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import close_proposal extra_params = { @@ -551,8 +563,9 @@ def dao_close_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(close_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(close_proposal(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- @@ -586,6 +599,7 @@ def dao_close_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_lockup_coins_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -597,7 +611,8 @@ def dao_lockup_coins_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import lockup_coins extra_params = { @@ -609,8 +624,9 @@ def dao_lockup_coins_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(lockup_coins(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(lockup_coins(extra_params, wallet_rpc_port, fingerprint)) @dao_cmd.command("release_coins", short_help="Release closed proposals from DAO CATs", no_args_is_help=True) @@ -633,6 +649,7 @@ def dao_lockup_coins_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_release_coins_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -643,7 +660,8 @@ def dao_release_coins_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool = True, +) -> List[TransactionRecord]: from .dao_funcs import release_coins extra_params = { @@ -654,8 +672,9 @@ def dao_release_coins_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(release_coins(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(release_coins(extra_params, wallet_rpc_port, fingerprint)) @dao_cmd.command("exit_lockup", short_help="Release DAO CATs from voting mode", no_args_is_help=True) @@ -678,6 +697,7 @@ def dao_release_coins_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_exit_lockup_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -688,7 +708,8 @@ def dao_exit_lockup_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import exit_lockup extra_params = { @@ -699,8 +720,9 @@ def dao_exit_lockup_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(exit_lockup(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(exit_lockup(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- @@ -772,6 +794,7 @@ def dao_proposal(ctx: click.Context) -> None: callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_spend_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -787,7 +810,8 @@ def dao_create_spend_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_spend_proposal extra_params = { @@ -803,8 +827,9 @@ def dao_create_spend_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_spend_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_spend_proposal(extra_params, wallet_rpc_port, fingerprint)) @dao_proposal.command("update", short_help="Create a proposal to change the DAO rules", no_args_is_help=True) @@ -877,6 +902,7 @@ def dao_create_spend_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_update_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -894,7 +920,8 @@ def dao_create_update_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_update_proposal extra_params = { @@ -912,8 +939,9 @@ def dao_create_update_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_update_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_update_proposal(extra_params, wallet_rpc_port, fingerprint)) @dao_proposal.command("mint", short_help="Create a proposal to mint new DAO CATs", no_args_is_help=True) @@ -959,6 +987,7 @@ def dao_create_update_proposal_cmd( callback=validate_fee, ) @tx_config_args +@tx_out_cmd def dao_create_mint_proposal_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -972,7 +1001,8 @@ def dao_create_mint_proposal_cmd( coins_to_exclude: Sequence[str], amounts_to_exclude: Sequence[str], reuse: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: from .dao_funcs import create_mint_proposal extra_params = { @@ -986,8 +1016,9 @@ def dao_create_mint_proposal_cmd( "coins_to_exclude": coins_to_exclude, "amounts_to_exclude": amounts_to_exclude, "reuse_puzhash": reuse, + "push": push, } - asyncio.run(create_mint_proposal(extra_params, wallet_rpc_port, fingerprint)) + return asyncio.run(create_mint_proposal(extra_params, wallet_rpc_port, fingerprint)) # ---------------------------------------------------------------------------------------- diff --git a/chia/cmds/dao_funcs.py b/chia/cmds/dao_funcs.py index 357e2d790e45..c3e396e04c1f 100644 --- a/chia/cmds/dao_funcs.py +++ b/chia/cmds/dao_funcs.py @@ -4,7 +4,7 @@ import json import time from decimal import Decimal -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client, transaction_status_msg, transaction_submitted_msg from chia.cmds.units import units @@ -13,6 +13,7 @@ from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.config import selected_network_address_prefix from chia.util.ints import uint64 +from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG from chia.wallet.util.wallet_types import WalletType @@ -45,7 +46,7 @@ async def add_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], f print(f"DAOCAT Wallet ID: {res.dao_cat_wallet_id}") -async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: proposal_minimum = uint64(int(Decimal(args["proposal_minimum_amount"]) * units["chia"])) if proposal_minimum % 2 == 0: @@ -95,13 +96,16 @@ async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int] "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - print("Successfully created DAO Wallet") + if args["push"]: + print("Successfully created DAO Wallet") print(f"DAO Treasury ID: {res.treasury_id.hex()}") print(f"DAO Wallet ID: {res.wallet_id}") print(f"CAT Wallet ID: {res.cat_wallet_id}") print(f"DAOCAT Wallet ID: {res.dao_cat_wallet_id}") + return res.transactions async def get_treasury_id(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -123,7 +127,9 @@ async def get_rules(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: in print(f"{rule}: {val}") -async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def add_funds_to_treasury( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] funding_wallet_id = args["funding_wallet_id"] amount = Decimal(args["amount"]) @@ -134,7 +140,7 @@ async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[ mojo_per_unit = get_mojo_per_unit(typ) except LookupError: # pragma: no cover print(f"Wallet id: {wallet_id} not found.") - return + return [] fee = Decimal(args["fee"]) final_fee: uint64 = uint64(int(fee * units["chia"])) @@ -154,18 +160,22 @@ async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[ "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover async def get_treasury_balance(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -283,7 +293,7 @@ async def show_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp print(f"Address: {address}") -async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] vote_amount = args["vote_amount"] fee = args["fee"] @@ -307,20 +317,24 @@ async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover - - -async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover + + +async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -341,20 +355,25 @@ async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], f "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover -async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] amount = args["amount"] final_amount: uint64 = uint64(int(Decimal(amount) * units["cat"])) @@ -374,20 +393,25 @@ async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover -async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + +async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -404,19 +428,24 @@ async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover - - -async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover + + +async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -434,19 +463,27 @@ async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - start = time.time() - while time.time() - start < 10: - await asyncio.sleep(0.1) - tx = await wallet_client.get_transaction(res.tx_id) - if len(tx.sent_to) > 0: - print(transaction_submitted_msg(tx)) - print(transaction_status_msg(fingerprint, res.tx_id)) - return None - print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") # pragma: no cover - - -async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: + + if args["push"]: + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(res.tx_id) + if len(tx.sent_to) > 0: + print(transaction_submitted_msg(tx)) + print(transaction_status_msg(fingerprint, res.tx_id)) + return res.transactions + + if args["push"]: # pragma: no cover + print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") + return res.transactions # pragma: no cover + + +async def create_spend_proposal( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -489,15 +526,20 @@ async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[ "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) asset_id_name = asset_id if asset_id else "XCH" print(f"Created spend proposal for asset: {asset_id_name}") - print("Successfully created proposal.") + if args["push"]: + print("Successfully created proposal.") print(f"Proposal ID: {res.proposal_id.hex()}") + return res.transactions -async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def create_update_proposal( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = Decimal(args["fee"]) final_fee: uint64 = uint64(int(fee * units["chia"])) @@ -532,13 +574,18 @@ async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - print("Successfully created proposal.") + if args["push"]: + print("Successfully created proposal.") print(f"Proposal ID: {res.proposal_id.hex()}") + return res.transactions -async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: +async def create_mint_proposal( + args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int +) -> List[TransactionRecord]: wallet_id = args["wallet_id"] fee = args["fee"] final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"])) @@ -562,7 +609,10 @@ async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[i "reuse_puzhash": args["reuse_puzhash"], } ).to_tx_config(units["chia"], config, fingerprint), + push=args["push"], ) - print("Successfully created proposal.") + if args["push"]: + print("Successfully created proposal.") print(f"Proposal ID: {res.proposal_id.hex()}") + return res.transactions diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 197ccd22f845..dfb3338f8e08 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1321,6 +1321,7 @@ async def create_new_dao_wallet( fee: uint64 = uint64(0), fee_for_cat: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> CreateNewDAOWalletResponse: request: Dict[str, Any] = { "wallet_type": "dao_wallet", @@ -1333,6 +1334,7 @@ async def create_new_dao_wallet( "fee": fee, "fee_for_cat": fee_for_cat, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("create_new_wallet", request) @@ -1362,6 +1364,7 @@ async def dao_create_proposal( new_dao_rules: Optional[Dict[str, Optional[uint64]]] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOCreateProposalResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1400,6 +1403,7 @@ async def dao_vote_on_proposal( is_yes_vote: bool = True, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOVoteOnProposalResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1408,6 +1412,7 @@ async def dao_vote_on_proposal( "is_yes_vote": is_yes_vote, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_vote_on_proposal", request) @@ -1426,6 +1431,7 @@ async def dao_close_proposal( self_destruct: Optional[bool] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOCloseProposalResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1433,6 +1439,7 @@ async def dao_close_proposal( "self_destruct": self_destruct, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_close_proposal", request) @@ -1444,6 +1451,7 @@ async def dao_free_coins_from_finished_proposals( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOFreeCoinsFromFinishedProposalsResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1467,6 +1475,7 @@ async def dao_add_funds_to_treasury( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOAddFundsToTreasuryResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, @@ -1474,6 +1483,7 @@ async def dao_add_funds_to_treasury( "amount": amount, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_add_funds_to_treasury", request) @@ -1486,12 +1496,14 @@ async def dao_send_to_lockup( tx_config: TXConfig, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOSendToLockupResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "amount": amount, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_send_to_lockup", request) @@ -1504,12 +1516,14 @@ async def dao_exit_lockup( coins: Optional[List[Dict[str, Any]]] = None, fee: uint64 = uint64(0), extra_conditions: Tuple[Condition, ...] = tuple(), + push: bool = True, ) -> DAOExitLockupResponse: request: Dict[str, Any] = { "wallet_id": wallet_id, "coins": coins, "fee": fee, "extra_conditions": list(extra_conditions), + "push": push, **tx_config.to_json_dict(), } response = await self.fetch("dao_exit_lockup", request) From a67e54f31ac28efeb548a746396c8cc474bd6109 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 Mar 2024 10:15:49 -0700 Subject: [PATCH 202/274] Exclude data layer and plotnft functions --- chia/cmds/data.py | 6 ++++++ chia/cmds/plotnft.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/chia/cmds/data.py b/chia/cmds/data.py index baa39345f7a1..a6f474c98246 100644 --- a/chia/cmds/data.py +++ b/chia/cmds/data.py @@ -137,6 +137,9 @@ def create_max_page_size_option() -> Callable[[FC], FC]: ) +# Functions with this mark in this file are not being ported to @tx_out_cmd due to API peculiarities +# They will therefore not work with observer-only functionality +# MARK: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @data_cmd.command("create_data_store", help="Create a new data store") @create_rpc_port_option() @create_fee_option() @@ -171,6 +174,7 @@ def get_value( run(get_value_cmd(data_rpc_port, id, key_string, root_hash, fingerprint=fingerprint)) +# MARK: tx_endpoint @data_cmd.command("update_data_store", help="Update a store by providing the changelist operations") @create_data_store_id_option() @create_changelist_option() @@ -469,6 +473,7 @@ def add_missing_files( ) +# MARK: tx_endpoint @data_cmd.command("add_mirror", help="Publish mirror urls on chain") @click.option("-i", "--id", help="Store id", type=str, required=True) @click.option( @@ -507,6 +512,7 @@ def add_mirror( ) +# MARK: tx_endpoint @data_cmd.command("delete_mirror", help="Delete an owned mirror by its coin id") @click.option("-c", "--coin_id", help="Coin id", type=str, required=True) @create_fee_option() diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py index 888c2d05ba74..8a9622f53222 100644 --- a/chia/cmds/plotnft.py +++ b/chia/cmds/plotnft.py @@ -53,6 +53,9 @@ def get_login_link_cmd(launcher_id: str) -> None: asyncio.run(get_login_link(launcher_id)) +# Functions with this mark in this file are not being ported to @tx_out_cmd due to lack of observer key support +# They will therefore not work with observer-only functionality +# MARK: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @plotnft_cmd.command("create", help="Create a plot NFT") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @options.create_fingerprint() @@ -101,6 +104,7 @@ def create_cmd( ) +# MARK: tx_endpoint @plotnft_cmd.command("join", help="Join a plot NFT to a Pool") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @@ -142,6 +146,7 @@ def join_cmd( ) +# MARK: tx_endpoint @plotnft_cmd.command("leave", help="Leave a pool and return to self-farming") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @@ -197,6 +202,7 @@ def inspect(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None: asyncio.run(inspect_cmd(wallet_rpc_port, fingerprint, id)) +# MARK: tx_endpoint @plotnft_cmd.command("claim", help="Claim rewards from a plot NFT") @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @options.create_fingerprint() From c2c4a04ab013ca07733908e7b9137b7a754542d8 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 May 2024 12:37:03 -0700 Subject: [PATCH 203/274] [no ci] From 1ba3b3aaae6a801580bbdb0af50f6dd665239d18 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 22 May 2024 14:12:11 -0700 Subject: [PATCH 204/274] Address comments by @AmineKhaldi --- chia/cmds/dao_funcs.py | 24 ++++++++++++------------ chia/cmds/data.py | 8 ++++---- chia/cmds/plotnft.py | 8 ++++---- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/chia/cmds/dao_funcs.py b/chia/cmds/dao_funcs.py index c3e396e04c1f..5049b0711f1e 100644 --- a/chia/cmds/dao_funcs.py +++ b/chia/cmds/dao_funcs.py @@ -173,9 +173,9 @@ async def add_funds_to_treasury( print(transaction_status_msg(fingerprint, res.tx_id)) return res.transactions - if args["push"]: # pragma: no cover + if args["push"]: print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") - return res.transactions # pragma: no cover + return res.transactions async def get_treasury_balance(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None: @@ -329,9 +329,9 @@ async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], print(transaction_status_msg(fingerprint, res.tx_id)) return res.transactions - if args["push"]: # pragma: no cover + if args["push"]: print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") - return res.transactions # pragma: no cover + return res.transactions async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: @@ -368,9 +368,9 @@ async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], f print(transaction_status_msg(fingerprint, res.tx_id)) return res.transactions - if args["push"]: # pragma: no cover + if args["push"]: print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") - return res.transactions # pragma: no cover + return res.transactions async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: @@ -405,10 +405,10 @@ async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: print(transaction_status_msg(fingerprint, res.tx_id)) return res.transactions - if args["push"]: # pragma: no cover + if args["push"]: print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") - return res.transactions # pragma: no cover + return res.transactions async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: @@ -440,9 +440,9 @@ async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp print(transaction_status_msg(fingerprint, res.tx_id)) return res.transactions - if args["push"]: # pragma: no cover + if args["push"]: print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") - return res.transactions # pragma: no cover + return res.transactions async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> List[TransactionRecord]: @@ -476,9 +476,9 @@ async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: print(transaction_status_msg(fingerprint, res.tx_id)) return res.transactions - if args["push"]: # pragma: no cover + if args["push"]: print(f"Transaction not yet submitted to nodes. TX ID: {res.tx_id.hex()}") - return res.transactions # pragma: no cover + return res.transactions async def create_spend_proposal( diff --git a/chia/cmds/data.py b/chia/cmds/data.py index a6f474c98246..3d35ff4ece8c 100644 --- a/chia/cmds/data.py +++ b/chia/cmds/data.py @@ -139,7 +139,7 @@ def create_max_page_size_option() -> Callable[[FC], FC]: # Functions with this mark in this file are not being ported to @tx_out_cmd due to API peculiarities # They will therefore not work with observer-only functionality -# MARK: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) +# NOTE: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @data_cmd.command("create_data_store", help="Create a new data store") @create_rpc_port_option() @create_fee_option() @@ -174,7 +174,7 @@ def get_value( run(get_value_cmd(data_rpc_port, id, key_string, root_hash, fingerprint=fingerprint)) -# MARK: tx_endpoint +# NOTE: tx_endpoint @data_cmd.command("update_data_store", help="Update a store by providing the changelist operations") @create_data_store_id_option() @create_changelist_option() @@ -473,7 +473,7 @@ def add_missing_files( ) -# MARK: tx_endpoint +# NOTE: tx_endpoint @data_cmd.command("add_mirror", help="Publish mirror urls on chain") @click.option("-i", "--id", help="Store id", type=str, required=True) @click.option( @@ -512,7 +512,7 @@ def add_mirror( ) -# MARK: tx_endpoint +# NOTE: tx_endpoint @data_cmd.command("delete_mirror", help="Delete an owned mirror by its coin id") @click.option("-c", "--coin_id", help="Coin id", type=str, required=True) @create_fee_option() diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py index 8a9622f53222..34a19c77720d 100644 --- a/chia/cmds/plotnft.py +++ b/chia/cmds/plotnft.py @@ -55,7 +55,7 @@ def get_login_link_cmd(launcher_id: str) -> None: # Functions with this mark in this file are not being ported to @tx_out_cmd due to lack of observer key support # They will therefore not work with observer-only functionality -# MARK: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) +# NOTE: tx_endpoint (This creates wallet transactions and should be parametrized by relevant options) @plotnft_cmd.command("create", help="Create a plot NFT") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @options.create_fingerprint() @@ -104,7 +104,7 @@ def create_cmd( ) -# MARK: tx_endpoint +# NOTE: tx_endpoint @plotnft_cmd.command("join", help="Join a plot NFT to a Pool") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @@ -146,7 +146,7 @@ def join_cmd( ) -# MARK: tx_endpoint +# NOTE: tx_endpoint @plotnft_cmd.command("leave", help="Leave a pool and return to self-farming") @click.option("-y", "--yes", "dont_prompt", help="No prompts", is_flag=True) @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @@ -202,7 +202,7 @@ def inspect(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None: asyncio.run(inspect_cmd(wallet_rpc_port, fingerprint, id)) -# MARK: tx_endpoint +# NOTE: tx_endpoint @plotnft_cmd.command("claim", help="Claim rewards from a plot NFT") @click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) @options.create_fingerprint() From a12341bbfcd200189f2309e6544684a479eb4437 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 23 May 2024 09:49:34 -0700 Subject: [PATCH 205/274] Fix rpc util --- chia/_tests/wallet/rpc/test_wallet_rpc.py | 6 +++++- chia/rpc/util.py | 26 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index c6f0a342e663..9346800ada58 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -83,6 +83,7 @@ from chia.wallet.transaction_sorting import SortKey from chia.wallet.uncurried_puzzle import uncurry_puzzle from chia.wallet.util.address_type import AddressType +from chia.wallet.util.blind_signer_tl import BLIND_SIGNER_TRANSLATION from chia.wallet.util.clvm_streamable import byte_deserialize_clvm_streamable from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.query_filter import AmountFilter, HashFilter, TransactionTypeFilter @@ -341,13 +342,16 @@ async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment "exclude_coins": [non_existent_coin.to_json_dict()], "reuse_puzhash": True, "CHIP-0029": True, + "translation": "CHIP-0028", "push": True, }, ) assert response["success"] tx = TransactionRecord.from_json_dict_convenience(response["transactions"][0]) [ - byte_deserialize_clvm_streamable(bytes.fromhex(utx), UnsignedTransaction) + byte_deserialize_clvm_streamable( + bytes.fromhex(utx), UnsignedTransaction, translation_layer=BLIND_SIGNER_TRANSLATION + ) for utx in response["unsigned_transactions"] ] assert tx == dataclasses.replace(tx_no_push, created_at_time=tx.created_at_time) diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 2798afbf01c2..06330d1418b8 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -171,14 +171,36 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s ] unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs(tx_records) - response["unsigned_transactions"] = unsigned_txs + if request.get("CHIP-0029", False): + response["unsigned_transactions"] = [ + json_serialize_with_clvm_streamable( + tx, + translation_layer=( + ALL_TRANSLATION_LAYERS[request["translation"]] if "translation" in request else None + ), + ) + for tx in unsigned_txs + ] + else: + response["unsigned_transactions"] = [tx.to_json_dict() for tx in unsigned_txs] new_txs: List[TransactionRecord] = [] if request.get("sign", self.service.config.get("auto_sign_txs", True)): new_txs, signing_responses = await self.service.wallet_state_manager.sign_transactions( tx_records, response.get("signing_responses", []), "signing_responses" in response ) - response["signing_responses"] = signing_responses + if request.get("CHIP-0029", False): + response["signing_responses"] = [ + json_serialize_with_clvm_streamable( + sr, + translation_layer=( + ALL_TRANSLATION_LAYERS[request["translation"]] if "translation" in request else None + ), + ) + for sr in signing_responses + ] + else: + response["signing_responses"] = [sr.to_json_dict() for sr in signing_responses] else: new_txs = tx_records # pragma: no cover From 95ac9f133c8c82efd924b18b26a148d8646ae490 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 4 Jun 2024 07:38:58 -0700 Subject: [PATCH 206/274] add test for BSTLSigningInstructions --- chia/_tests/wallet/test_signer_protocol.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/chia/_tests/wallet/test_signer_protocol.py b/chia/_tests/wallet/test_signer_protocol.py index f232474ee9ad..5a85c78ba085 100644 --- a/chia/_tests/wallet/test_signer_protocol.py +++ b/chia/_tests/wallet/test_signer_protocol.py @@ -514,7 +514,7 @@ def test_blind_signer_translation_layer() -> None: BSTLSigningTarget(b"pubkey2", b"message2", bytes32([1] * 32)), ] - BSTLSigningInstructions( + bstl_instructions: BSTLSigningInstructions = BSTLSigningInstructions( bstl_sum_hints, bstl_path_hints, bstl_signing_targets, @@ -528,8 +528,12 @@ def test_blind_signer_translation_layer() -> None: b"signature", bytes32([1] * 32), ) + bstl_instructions_json = json_serialize_with_clvm_streamable(bstl_instructions) bstl_transaction_json = json_serialize_with_clvm_streamable(bstl_transaction) bstl_signing_response_json = json_serialize_with_clvm_streamable(bstl_signing_response) + assert bstl_instructions_json == json_serialize_with_clvm_streamable( + instructions, translation_layer=BLIND_SIGNER_TRANSLATION + ) assert bstl_transaction_json == json_serialize_with_clvm_streamable( transaction, translation_layer=BLIND_SIGNER_TRANSLATION ) @@ -537,6 +541,12 @@ def test_blind_signer_translation_layer() -> None: signing_response, translation_layer=BLIND_SIGNER_TRANSLATION ) + assert ( + json_deserialize_with_clvm_streamable( + bstl_instructions_json, SigningInstructions, translation_layer=BLIND_SIGNER_TRANSLATION + ) + == instructions + ) assert ( json_deserialize_with_clvm_streamable( bstl_transaction_json, UnsignedTransaction, translation_layer=BLIND_SIGNER_TRANSLATION From f370c45fb7d74b2284501d2fb88787e4ae53e0a3 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 17 Jun 2024 08:40:25 -0700 Subject: [PATCH 207/274] Rename p2_singeton_via_delegated_puzzle -> p2_singeton_via_delegated_puzzle_w_aggregator --- chia/_tests/wallet/dao_wallet/test_dao_clvm.py | 2 +- chia/wallet/dao_wallet/dao_utils.py | 2 +- chia/wallet/puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/puzzles/p2_singleton_aggregator.clsp | 2 +- ...clsp => p2_singleton_via_delegated_puzzle_w_aggregator.clsp} | 0 ... => p2_singleton_via_delegated_puzzle_w_aggregator.clsp.hex} | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename chia/wallet/puzzles/{p2_singleton_via_delegated_puzzle.clsp => p2_singleton_via_delegated_puzzle_w_aggregator.clsp} (100%) rename chia/wallet/puzzles/{p2_singleton_via_delegated_puzzle.clsp.hex => p2_singleton_via_delegated_puzzle_w_aggregator.clsp.hex} (100%) diff --git a/chia/_tests/wallet/dao_wallet/test_dao_clvm.py b/chia/_tests/wallet/dao_wallet/test_dao_clvm.py index 7840d143681c..4f3875d69e8b 100644 --- a/chia/_tests/wallet/dao_wallet/test_dao_clvm.py +++ b/chia/_tests/wallet/dao_wallet/test_dao_clvm.py @@ -47,7 +47,7 @@ "genesis_by_coin_id_or_singleton.clsp", package_or_requirement="chia.wallet.cat_wallet.puzzles" ) DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash() -P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp") +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle_w_aggregator.clsp") P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash() P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp") P2_SINGLETON_AGGREGATOR_MOD_HASH: bytes32 = P2_SINGLETON_AGGREGATOR_MOD.get_tree_hash() diff --git a/chia/wallet/dao_wallet/dao_utils.py b/chia/wallet/dao_wallet/dao_utils.py index 214a0c5180ed..3a993148e343 100644 --- a/chia/wallet/dao_wallet/dao_utils.py +++ b/chia/wallet/dao_wallet/dao_utils.py @@ -40,7 +40,7 @@ ) DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash() DAO_CAT_LAUNCHER: Program = load_clvm("dao_cat_launcher.clsp") -P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp") +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle_w_aggregator.clsp") P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash() DAO_UPDATE_PROPOSAL_MOD: Program = load_clvm("dao_update_proposal.clsp") DAO_UPDATE_PROPOSAL_MOD_HASH: bytes32 = DAO_UPDATE_PROPOSAL_MOD.get_tree_hash() diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 172c5deaa627..4b707d27dc1a 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -52,7 +52,7 @@ "p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834", "p2_singleton_aggregator": "f79a31fcfe3736cc75720617b4cdcb4376b4b8f8f71108617710612b909a4924", "p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b", - "p2_singleton_via_delegated_puzzle": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", + "p2_singleton_via_delegated_puzzle_w_aggregator": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", "pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494", "pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307", "rom_bootstrap_generator": "161bade1f822dcd62ab712ebaf30f3922a301e48a639e4295c5685f8bece7bd9", diff --git a/chia/wallet/puzzles/p2_singleton_aggregator.clsp b/chia/wallet/puzzles/p2_singleton_aggregator.clsp index 17d03dd596e6..99c32565d965 100644 --- a/chia/wallet/puzzles/p2_singleton_aggregator.clsp +++ b/chia/wallet/puzzles/p2_singleton_aggregator.clsp @@ -1,4 +1,4 @@ -;; Works with p2_singleton_via_delegated_puzzle +;; Works with p2_singleton_via_delegated_puzzle_w_aggregator ;; When we have many p2_singleton coins and want to aggregate them together ;; all coins make announcements of their puzhash, amount, and ID diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp similarity index 100% rename from chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp rename to chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp.hex b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp.hex similarity index 100% rename from chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp.hex rename to chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_w_aggregator.clsp.hex From d9c30a26a8ac4500a26e59e87bdd3c3ccff0a3e6 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 17 Jun 2024 12:59:13 -0700 Subject: [PATCH 208/274] Add Add p2_singleton_via_delegated_puzzle_safe.clsp --- chia/_tests/clvm/test_p2_singletons.py | 152 ++++++++++++++++++ .../puzzles/deployed_puzzle_hashes.json | 1 + ...2_singleton_via_delegated_puzzle_safe.clsp | 33 ++++ ...ngleton_via_delegated_puzzle_safe.clsp.hex | 1 + .../p2_singleton_via_delegated_puzzle_safe.py | 76 +++++++++ 5 files changed, 263 insertions(+) create mode 100644 chia/_tests/clvm/test_p2_singletons.py create mode 100644 chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp create mode 100644 chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex create mode 100644 chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py diff --git a/chia/_tests/clvm/test_p2_singletons.py b/chia/_tests/clvm/test_p2_singletons.py new file mode 100644 index 000000000000..6afd0789aee1 --- /dev/null +++ b/chia/_tests/clvm/test_p2_singletons.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +import pytest +from chia_rs import G2Element + +from chia.clvm.spend_sim import CostLogger, sim_and_client +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_spend import make_spend +from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.spend_bundle import SpendBundle +from chia.util.errors import Err +from chia.util.ints import uint64 +from chia.wallet.conditions import CreateCoin +from chia.wallet.puzzles import p2_singleton_via_delegated_puzzle_safe as dp_safe + +ACS = Program.to(1) +ACS_HASH = ACS.get_tree_hash() + +MOCK_SINGLETON_MOD = Program.to([2, 5, 7]) # (a 5 11) - (mod (_ PUZZLE . solution) (a PUZZLE solution)) +MOCK_SINGLETON_MOD_HASH = MOCK_SINGLETON_MOD.get_tree_hash() +MOCK_SINGLETON_LAUNCHER_ID = bytes32([0] * 32) +MOCK_SINGLETON_LAUNCHER_HASH = bytes32([0] * 32) +MOCK_SINGLETON = MOCK_SINGLETON_MOD.curry( + Program.to((MOCK_SINGLETON_MOD_HASH, (MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_LAUNCHER_HASH))), + ACS, +) +MOCK_SINGLETON_HASH = MOCK_SINGLETON.get_tree_hash() + + +@pytest.mark.anyio +async def test_dp_safe_lifecycle(cost_logger: CostLogger) -> None: + P2_SINGLETON = dp_safe.construct(MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_MOD_HASH, MOCK_SINGLETON_LAUNCHER_HASH) + P2_SINGLETON_HASH = dp_safe.construct_hash( + MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_MOD_HASH, MOCK_SINGLETON_LAUNCHER_HASH + ) + + async with sim_and_client() as (sim, sim_client): + await sim.farm_block(P2_SINGLETON_HASH) + await sim.farm_block(MOCK_SINGLETON_HASH) + p2_singleton = (await sim_client.get_coin_records_by_puzzle_hash(P2_SINGLETON_HASH, include_spent_coins=False))[ + 0 + ].coin + singleton = (await sim_client.get_coin_records_by_puzzle_hash(MOCK_SINGLETON_HASH, include_spent_coins=False))[ + 0 + ].coin + + dp = Program.to(1) + dp_hash = dp.get_tree_hash() + bundle = cost_logger.add_cost( + "p2_singleton_w_mock_singleton", + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + p2_singleton.name(), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([dp_safe.required_announcement(dp_hash, p2_singleton.name()).to_program()]), + ), + ], + G2Element(), + ), + ) + result = await sim_client.push_tx(bundle) + assert result == (MempoolInclusionStatus.SUCCESS, None) + checkpoint = sim.block_height + await sim.farm_block() + + assert len(await sim_client.get_coin_records_by_puzzle_hash(bytes32([0] * 32), include_spent_coins=False)) == 1 + + await sim.rewind(checkpoint) + + result = await sim_client.push_tx( + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + bytes32([0] * 32), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([dp_safe.required_announcement(dp_hash, p2_singleton.name()).to_program()]), + ), + ], + G2Element(), + ) + ) + assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_MY_COIN_ID_FAILED) + + result = await sim_client.push_tx( + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + p2_singleton.name(), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([]), + ), + ], + G2Element(), + ) + ) + assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_ANNOUNCE_CONSUMED_FAILED) + + result = await sim_client.push_tx( + SpendBundle( + [ + make_spend( + p2_singleton, + P2_SINGLETON, + dp_safe.solve( + ACS_HASH, + dp, + Program.to([CreateCoin(bytes32([0] * 32), uint64(0)).to_program()]), + p2_singleton.name(), + ), + ), + make_spend( + singleton, + MOCK_SINGLETON, + Program.to([dp_safe.required_announcement(dp_hash, bytes32([0] * 32)).to_program()]), + ), + ], + G2Element(), + ) + ) + assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_ANNOUNCE_CONSUMED_FAILED) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 4b707d27dc1a..f8a0d53c54a2 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -52,6 +52,7 @@ "p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834", "p2_singleton_aggregator": "f79a31fcfe3736cc75720617b4cdcb4376b4b8f8f71108617710612b909a4924", "p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b", + "p2_singleton_via_delegated_puzzle_safe": "56bb476b46b1a45433a6b7678feb39ff0d439292f4fc7c78e51d38b14521f64e", "p2_singleton_via_delegated_puzzle_w_aggregator": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", "pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494", "pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307", diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp new file mode 100644 index 000000000000..6261dc02fce4 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp @@ -0,0 +1,33 @@ +(mod ( + SINGLETON_MOD_HASH + SINGLETON_STRUCT_HASH + singleton_inner_puzhash + delegated_puzzle + delegated_solution + my_id + ) + + (include condition_codes.clib) + (include curry-and-treehash.clib) + + (defun-inline calculate_full_puzzle_hash (SINGLETON_MOD_HASH SINGLETON_STRUCT_HASH singleton_inner_puzhash) + (puzzle-hash-of-curried-function SINGLETON_MOD_HASH + singleton_inner_puzhash + SINGLETON_STRUCT_HASH + ) + ) + + (c + (list + ASSERT_PUZZLE_ANNOUNCEMENT + (sha256 + (calculate_full_puzzle_hash SINGLETON_MOD_HASH SINGLETON_STRUCT_HASH singleton_inner_puzhash) + (sha256tree (list my_id (sha256tree delegated_puzzle))) + ) + ) + (c + (list ASSERT_MY_COIN_ID my_id) + (a delegated_puzzle delegated_solution) + ) + ) +) diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex new file mode 100644 index 000000000000..cf63f3dbf607 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff17ffff04ff0bff808080808080ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ff2fff80808080ff808080ff8080808080ff808080ffff04ffff04ff10ffff04ff81bfff808080ffff02ff2fff5f808080ffff04ffff01ffffff463fff02ff0401ffff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff1affff0bff3cff2c80ffff0bff1affff0bff1affff0bff3cff1280ff0980ffff0bff1aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffff0bff1affff0bff3cff1480ffff0bff1affff0bff1affff0bff3cff1280ff0580ffff0bff1affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py new file mode 100644 index 000000000000..34a9638cd27d --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import Dict, Optional + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.wallet.conditions import CreatePuzzleAnnouncement +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.singleton_top_layer_v1_1 import SINGLETON_LAUNCHER_HASH, SINGLETON_MOD_HASH +from chia.wallet.util.curry_and_treehash import curry_and_treehash, shatree_atom, shatree_pair + +PUZZLE = load_clvm("p2_singleton_via_delegated_puzzle_safe.clsp") +PUZZLE_HASH = PUZZLE.get_tree_hash() +QUOTED_PUZZLE = Program.to((1, PUZZLE)) +QUOTED_PUZZLE_HASH = QUOTED_PUZZLE.get_tree_hash() +PRE_HASHED_HASHES: Dict[bytes32, bytes32] = { + SINGLETON_MOD_HASH: shatree_atom(SINGLETON_MOD_HASH), + SINGLETON_LAUNCHER_HASH: shatree_atom(SINGLETON_LAUNCHER_HASH), +} + + +def _treehash_hash(atom_hash: bytes32) -> bytes32: + if atom_hash in PRE_HASHED_HASHES: + return PRE_HASHED_HASHES[atom_hash] + else: + return shatree_atom(atom_hash) + + +def _struct_hash(singleton_mod_hash: bytes32, launcher_id: bytes32, singleton_launcher_hash: bytes32) -> bytes32: + return shatree_pair( + _treehash_hash(singleton_mod_hash), + shatree_pair(_treehash_hash(launcher_id), _treehash_hash(singleton_launcher_hash)), + ) + + +def match(potential_match: Program) -> Optional[Program]: + r = potential_match.uncurry() + if r is None: + return r + elif r[0] == PUZZLE: + return r[1] + else: + return None + + +def construct( + launcher_id: bytes32, + singleton_mod_hash: bytes32 = SINGLETON_MOD_HASH, + singleton_launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH, +) -> Program: + return PUZZLE.curry( + singleton_mod_hash, + _struct_hash(singleton_mod_hash, launcher_id, singleton_launcher_hash), + ) + + +def construct_hash( + launcher_id: bytes32, + singleton_mod_hash: bytes32 = SINGLETON_MOD_HASH, + singleton_launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH, +) -> bytes32: + return curry_and_treehash( + QUOTED_PUZZLE_HASH, + _treehash_hash(singleton_mod_hash), + shatree_atom(_struct_hash(singleton_mod_hash, launcher_id, singleton_launcher_hash)), + ) + + +def solve( + singleton_inner_puzhash: bytes32, delegated_puzzle: Program, delegated_solution: Program, my_id: bytes32 +) -> Program: + return Program.to([singleton_inner_puzhash, delegated_puzzle, delegated_solution, my_id]) + + +def required_announcement(delegated_puzzle_hash: bytes32, my_id: bytes32) -> CreatePuzzleAnnouncement: + return CreatePuzzleAnnouncement(Program.to([my_id, delegated_puzzle_hash]).get_tree_hash()) From 0c993a09ee3e17832f12088b5efbb7d715fd837d Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 18 Jun 2024 13:25:06 -0700 Subject: [PATCH 209/274] Add test coverage --- chia/_tests/clvm/test_p2_singletons.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chia/_tests/clvm/test_p2_singletons.py b/chia/_tests/clvm/test_p2_singletons.py index 6afd0789aee1..229bc9a13bf3 100644 --- a/chia/_tests/clvm/test_p2_singletons.py +++ b/chia/_tests/clvm/test_p2_singletons.py @@ -13,6 +13,7 @@ from chia.util.ints import uint64 from chia.wallet.conditions import CreateCoin from chia.wallet.puzzles import p2_singleton_via_delegated_puzzle_safe as dp_safe +from chia.wallet.util.curry_and_treehash import shatree_atom ACS = Program.to(1) ACS_HASH = ACS.get_tree_hash() @@ -26,6 +27,8 @@ ACS, ) MOCK_SINGLETON_HASH = MOCK_SINGLETON.get_tree_hash() +dp_safe.PRE_HASHED_HASHES[MOCK_SINGLETON_MOD_HASH] = shatree_atom(MOCK_SINGLETON_MOD_HASH) +dp_safe.PRE_HASHED_HASHES[MOCK_SINGLETON_LAUNCHER_HASH] = shatree_atom(MOCK_SINGLETON_LAUNCHER_HASH) @pytest.mark.anyio @@ -34,6 +37,9 @@ async def test_dp_safe_lifecycle(cost_logger: CostLogger) -> None: P2_SINGLETON_HASH = dp_safe.construct_hash( MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_MOD_HASH, MOCK_SINGLETON_LAUNCHER_HASH ) + assert dp_safe.match(P2_SINGLETON) is not None + assert dp_safe.match(ACS) is None + assert dp_safe.match(MOCK_SINGLETON) is None async with sim_and_client() as (sim, sim_client): await sim.farm_block(P2_SINGLETON_HASH) From 62e73a017d5139390664eef9bc422f1b9745f5fb Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 20 Jun 2024 12:16:37 -0700 Subject: [PATCH 210/274] test coverage again --- chia/_tests/clvm/test_p2_singletons.py | 2 +- .../puzzles/p2_singleton_via_delegated_puzzle_safe.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chia/_tests/clvm/test_p2_singletons.py b/chia/_tests/clvm/test_p2_singletons.py index 229bc9a13bf3..3bf981d92fb1 100644 --- a/chia/_tests/clvm/test_p2_singletons.py +++ b/chia/_tests/clvm/test_p2_singletons.py @@ -21,7 +21,7 @@ MOCK_SINGLETON_MOD = Program.to([2, 5, 7]) # (a 5 11) - (mod (_ PUZZLE . solution) (a PUZZLE solution)) MOCK_SINGLETON_MOD_HASH = MOCK_SINGLETON_MOD.get_tree_hash() MOCK_SINGLETON_LAUNCHER_ID = bytes32([0] * 32) -MOCK_SINGLETON_LAUNCHER_HASH = bytes32([0] * 32) +MOCK_SINGLETON_LAUNCHER_HASH = bytes32([1] * 32) MOCK_SINGLETON = MOCK_SINGLETON_MOD.curry( Program.to((MOCK_SINGLETON_MOD_HASH, (MOCK_SINGLETON_LAUNCHER_ID, MOCK_SINGLETON_LAUNCHER_HASH))), ACS, diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py index 34a9638cd27d..024f1a2717ad 100644 --- a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.py @@ -34,11 +34,9 @@ def _struct_hash(singleton_mod_hash: bytes32, launcher_id: bytes32, singleton_la def match(potential_match: Program) -> Optional[Program]: - r = potential_match.uncurry() - if r is None: - return r - elif r[0] == PUZZLE: - return r[1] + mod, args = potential_match.uncurry() + if mod == PUZZLE: + return args else: return None From 5c7364d08a6a31be544a02209fe95074faabfb2a Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 24 Jun 2024 17:46:39 +0100 Subject: [PATCH 211/274] move gather_signing_info to main wallet --- chia/wallet/wallet.py | 27 +++++++++++++++++++++++++++ chia/wallet/wallet_protocol.py | 2 ++ chia/wallet/wallet_state_manager.py | 26 +------------------------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index c2f749a0c27d..619a22bf0a29 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -14,6 +14,7 @@ from chia.types.coin_spend import CoinSpend, make_spend from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode from chia.types.spend_bundle import SpendBundle +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.util.streamable import Streamable @@ -50,6 +51,7 @@ SignedTransaction, SigningInstructions, SigningResponse, + SigningTarget, Spend, SumHint, TransactionInfo, @@ -565,6 +567,31 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: [uint64(12381), uint64(8444), uint64(2), uint64(index)], ) + async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: + pks: List[bytes] = [] + signing_targets: List[SigningTarget] = [] + for coin_spend in coin_spends: + _coin_spend = coin_spend.as_coin_spend() + # Get AGG_SIG conditions + conditions_dict = conditions_dict_for_solution( + _coin_spend.puzzle_reveal.to_program(), + _coin_spend.solution.to_program(), + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + # Create signature + for pk, msg in pkm_pairs_for_conditions_dict( + conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + pk_bytes = bytes(pk) + pks.append(pk_bytes) + fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") + signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) + + return SigningInstructions( + await self.wallet_state_manager.key_hints_for_pubkeys(pks), + signing_targets, + ) + async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 70235c6a0377..9ccee02f8dd6 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -130,6 +130,8 @@ async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: ... + async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: ... + async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: ... async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: ... diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f8611d31fd51..37a3166d4c73 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -51,7 +51,6 @@ 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.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.db_synchronous import db_synchronous_on from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err @@ -113,7 +112,6 @@ SignedTransaction, SigningInstructions, SigningResponse, - SigningTarget, Spend, SumHint, TransactionInfo, @@ -2642,29 +2640,7 @@ async def key_hints_for_pubkeys(self, pks: List[bytes]) -> KeyHints: ) async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: - pks: List[bytes] = [] - signing_targets: List[SigningTarget] = [] - for coin_spend in coin_spends: - _coin_spend = coin_spend.as_coin_spend() - # Get AGG_SIG conditions - conditions_dict = conditions_dict_for_solution( - _coin_spend.puzzle_reveal.to_program(), - _coin_spend.solution.to_program(), - self.constants.MAX_BLOCK_COST_CLVM, - ) - # Create signature - for pk, msg in pkm_pairs_for_conditions_dict( - conditions_dict, _coin_spend.coin, self.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - pk_bytes = bytes(pk) - pks.append(pk_bytes) - fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") - signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) - - return SigningInstructions( - await self.key_hints_for_pubkeys(pks), - signing_targets, - ) + return await self.main_wallet.gather_signing_info(coin_spends) async def gather_signing_info_for_bundles(self, bundles: List[SpendBundle]) -> List[UnsignedTransaction]: utxs: List[UnsignedTransaction] = [] From 844cb8ea94ce7f419c3d26ba471f747de021784f Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 26 Jun 2024 12:30:26 -0700 Subject: [PATCH 212/274] Bring Wallet into conformity with MainWalletProtocol --- chia/wallet/wallet.py | 1 + chia/wallet/wallet_protocol.py | 1 + 2 files changed, 2 insertions(+) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 75626f86509c..3dd302411f20 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -87,6 +87,7 @@ async def create( self = Wallet() self.log = logging.getLogger(name) self.wallet_state_manager = wallet_state_manager + self.wallet_info = info self.wallet_id = info.id return self diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 1d1c0bfbd796..6d64ec86662f 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -37,6 +37,7 @@ T = TypeVar("T", contravariant=True) +@runtime_checkable class WalletProtocol(Protocol[T]): @classmethod def type(cls) -> WalletType: ... From d8e80115e5e9871d9b6c3f84977fab3881e564e0 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 9 Jul 2024 11:52:05 +0100 Subject: [PATCH 213/274] updates from long_lived merge --- chia/_tests/cmds/wallet/test_vault.py | 140 ++++++++++++++++++ chia/_tests/wallet/vault/test_vault_wallet.py | 30 +--- .../puzzles/deployed_puzzle_hashes.json | 2 +- chia/wallet/vault/vault_wallet.py | 55 +++---- chia/wallet/wallet_protocol.py | 3 +- chia/wallet/wallet_puzzle_store.py | 2 +- chia/wallet/wallet_state_manager.py | 32 +++- 7 files changed, 198 insertions(+), 66 deletions(-) create mode 100644 chia/_tests/cmds/wallet/test_vault.py diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py new file mode 100644 index 000000000000..eb9ac39b3fdf --- /dev/null +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from pathlib import Path +from typing import List, Optional, Tuple + +from chia_rs import Coin, G1Element, G2Element +from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert +from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, WALLET_ID_ARG, get_bytes32 + +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint8, uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.tx_config import TXConfig + + +def test_vault_create(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: + test_rpc_clients, root_dir = get_test_cli_clients + + # set RPC clients + class CreateVaultRpcClient(TestWalletRpcClient): + async def vault_create( + self, + secp_pk: bytes, + hp_index: uint32, + tx_config: TXConfig, + bls_pk: Optional[bytes] = None, + timelock: Optional[uint64] = None, + fee: uint64 = uint64(0), + push: bool = True, + ) -> List[TransactionRecord]: + tx_rec = TransactionRecord( + confirmed_at_height=uint32(1), + created_at_time=uint64(1234), + to_puzzle_hash=get_bytes32(1), + amount=uint64(12345678), + fee_amount=uint64(1234567), + confirmed=False, + sent=uint32(0), + spend_bundle=SpendBundle([], G2Element()), + additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], + removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], + wallet_id=uint32(1), + sent_to=[("aaaaa", uint8(1), None)], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=get_bytes32(2), + memos=[(get_bytes32(3), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), + ) + return [tx_rec] + + inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter + test_rpc_clients.wallet_rpc_client = inst_rpc_client + pk = get_bytes32(0).hex() + recovery_pk = get_bytes32(1).hex() + timelock = "100" + hidden_puzzle_index = "10" + fee = "0.1" + command_args = [ + "vault", + "create", + "-pk", + pk, + "-rk", + recovery_pk, + "-rt", + timelock, + "-i", + hidden_puzzle_index, + "-m", + fee, + ] + assert_list = ["Successfully created a Vault wallet"] + run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG], assert_list) + + +def test_vault_recovery(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: + test_rpc_clients, root_dir = get_test_cli_clients + + # set RPC clients + class CreateVaultRpcClient(TestWalletRpcClient): + async def vault_recovery( + self, + wallet_id: uint32, + secp_pk: bytes, + hp_index: uint32, + tx_config: TXConfig, + bls_pk: Optional[G1Element] = None, + timelock: Optional[uint64] = None, + ) -> List[TransactionRecord]: + tx_rec = TransactionRecord( + confirmed_at_height=uint32(1), + created_at_time=uint64(1234), + to_puzzle_hash=get_bytes32(1), + amount=uint64(12345678), + fee_amount=uint64(1234567), + confirmed=False, + sent=uint32(0), + spend_bundle=SpendBundle([], G2Element()), + additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], + removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], + wallet_id=uint32(1), + sent_to=[("aaaaa", uint8(1), None)], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=get_bytes32(2), + memos=[(get_bytes32(3), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), + ) + return [tx_rec, tx_rec] + + inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter + test_rpc_clients.wallet_rpc_client = inst_rpc_client + pk = get_bytes32(0).hex() + recovery_pk = get_bytes32(1).hex() + timelock = "100" + hidden_puzzle_index = "10" + command_args = [ + "vault", + "recover", + "-pk", + pk, + "-rk", + recovery_pk, + "-rt", + timelock, + "-i", + hidden_puzzle_index, + "-ri", + "recovery_init.json", + "-rf", + "recovery_finish.json", + ] + assert_list = [ + "Initiate Recovery transaction written to: recovery_init.json", + "Finish Recovery transaction written to: recovery_finish.json", + ] + run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG, WALLET_ID_ARG], assert_list) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index c324facfa7dd..4396ed1beb42 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -8,6 +8,8 @@ from ecdsa import NIST256p, SigningKey from ecdsa.util import PRNG +from chia._tests.conftest import ConsensusMode +from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia.rpc.wallet_request_types import GatherSigningInfo from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.blockchain_format.sized_bytes import bytes32 @@ -19,8 +21,6 @@ from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault -from tests.conftest import ConsensusMode -from tests.environments.wallet import WalletStateTransition, WalletTestFramework async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: bool) -> None: @@ -117,7 +117,6 @@ async def test_vault_creation( # get a p2_singleton p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() - await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) coins_to_create = 2 funding_amount = uint64(1000000000) @@ -150,36 +149,21 @@ async def test_vault_creation( ], ) - recs = await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG) - coin = recs.pop() - assert coin.amount == funding_amount recipient_ph = await funding_wallet.get_new_puzzlehash() primaries = [ - Payment(recipient_ph, uint64(500000000)), - Payment(recipient_ph, uint64(510000000)), + Payment(recipient_ph, uint64(500000000), memos=[recipient_ph]), + Payment(recipient_ph, uint64(510000000), memos=[recipient_ph]), ] amount = uint64(1000000) fee = uint64(100) balance_delta = 1011000099 unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( - amount, recipient_ph, DEFAULT_TX_CONFIG, primaries=primaries, fee=fee + amount, recipient_ph, DEFAULT_TX_CONFIG, primaries=primaries, fee=fee, memos=[recipient_ph] ) - assert len(unsigned_txs) == 1 - # Farm a block so the vault balance includes farmed coins from the test setup in pre-block update. - # Do this after generating the tx so we can be sure to spend the right funding coins - await wallet_environments.full_node.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) - - assert unsigned_txs[0].spend_bundle is not None - spends = [Spend.from_coin_spend(spend) for spend in unsigned_txs[0].spend_bundle.coin_spends] - signing_info = await env.rpc_client.gather_signing_info(GatherSigningInfo(spends)) - - signing_responses = await wallet.execute_signing_instructions(signing_info.signing_instructions) - - signed_response = await wallet.apply_signatures(spends, signing_responses) - await env.wallet_state_manager.submit_transactions([signed_response]) + await wallet_environments.environments[0].rpc_client.push_transactions(unsigned_txs, sign=True) vault_eve_id = wallet.vault_info.coin.name() await wallet_environments.process_pending_states( @@ -256,7 +240,6 @@ async def test_vault_recovery( assert wallet.vault_info p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() - await wallet_environments.full_node.farm_blocks_to_puzzlehash(1, p2_singleton_puzzle_hash) coins_to_create = 2 funding_amount = uint64(1000000000) @@ -297,6 +280,7 @@ async def test_vault_recovery( bls_pk=bls_pk, timelock=timelock, ) + await wallet_environments.environments[1].rpc_client.push_transactions([initiate_tx], sign=True) vault_coin = wallet.vault_info.coin diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 172c5deaa627..aec615c8b4c6 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -64,7 +64,7 @@ "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", "test_generator_deserialize": "52add794fc76e89512e4a063c383418bda084c8a78c74055abe80179e4a7832c", "test_multiple_generator_input_arguments": "156dafbddc3e1d3bfe1f2a84e48e5e46b287b8358bf65c3c091c93e855fbfc5b", - "vault_p2_recovery": "5bcfac8571e7464ab8852794b37b428ce23c55976d0a6b092227fd0a6b0e07c5", + "vault_p2_recovery": "59d20b29c583990dca222583cf6fa6b03189841b18ad866e1271aeeec9774828", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 347688f15da5..6545463046df 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -177,33 +177,6 @@ async def generate_signed_transaction( ) return [tx_record] - async def generate_p2_singleton_spends( - self, - amount: uint64, - tx_config: TXConfig, - coins: Optional[Set[Coin]] = None, - ) -> List[CoinSpend]: - total_balance = await self.get_spendable_balance() - if coins is None: - if amount > total_balance: - raise ValueError( - f"Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {amount} mojos" - ) - coins = await self.select_coins( - uint64(amount), - tx_config.coin_selection_config, - ) - assert len(coins) > 0 - - p2_singleton_puzzle: Program = get_p2_singleton_puzzle(self.launcher_id) - - spends: List[CoinSpend] = [] - for coin in list(coins): - p2_singleton_solution: Program = Program.to([self.vault_info.inner_puzzle_hash, coin.name()]) - spends.append(make_spend(coin, p2_singleton_puzzle, p2_singleton_solution)) - - return spends - async def _generate_unsigned_transaction( self, amount: uint64, @@ -249,7 +222,11 @@ async def _generate_unsigned_transaction( p2_singleton_puzhash = p2_singleton_puzzle.get_tree_hash() # add the change condition if selected_amount > amount: - conditions.append(Payment(p2_singleton_puzhash, uint64(selected_amount - total_amount)).as_condition()) + conditions.append( + Payment( + p2_singleton_puzhash, uint64(selected_amount - total_amount), memos=[p2_singleton_puzhash] + ).as_condition() + ) # create the p2_singleton spends delegated_puzzle = puzzle_for_conditions(conditions) delegated_solution = solution_for_conditions(conditions) @@ -262,6 +239,7 @@ async def _generate_unsigned_transaction( ) else: p2_solution = Program.to([0, self.vault_info.inner_puzzle_hash, 0, 0, coin.name()]) + sb = SpendBundle([make_spend(coin, p2_singleton_puzzle, p2_solution)], G2Element()) p2_singleton_spends.append(make_spend(coin, p2_singleton_puzzle, p2_solution)) next_puzzle_hash = ( @@ -278,7 +256,6 @@ async def _generate_unsigned_transaction( [ CreatePuzzleAnnouncement( Program.to([spend.coin.name(), puzzle_to_assert.get_tree_hash()]).get_tree_hash(), - # self.vault_info.coin.puzzle_hash ).to_program(), AssertCoinAnnouncement(asserted_id=spend.coin.name(), asserted_msg=b"$").to_program(), ] @@ -345,9 +322,9 @@ 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()) + 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 @@ -385,6 +362,8 @@ async def apply_signatures( mod, curried_args = spend.puzzle.uncurry() if match_vault_puzzle(mod, curried_args): new_sol = spend.solution.replace(rrfrrfrrf=signing_responses[0].signature) + # spend.puzzle.run(new_sol) + # breakpoint() signed_spends.append(Spend(spend.coin, spend.puzzle, new_sol)) else: signed_spends.append(spend) @@ -440,9 +419,9 @@ 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()) + 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() puzzle = construct_p2_delegated_secp( @@ -459,9 +438,9 @@ def require_derivation_paths(self) -> bool: return False async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: - wallet_identifier: Optional[ - WalletIdentifier - ] = await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(hint) + wallet_identifier: Optional[WalletIdentifier] = ( + await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(hint) + ) if wallet_identifier: return True return False diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index cc29fc1cbffa..add99ca0d8e8 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -150,8 +150,7 @@ def make_solution( async def get_puzzle(self, new: bool) -> Program: ... - async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: - ... + async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: ... class GSTOptionalArgs(TypedDict): diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index e07adbb15525..2afe00bd156b 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -84,7 +84,7 @@ async def add_derivation_paths(self, records: List[DerivationRecord]) -> None: sql_records.append( ( record.index, - bytes(record.pubkey).hex(), + bytes(record._pubkey).hex(), record.puzzle_hash.hex(), record.wallet_type, record.wallet_id, diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 915b0a3048cf..5bc70adbfe51 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -52,6 +52,7 @@ 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.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.db_synchronous import db_synchronous_on from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err @@ -113,6 +114,7 @@ SignedTransaction, SigningInstructions, SigningResponse, + SigningTarget, Spend, SumHint, TransactionInfo, @@ -143,7 +145,12 @@ last_change_height_cs, ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType -from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash, match_vault_puzzle +from chia.wallet.vault.vault_drivers import ( + get_vault_full_puzzle_hash, + get_vault_inner_puzzle_hash, + match_recovery_puzzle, + match_vault_puzzle, +) from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, ProofsChecker, construct_pending_approval_state @@ -1801,6 +1808,19 @@ async def _add_coin_states( wallet_identifier = WalletIdentifier.create(dl_wallet) if wallet_identifier is None: + # Confirm tx records for txs which we submitted for coins which aren't in our wallet + # Used for vault recovery spends + if coin_state.spent_height is not None: + all_unconfirmed: List[TransactionRecord] = await self.tx_store.get_all_unconfirmed() + tx_records: List[TransactionRecord] = [] + for out_tx_record in all_unconfirmed: + for rem_coin in out_tx_record.removals: + if rem_coin == coin_state.coin: + tx_records.append(out_tx_record) + + if len(tx_records) > 0: + for tx_record in tx_records: + await self.tx_store.set_confirmed(tx_record.name, uint32(coin_state.spent_height)) self.log.debug(f"No wallet for coin state: {coin_state}") continue @@ -2351,6 +2371,14 @@ async def add_pending_transactions( additional_signing_responses != [], ) all_coins_names = [] + # Check that tx_records have additions/removals since vault txs don't have them until they're signed + for i, tx in enumerate(tx_records): + if tx.additions == []: + tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) + if tx.removals == []: + tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) + tx_records[i] = tx + async with self.db_wrapper.writer_maybe_transaction(): for tx_record in tx_records: # Wallet node will use this queue to retry sending this transaction until full nodes receives it @@ -2669,6 +2697,8 @@ async def key_hints_for_pubkeys(self, pks: List[bytes]) -> KeyHints: ) async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: + if isinstance(self.main_wallet, Vault): + return await self.main_wallet.gather_signing_info(coin_spends) pks: List[bytes] = [] signing_targets: List[SigningTarget] = [] for coin_spend in coin_spends: From 8eb18d3761c2d3899be90eeed0b3cebede3c6368 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 9 Jul 2024 12:50:01 +0100 Subject: [PATCH 214/274] merge cleanup and use p2_singleton_via_delegated_safe --- .../puzzles/deployed_puzzle_hashes.json | 2 +- ...2_singleton_via_delegated_puzzle_safe.clsp | 5 +- ...ngleton_via_delegated_puzzle_safe.clsp.hex | 2 +- chia/wallet/vault/vault_drivers.py | 5 +- chia/wallet/vault/vault_wallet.py | 6 +-- chia/wallet/wallet.py | 53 ++++++++++--------- 6 files changed, 38 insertions(+), 35 deletions(-) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index d12afd1d5278..1e16662a19bf 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -52,7 +52,7 @@ "p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834", "p2_singleton_aggregator": "f79a31fcfe3736cc75720617b4cdcb4376b4b8f8f71108617710612b909a4924", "p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b", - "p2_singleton_via_delegated_puzzle_safe": "56bb476b46b1a45433a6b7678feb39ff0d439292f4fc7c78e51d38b14521f64e", + "p2_singleton_via_delegated_puzzle_safe": "ca336579834fad16c1b56946f4ad697aa7c37975836332178320e242b482bebd", "p2_singleton_via_delegated_puzzle_w_aggregator": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", "pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494", "pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307", diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp index 6261dc02fce4..2acfd39399e1 100644 --- a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp @@ -27,7 +27,10 @@ ) (c (list ASSERT_MY_COIN_ID my_id) - (a delegated_puzzle delegated_solution) + (c + (list CREATE_COIN_ANNOUNCEMENT '$') + (a delegated_puzzle delegated_solution) + ) ) ) ) diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex index cf63f3dbf607..56bc05e77d83 100644 --- a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff17ffff04ff0bff808080808080ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ff2fff80808080ff808080ff8080808080ff808080ffff04ffff04ff10ffff04ff81bfff808080ffff02ff2fff5f808080ffff04ffff01ffffff463fff02ff0401ffff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff1affff0bff3cff2c80ffff0bff1affff0bff1affff0bff3cff1280ff0980ffff0bff1aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffff0bff1affff0bff3cff1480ffff0bff1affff0bff1affff0bff3cff1280ff0580ffff0bff1affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 +ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff17ffff04ff0bff808080808080ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ff2fff80808080ff808080ff8080808080ff808080ffff04ffff04ff10ffff04ff81bfff808080ffff04ffff04ff2cffff01ff248080ffff02ff2fff5f80808080ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 341b5070c190..25884ad1fd82 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -30,9 +30,8 @@ P2_RECOVERY_MOD_HASH = P2_RECOVERY_MOD.get_tree_hash() RECOVERY_FINISH_MOD: Program = load_clvm("vault_recovery_finish.clsp") RECOVERY_FINISH_MOD_HASH = RECOVERY_FINISH_MOD.get_tree_hash() -P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp") +P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle_safe.clsp") P2_SINGLETON_MOD_HASH = P2_SINGLETON_MOD.get_tree_hash() -P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp") # PUZZLES @@ -110,7 +109,7 @@ def get_recovery_finish_puzzle( def get_p2_singleton_puzzle(launcher_id: bytes32) -> Program: singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH))) - puzzle = P2_SINGLETON_MOD.curry(singleton_struct, P2_SINGLETON_AGGREGATOR_MOD) + puzzle = P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, singleton_struct.get_tree_hash()) return puzzle diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 6545463046df..6038407ee068 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -235,11 +235,11 @@ async def _generate_unsigned_transaction( for coin in coins: if not p2_singleton_spends: p2_solution = Program.to( - [0, self.vault_info.inner_puzzle_hash, delegated_puzzle, delegated_solution, coin.name()] + [self.vault_info.inner_puzzle_hash, delegated_puzzle, delegated_solution, coin.name()] ) else: - p2_solution = Program.to([0, self.vault_info.inner_puzzle_hash, 0, 0, coin.name()]) - sb = SpendBundle([make_spend(coin, p2_singleton_puzzle, p2_solution)], G2Element()) + p2_solution = Program.to([self.vault_info.inner_puzzle_hash, 0, 0, coin.name()]) + p2_singleton_spends.append(make_spend(coin, p2_singleton_puzzle, p2_solution)) next_puzzle_hash = ( diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 893272c6f8a6..1d223a758a20 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -569,30 +569,30 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: [uint64(12381), uint64(8444), uint64(2), uint64(index)], ) - async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: - pks: List[bytes] = [] - signing_targets: List[SigningTarget] = [] - for coin_spend in coin_spends: - _coin_spend = coin_spend.as_coin_spend() - # Get AGG_SIG conditions - conditions_dict = conditions_dict_for_solution( - _coin_spend.puzzle_reveal.to_program(), - _coin_spend.solution.to_program(), - self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - ) - # Create signature - for pk, msg in pkm_pairs_for_conditions_dict( - conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ): - pk_bytes = bytes(pk) - pks.append(pk_bytes) - fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") - signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) - - return SigningInstructions( - await self.wallet_state_manager.key_hints_for_pubkeys(pks), - signing_targets, - ) + # async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: + # pks: List[bytes] = [] + # signing_targets: List[SigningTarget] = [] + # for coin_spend in coin_spends: + # _coin_spend = coin_spend.as_coin_spend() + # # Get AGG_SIG conditions + # conditions_dict = conditions_dict_for_solution( + # _coin_spend.puzzle_reveal.to_program(), + # _coin_spend.solution.to_program(), + # self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + # ) + # # Create signature + # for pk, msg in pkm_pairs_for_conditions_dict( + # conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + # ): + # pk_bytes = bytes(pk) + # pks.append(pk_bytes) + # fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") + # signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) + + # return SigningInstructions( + # await self.wallet_state_manager.key_hints_for_pubkeys(pks), + # signing_targets, + # ) async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False @@ -755,11 +755,12 @@ async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstruct self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, ) # Create signature - for pk_bytes, msg in pkm_pairs_for_conditions_dict( + for pk, msg in pkm_pairs_for_conditions_dict( conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA ): + pk_bytes = pk.to_bytes() pks.append(pk_bytes) - fingerprint: bytes = G1Element.from_bytes(pk_bytes).get_fingerprint().to_bytes(4, "big") + fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) return SigningInstructions( From dc0be9ce028e1bf04d7f00475a638907a29ed7ad Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 15 Jul 2024 13:56:11 +0100 Subject: [PATCH 215/274] tidy up tests and mypy etc --- chia/_tests/cmds/wallet/test_vault.py | 7 +-- chia/_tests/wallet/vault/test_vault_wallet.py | 52 +++++++++++++------ chia/cmds/vault.py | 49 ++++++++--------- chia/cmds/vault_funcs.py | 20 +++---- chia/wallet/vault/vault_drivers.py | 6 +-- chia/wallet/vault/vault_wallet.py | 20 ++++--- chia/wallet/wallet_protocol.py | 2 - chia/wallet/wallet_singleton_store.py | 2 +- chia/wallet/wallet_state_manager.py | 16 +++--- 9 files changed, 92 insertions(+), 82 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py index eb9ac39b3fdf..87c56697bf0f 100644 --- a/chia/_tests/cmds/wallet/test_vault.py +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -4,9 +4,9 @@ from typing import List, Optional, Tuple from chia_rs import Coin, G1Element, G2Element -from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert -from tests.cmds.wallet.test_consts import FINGERPRINT_ARG, WALLET_ID_ARG, get_bytes32 +from chia._tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert +from chia._tests.cmds.wallet.test_consts import FINGERPRINT_ARG, WALLET_ID_ARG, get_bytes32 from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint8, uint32, uint64 from chia.wallet.conditions import ConditionValidTimes @@ -61,6 +61,7 @@ async def vault_create( command_args = [ "vault", "create", + FINGERPRINT_ARG, "-pk", pk, "-rk", @@ -73,7 +74,7 @@ async def vault_create( fee, ] assert_list = ["Successfully created a Vault wallet"] - run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG], assert_list) + run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) def test_vault_recovery(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 4396ed1beb42..1402b50b1f59 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -10,12 +10,9 @@ from chia._tests.conftest import ConsensusMode from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework -from chia.rpc.wallet_request_types import GatherSigningInfo -from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment -from chia.wallet.signer_protocol import Spend from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo @@ -239,19 +236,13 @@ async def test_vault_recovery( await wallet.sync_vault_launcher() assert wallet.vault_info - p2_singleton_puzzle_hash = wallet.get_p2_singleton_puzzle_hash() + p2_addr = await wallet_environments.environments[0].rpc_client.get_next_address(wallet.id(), False) - coins_to_create = 2 funding_amount = uint64(1000000000) funding_wallet = wallet_environments.environments[1].xch_wallet - for _ in range(coins_to_create): - funding_tx = await funding_wallet.generate_signed_transaction( - funding_amount, - p2_singleton_puzzle_hash, - DEFAULT_TX_CONFIG, - memos=[wallet.vault_info.pubkey], - ) - await funding_wallet.wallet_state_manager.add_pending_transactions(funding_tx) + await wallet_environments.environments[1].rpc_client.send_transaction( + funding_wallet.id(), funding_amount, p2_addr, DEFAULT_TX_CONFIG + ) await wallet_environments.process_pending_states( [ @@ -264,7 +255,7 @@ async def test_vault_recovery( }, post_block_balance_updates={ 1: { - "confirmed_wallet_balance": funding_amount * 2, + "confirmed_wallet_balance": funding_amount, "set_remainder": True, } }, @@ -296,6 +287,7 @@ async def test_vault_recovery( }, post_block_balance_updates={ 1: { + "confirmed_wallet_balance": 1, "set_remainder": True, } }, @@ -307,7 +299,9 @@ async def test_vault_recovery( assert recovery_coin.parent_coin_info == vault_coin.name() wallet_environments.full_node.time_per_block = 100 - await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=2, guarantee_transaction_blocks=True) + await wallet_environments.full_node.farm_blocks_to_puzzlehash( + count=2, guarantee_transaction_blocks=True, farm_to=bytes32(b"1" * 32) + ) await wallet_environments.environments[1].rpc_client.push_transactions([finish_tx]) @@ -331,3 +325,31 @@ async def test_vault_recovery( recovered_coin = wallet.vault_info.coin assert recovered_coin.parent_coin_info == recovery_coin.name() + + # spend recovery balance + env.wallet_state_manager.config["test_sk"] = RECOVERY_SECP_SK + recipient_ph = await funding_wallet.get_new_puzzlehash() + amount = uint64(200) + + unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( + amount, recipient_ph, DEFAULT_TX_CONFIG, memos=[recipient_ph] + ) + + await wallet_environments.environments[0].rpc_client.push_transactions(unsigned_txs, sign=True) + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -amount - 1, + "set_remainder": True, + } + }, + ), + ], + ) diff --git a/chia/cmds/vault.py b/chia/cmds/vault.py index a6fcde48996e..dad63aa8969c 100644 --- a/chia/cmds/vault.py +++ b/chia/cmds/vault.py @@ -1,13 +1,14 @@ from __future__ import annotations import asyncio -from decimal import Decimal from typing import Optional, Sequence import click from chia.cmds import options -from chia.cmds.plotnft import validate_fee +from chia.cmds.param_types import AmountParamType, Bytes32ParamType, CliAmount, cli_amount_none +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 @click.group("vault", help="Manage your vault") @@ -56,36 +57,29 @@ def vault_cmd(ctx: click.Context) -> None: required=False, default=0, ) -@click.option( - "-m", - "--fee", - help="Set the fees per transaction, in XCH.", - type=str, - default="0", - show_default=True, - callback=validate_fee, -) +@options.create_fee() @click.option("-n", "--name", help="Set the vault name", type=str) @click.option( "-ma", "--min-coin-amount", help="Ignore coins worth less then this much XCH or CAT units", - type=str, + type=AmountParamType(), required=False, - default="0", + default=cli_amount_none, ) @click.option( "-l", "--max-coin-amount", help="Ignore coins worth more then this much XCH or CAT units", - type=str, + type=AmountParamType(), required=False, - default=None, + default=cli_amount_none, ) @click.option( "--exclude-coin", "coins_to_exclude", multiple=True, + type=Bytes32ParamType(), help="Exclude this coin from being spent.", ) @click.option( @@ -101,11 +95,11 @@ def vault_create_cmd( recovery_public_key: Optional[str], recovery_timelock: Optional[int], hidden_puzzle_index: Optional[int], - fee: str, + fee: uint64, name: Optional[str], - min_coin_amount: str, - max_coin_amount: Optional[str], - coins_to_exclude: Sequence[str], + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + coins_to_exclude: Sequence[bytes32], reuse: bool, ) -> None: from .vault_funcs import create_vault @@ -121,7 +115,7 @@ def vault_create_cmd( recovery_public_key, recovery_timelock, hidden_puzzle_index, - Decimal(fee), + fee, name, min_coin_amount=min_coin_amount, max_coin_amount=max_coin_amount, @@ -192,22 +186,23 @@ def vault_create_cmd( "-ma", "--min-coin-amount", help="Ignore coins worth less then this much XCH or CAT units", - type=str, + type=AmountParamType(), required=False, - default="0", + default=cli_amount_none, ) @click.option( "-l", "--max-coin-amount", help="Ignore coins worth more then this much XCH or CAT units", - type=str, + type=AmountParamType(), required=False, - default=None, + default=cli_amount_none, ) @click.option( "--exclude-coin", "coins_to_exclude", multiple=True, + type=Bytes32ParamType(), help="Exclude this coin from being spent.", ) @click.option( @@ -226,9 +221,9 @@ def vault_recover_cmd( recovery_timelock: Optional[int], recovery_initiate_file: str, recovery_finish_file: str, - min_coin_amount: str, - max_coin_amount: Optional[str], - coins_to_exclude: Sequence[str], + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + coins_to_exclude: Sequence[bytes32], reuse: bool, ) -> None: from .vault_funcs import recover_vault diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index ea1e191215ab..d98e4c9feee5 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -1,11 +1,12 @@ from __future__ import annotations import json -from decimal import Decimal from typing import Optional, Sequence from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client +from chia.cmds.param_types import CliAmount from chia.cmds.units import units +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 @@ -16,15 +17,14 @@ async def create_vault( recovery_public_key: Optional[str], timelock: Optional[int], hidden_puzzle_index: int, - d_fee: Decimal, + fee: uint64, name: Optional[str], - min_coin_amount: Optional[str], - max_coin_amount: Optional[str], - excluded_coin_ids: Sequence[str], + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + excluded_coin_ids: Sequence[bytes32], reuse_puzhash: Optional[bool], ) -> None: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): - fee: int = int(d_fee * units["chia"]) assert hidden_puzzle_index >= 0 tx_config = CMDTXConfigLoader( min_coin_amount=min_coin_amount, @@ -41,7 +41,7 @@ async def create_vault( tx_config, bytes.fromhex(recovery_public_key) if recovery_public_key else None, uint64(timelock) if timelock else None, - uint64(fee), + fee, push=True, ) print("Successfully created a Vault wallet") @@ -59,9 +59,9 @@ async def recover_vault( timelock: Optional[int], initiate_file: str, finish_file: str, - min_coin_amount: Optional[str], - max_coin_amount: Optional[str], - excluded_coin_ids: Sequence[str], + min_coin_amount: CliAmount, + max_coin_amount: CliAmount, + excluded_coin_ids: Sequence[bytes32], reuse_puzhash: Optional[bool], ) -> None: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index 25884ad1fd82..fe97e8429695 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Union from chia_rs import G1Element @@ -101,7 +101,7 @@ def get_vault_full_puzzle_hash(launcher_id: bytes32, inner_puzzle_hash: bytes32) def get_recovery_finish_puzzle( - new_vault_inner_puzhash: bytes32, timelock: uint64, amount: uint64, memos: List[bytes] + new_vault_inner_puzhash: bytes32, timelock: uint64, amount: uint64, memos: Union[List[bytes], Program] ) -> Program: recovery_condition = Program.to([[51, new_vault_inner_puzhash, amount, memos]]) return RECOVERY_FINISH_MOD.curry(timelock, recovery_condition) @@ -162,7 +162,7 @@ def get_new_vault_info_from_spend(spend: CoinSpend) -> Tuple[bytes, bytes32, Opt memos = cond.at("rrrf") if cond.list_len() == 4 else None assert memos is not None secp_pk = memos.at("f").as_atom() - hidden_puzzle_hash = memos.at("rf").as_atom() + hidden_puzzle_hash = bytes32(memos.at("rf").as_atom()) if memos.list_len() > 2: bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) timelock = uint64(memos.at("rrrf").as_int()) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 6038407ee068..7880992f0eec 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -7,7 +7,6 @@ from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element -from clvm.casts import int_to_bytes from ecdsa.keys import SigningKey from typing_extensions import Unpack @@ -117,7 +116,7 @@ async def get_new_puzzle(self) -> Program: ) return puzzle - async def get_new_puzzlehash(self) -> bytes32: + async def get_new_vault_puzzlehash(self) -> bytes32: puzzle = await self.get_new_puzzle() return puzzle.get_tree_hash() @@ -243,9 +242,9 @@ async def _generate_unsigned_transaction( p2_singleton_spends.append(make_spend(coin, p2_singleton_puzzle, p2_solution)) next_puzzle_hash = ( - self.vault_info.coin.puzzle_hash if tx_config.reuse_puzhash else (await self.get_new_puzzlehash()) + self.vault_info.coin.puzzle_hash if tx_config.reuse_puzhash else (await self.get_new_vault_puzzlehash()) ) - vault_conditions: List[Condition] = [] + vault_conditions: List[Program] = [] recreate_vault_condition = CreateCoin( next_puzzle_hash, uint64(self.vault_info.coin.amount), memos=[next_puzzle_hash] ).to_program() @@ -320,13 +319,13 @@ async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMo async def get_puzzle_hash(self, new: bool) -> bytes32: if new: - return await self.get_new_puzzlehash() + return self.get_p2_singleton_puzzle_hash() 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 self.get_p2_singleton_puzzle_hash() return record.puzzle_hash async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: @@ -362,8 +361,6 @@ async def apply_signatures( mod, curried_args = spend.puzzle.uncurry() if match_vault_puzzle(mod, curried_args): new_sol = spend.solution.replace(rrfrrfrrf=signing_responses[0].signature) - # spend.puzzle.run(new_sol) - # breakpoint() signed_spends.append(Spend(spend.coin, spend.puzzle, new_sol)) else: signed_spends.append(spend) @@ -424,8 +421,9 @@ async def get_puzzle(self, new: bool) -> Program: ) if record is None: return await self.get_new_puzzle() + assert isinstance(record._pubkey, bytes) puzzle = construct_p2_delegated_secp( - record.pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, record.puzzle_hash + record._pubkey, self.wallet_state_manager.constants.GENESIS_CHALLENGE, record.puzzle_hash ) return puzzle @@ -457,7 +455,7 @@ async def select_coins(self, amount: uint64, coin_selection_config: CoinSelectio ) puzhash = self.get_p2_singleton_puzzle_hash() records = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash(puzhash) - assert records is not None + assert records spendable_amount = uint128(sum([rec.coin.amount for rec in records])) coins = await select_coins( spendable_amount, @@ -515,7 +513,7 @@ async def create_recovery_spends( ) memos = [secp_pk, hidden_puzzle_hash] if bls_pk: - memos.extend([bls_pk.to_bytes(), int_to_bytes(timelock)]) + memos.extend([bls_pk.to_bytes(), Program.to(timelock).as_atom()]) # Generate the current inner puzzle inner_puzzle = get_vault_inner_puzzle( diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 21d7e6dfad25..c2c19e0a126f 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -154,8 +154,6 @@ def make_solution( async def get_puzzle(self, new: bool) -> Program: ... - async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: ... - async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: ... async def get_coins_to_offer( diff --git a/chia/wallet/wallet_singleton_store.py b/chia/wallet/wallet_singleton_store.py index 8afbe5f6ffe2..d6fd55d26bba 100644 --- a/chia/wallet/wallet_singleton_store.py +++ b/chia/wallet/wallet_singleton_store.py @@ -138,7 +138,7 @@ async def add_spend( cc_cond = [cond for cond in conditions[ConditionOpcode.CREATE_COIN] if int_from_bytes(cond.vars[1]) % 2 == 1][0] - coin = Coin(coin_spend.coin.name(), cc_cond.vars[0], int_from_bytes(cc_cond.vars[1])) + coin = Coin(coin_spend.coin.name(), cc_cond.vars[0], uint64(int_from_bytes(cc_cond.vars[1]))) inner_puz = get_inner_puzzle_from_singleton(coin_spend.puzzle_reveal) if inner_puz is None: # pragma: no cover raise RuntimeError("Could not get inner puzzle from puzzle reveal in coin spend %s", coin_spend) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 7b6c9258f323..e628d8e231a4 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -27,7 +27,6 @@ import aiosqlite from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey -from clvm.casts import int_to_bytes from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.consensus.coinbase import farmer_parent_id, pool_parent_id @@ -143,12 +142,7 @@ last_change_height_cs, ) from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType -from chia.wallet.vault.vault_drivers import ( - get_vault_full_puzzle_hash, - get_vault_inner_puzzle_hash, - match_recovery_puzzle, - match_vault_puzzle, -) +from chia.wallet.vault.vault_drivers import get_vault_full_puzzle_hash, get_vault_inner_puzzle_hash, match_vault_puzzle from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, ProofsChecker, construct_pending_approval_state @@ -1810,7 +1804,7 @@ async def _add_coin_states( # Confirm tx records for txs which we submitted for coins which aren't in our wallet # Used for vault recovery spends if coin_state.spent_height is not None: - all_unconfirmed: List[TransactionRecord] = await self.tx_store.get_all_unconfirmed() + all_unconfirmed = await self.tx_store.get_all_unconfirmed() tx_records: List[TransactionRecord] = [] for out_tx_record in all_unconfirmed: for rem_coin in out_tx_record.removals: @@ -1961,7 +1955,7 @@ async def _add_coin_states( # Reorg rollback adds reorged transactions so it's possible there is tx_record already # Even though we are just adding coin record to the db (after reorg) - tx_records: List[TransactionRecord] = [] + tx_records = [] for out_tx_record in all_unconfirmed: for rem_coin in out_tx_record.removals: if rem_coin == coin_state.coin: @@ -2388,8 +2382,10 @@ async def add_pending_transactions( # Check that tx_records have additions/removals since vault txs don't have them until they're signed for i, tx in enumerate(tx_records): if tx.additions == []: + assert isinstance(tx.spend_bundle, SpendBundle) tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) if tx.removals == []: + assert isinstance(tx.spend_bundle, SpendBundle) tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) tx_records[i] = tx @@ -2876,7 +2872,7 @@ async def create_vault_wallet( vault_full_puzzle_hash = get_vault_full_puzzle_hash(launcher_coin.name(), vault_inner_puzzle_hash) memos = [secp_pk, hidden_puzzle_hash] if bls_pk: - memos.extend([bls_pk.to_bytes(), int_to_bytes(timelock)]) + memos.extend([bls_pk.to_bytes(), Program.to(timelock).as_atom()]) genesis_launcher_solution = Program.to([vault_full_puzzle_hash, amount, memos]) announcement_message = genesis_launcher_solution.get_tree_hash() From 7606a14bf419734323fea6aa9e1e9bf7537a392d Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 15 Jul 2024 15:23:55 +0100 Subject: [PATCH 216/274] fix spend bundle assertion so we don't break offers --- chia/wallet/wallet_state_manager.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index e628d8e231a4..0454d246cee7 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -2381,12 +2381,11 @@ async def add_pending_transactions( # Check that tx_records have additions/removals since vault txs don't have them until they're signed for i, tx in enumerate(tx_records): - if tx.additions == []: - assert isinstance(tx.spend_bundle, SpendBundle) - tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) - if tx.removals == []: - assert isinstance(tx.spend_bundle, SpendBundle) - tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) + if tx.spend_bundle is not None: + if tx.additions == []: + tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) + if tx.removals == []: + tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) tx_records[i] = tx if push: From e56c52fe1bc8f1cebbca78c28ae1cd43fa5e58f8 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 16 Jul 2024 13:08:09 +0100 Subject: [PATCH 217/274] add singletons table to db_resync, fix naming issue breaking nft tests --- chia/wallet/wallet_node.py | 1 + chia/wallet/wallet_state_manager.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 66fe471c3333..e3e8341737a8 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -338,6 +338,7 @@ async def reset_sync_db(self, db_path: Union[Path, str], fingerprint: int) -> bo "tx_times", "pool_state_transitions", "singleton_records", + "singletons", "mirrors", "mirror_confirmations", "launchers", diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 0454d246cee7..2718fe48d7ad 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1805,14 +1805,14 @@ async def _add_coin_states( # Used for vault recovery spends if coin_state.spent_height is not None: all_unconfirmed = await self.tx_store.get_all_unconfirmed() - tx_records: List[TransactionRecord] = [] + tx_records_to_confirm: List[TransactionRecord] = [] for out_tx_record in all_unconfirmed: for rem_coin in out_tx_record.removals: if rem_coin == coin_state.coin: - tx_records.append(out_tx_record) + tx_records_to_confirm.append(out_tx_record) - if len(tx_records) > 0: - for tx_record in tx_records: + if len(tx_records_to_confirm) > 0: + for tx_record in tx_records_to_confirm: await self.tx_store.set_confirmed(tx_record.name, uint32(coin_state.spent_height)) self.log.debug(f"No wallet for coin state: {coin_state}") continue @@ -1955,7 +1955,7 @@ async def _add_coin_states( # Reorg rollback adds reorged transactions so it's possible there is tx_record already # Even though we are just adding coin record to the db (after reorg) - tx_records = [] + tx_records: List[TransactionRecord] = [] for out_tx_record in all_unconfirmed: for rem_coin in out_tx_record.removals: if rem_coin == coin_state.coin: @@ -2385,6 +2385,7 @@ async def add_pending_transactions( if tx.additions == []: tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) if tx.removals == []: + assert isinstance(tx.spend_bundle, SpendBundle) tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) tx_records[i] = tx From c270810d261a2e6bbf5df8aecee97ed35fff663b Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 16 Jul 2024 15:23:49 +0100 Subject: [PATCH 218/274] lint and mypy --- chia/wallet/vault/vault_wallet.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 7880992f0eec..0d0efa1815c4 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -212,7 +212,7 @@ async def _generate_unsigned_transaction( tx_config.coin_selection_config, ) assert len(coins) > 0 - selected_amount = sum([coin.amount for coin in coins]) + selected_amount = sum(coin.amount for coin in coins) assert selected_amount >= amount conditions = [primary.as_condition() for primary in primaries] @@ -456,7 +456,7 @@ async def select_coins(self, amount: uint64, coin_selection_config: CoinSelectio puzhash = self.get_p2_singleton_puzzle_hash() records = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash(puzhash) assert records - spendable_amount = uint128(sum([rec.coin.amount for rec in records])) + spendable_amount = uint128(sum(rec.coin.amount for rec in records)) coins = await select_coins( spendable_amount, coin_selection_config, @@ -685,6 +685,7 @@ async def update_vault_singleton( hints, _ = compute_spend_hints_and_additions(coin_spend) inner_puzzle_hash = hints[coin_state.coin.name()].hint dr = None + new_recovery_info = None replace_key = False replace_recovery = False if inner_puzzle_hash is not None: @@ -739,7 +740,11 @@ async def update_vault_singleton( lineage_proof = LineageProof( parent_state.coin.parent_coin_info, parent_inner_puzzle_hash, parent_state.coin.amount ) - # assert hidden_puzzle_hash is not None + if replace_recovery: + assert isinstance(new_recovery_info, RecoveryInfo) + recovery: RecoveryInfo = new_recovery_info + else: + recovery = self.vault_info.recovery_info new_vault_info = VaultInfo( coin_state.coin, self.vault_info.pubkey if not replace_key else new_secp_pk, @@ -747,7 +752,7 @@ async def update_vault_singleton( next_inner_puzzle.get_tree_hash(), lineage_proof, self.vault_info.is_recoverable if not replace_key else replace_recovery, - self.vault_info.recovery_info if not replace_recovery else new_recovery_info, + recovery, ) await self.update_vault_store(new_vault_info, coin_spend) From 301231ab61de1a7e9390ba3178402fab6af785d1 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 17 Jul 2024 14:33:06 +0100 Subject: [PATCH 219/274] check for vault puz when confirming recovery txs --- chia/_tests/wallet/vault/test_vault_wallet.py | 39 ++++++++++++++++--- chia/wallet/wallet_state_manager.py | 10 +++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 1402b50b1f59..88b8caef8ae2 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -213,12 +213,14 @@ async def test_vault_creation( ) @pytest.mark.parametrize("setup_function", [vault_setup]) @pytest.mark.parametrize("with_recovery", [True]) +@pytest.mark.parametrize("spent_recovery", [True, False]) @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp") @pytest.mark.anyio async def test_vault_recovery( wallet_environments: WalletTestFramework, setup_function: Callable[[WalletTestFramework, bool], Awaitable[None]], with_recovery: bool, + spent_recovery: bool, ) -> None: await setup_function(wallet_environments, with_recovery) env = wallet_environments.environments[0] @@ -263,6 +265,34 @@ async def test_vault_recovery( ], ) + # make a spend before recovery + if spent_recovery: + amount = uint64(10000) + recipient_ph = await funding_wallet.get_new_puzzlehash() + unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( + amount, recipient_ph, DEFAULT_TX_CONFIG, memos=[recipient_ph] + ) + + await wallet_environments.environments[0].rpc_client.push_transactions(unsigned_txs, sign=True) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={ + 1: { + "set_remainder": True, + } + }, + post_block_balance_updates={ + 1: { + "confirmed_wallet_balance": -amount + 1, + "set_remainder": True, + } + }, + ), + ], + ) + [initiate_tx, finish_tx] = await env.rpc_client.vault_recovery( wallet_id=wallet.id(), secp_pk=RECOVERY_SECP_PK, @@ -281,13 +311,12 @@ async def test_vault_recovery( WalletStateTransition( pre_block_balance_updates={ 1: { - "init": True, "set_remainder": True, } }, post_block_balance_updates={ 1: { - "confirmed_wallet_balance": 1, + "<=#confirmed_wallet_balance": 1, "set_remainder": True, } }, @@ -310,12 +339,12 @@ async def test_vault_recovery( WalletStateTransition( pre_block_balance_updates={ 1: { - "init": True, "set_remainder": True, } }, post_block_balance_updates={ 1: { + "<=#confirmed_wallet_balance": 1, "set_remainder": True, } }, @@ -331,11 +360,11 @@ async def test_vault_recovery( recipient_ph = await funding_wallet.get_new_puzzlehash() amount = uint64(200) - unsigned_txs: List[TransactionRecord] = await wallet.generate_signed_transaction( + unsigned_spend: List[TransactionRecord] = await wallet.generate_signed_transaction( amount, recipient_ph, DEFAULT_TX_CONFIG, memos=[recipient_ph] ) - await wallet_environments.environments[0].rpc_client.push_transactions(unsigned_txs, sign=True) + await wallet_environments.environments[0].rpc_client.push_transactions(unsigned_spend, sign=True) await wallet_environments.process_pending_states( [ WalletStateTransition( diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 2718fe48d7ad..4efd286077ba 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1807,9 +1807,13 @@ async def _add_coin_states( all_unconfirmed = await self.tx_store.get_all_unconfirmed() tx_records_to_confirm: List[TransactionRecord] = [] for out_tx_record in all_unconfirmed: - for rem_coin in out_tx_record.removals: - if rem_coin == coin_state.coin: - tx_records_to_confirm.append(out_tx_record) + if out_tx_record.spend_bundle is not None: + for tx_spend in out_tx_record.spend_bundle.coin_spends: + if tx_spend.coin == coin_state.coin: + # check for a vault spend + mod, args = tx_spend.puzzle_reveal.to_program().uncurry() + if match_vault_puzzle(mod, args): + tx_records_to_confirm.append(out_tx_record) if len(tx_records_to_confirm) > 0: for tx_record in tx_records_to_confirm: From 8323b113232fb3df8ca723e3d867c29f950da597 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Thu, 18 Jul 2024 13:40:08 +0100 Subject: [PATCH 220/274] coverage fixes --- chia/_tests/wallet/vault/test_vault_wallet.py | 7 ------- chia/cmds/vault.py | 5 +---- chia/wallet/vault/vault_drivers.py | 5 ++--- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 88b8caef8ae2..9b26b82f389e 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -83,13 +83,6 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b await wallet_environments.full_node.wait_for_wallet_synced(env.node, 20) -def sign_message(message: bytes) -> bytes: - seed = b"chia_secp" - SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) - signed_message: bytes = SECP_SK.sign_deterministic(message) - return signed_message - - @pytest.mark.parametrize( "wallet_environments", [{"num_environments": 2, "blocks_needed": [1, 1]}], diff --git a/chia/cmds/vault.py b/chia/cmds/vault.py index dad63aa8969c..86cc0434abd5 100644 --- a/chia/cmds/vault.py +++ b/chia/cmds/vault.py @@ -94,7 +94,7 @@ def vault_create_cmd( public_key: str, recovery_public_key: Optional[str], recovery_timelock: Optional[int], - hidden_puzzle_index: Optional[int], + hidden_puzzle_index: int, fee: uint64, name: Optional[str], min_coin_amount: CliAmount, @@ -104,9 +104,6 @@ def vault_create_cmd( ) -> None: from .vault_funcs import create_vault - if hidden_puzzle_index is None: - hidden_puzzle_index = 0 - asyncio.run( create_vault( wallet_rpc_port, diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index fe97e8429695..bc3d3f68b28c 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -163,12 +163,11 @@ def get_new_vault_info_from_spend(spend: CoinSpend) -> Tuple[bytes, bytes32, Opt assert memos is not None secp_pk = memos.at("f").as_atom() hidden_puzzle_hash = bytes32(memos.at("rf").as_atom()) + bls_pk = None + timelock = None if memos.list_len() > 2: bls_pk = G1Element.from_bytes(memos.at("rrf").as_atom()) timelock = uint64(memos.at("rrrf").as_int()) - else: - bls_pk = None - timelock = None break return secp_pk, hidden_puzzle_hash, bls_pk, timelock From 2920b724262e333257483dbdd223f963cd655ec4 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Mon, 22 Jul 2024 17:14:20 +0100 Subject: [PATCH 221/274] Address Quex's comments --- chia/_tests/wallet/vault/test_vault_wallet.py | 10 +++----- .../puzzles/deployed_puzzle_hashes.json | 4 +-- ...2_singleton_via_delegated_puzzle_safe.clsp | 2 +- ...ngleton_via_delegated_puzzle_safe.clsp.hex | 2 +- chia/wallet/puzzles/vault_p2_recovery.clsp | 19 ++++++-------- .../wallet/puzzles/vault_p2_recovery.clsp.hex | 2 +- chia/wallet/vault/vault_wallet.py | 15 +++-------- chia/wallet/wallet.py | 25 ------------------- 8 files changed, 20 insertions(+), 59 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 9b26b82f389e..17c7060375f9 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json from hashlib import sha256 from typing import Awaitable, Callable, List @@ -15,7 +14,7 @@ from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG -from chia.wallet.vault.vault_info import RecoveryInfo, VaultInfo +from chia.wallet.vault.vault_info import VaultInfo from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault @@ -182,11 +181,10 @@ async def test_vault_creation( assert record is not None assert isinstance(record.custom_data, bytes) - custom_data = json.loads(record.custom_data) - vault_info = VaultInfo.from_json_dict(custom_data["vault_info"]) + custom_data = record.custom_data + vault_info = VaultInfo.from_bytes(custom_data) assert vault_info == wallet.vault_info - recovery_info = RecoveryInfo.from_json_dict(custom_data["vault_info"]["recovery_info"]) - assert recovery_info == wallet.vault_info.recovery_info + assert vault_info.recovery_info == wallet.vault_info.recovery_info # test make_solution coin = (await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG)).pop() diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index 1e16662a19bf..6942bf536a10 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -52,7 +52,7 @@ "p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834", "p2_singleton_aggregator": "f79a31fcfe3736cc75720617b4cdcb4376b4b8f8f71108617710612b909a4924", "p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b", - "p2_singleton_via_delegated_puzzle_safe": "ca336579834fad16c1b56946f4ad697aa7c37975836332178320e242b482bebd", + "p2_singleton_via_delegated_puzzle_safe": "20f2e0c7e3ccc1dc035f23fb70d4cbeb32b232615a916d56bc7d04d1d15b3121", "p2_singleton_via_delegated_puzzle_w_aggregator": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e", "pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494", "pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307", @@ -65,7 +65,7 @@ "std_parent_morpher": "8c3f1dc2e46c0d7ec4c2cbd007e23c0368ff8f80c5bc0101647a5c27626ebce6", "test_generator_deserialize": "52add794fc76e89512e4a063c383418bda084c8a78c74055abe80179e4a7832c", "test_multiple_generator_input_arguments": "156dafbddc3e1d3bfe1f2a84e48e5e46b287b8358bf65c3c091c93e855fbfc5b", - "vault_p2_recovery": "59d20b29c583990dca222583cf6fa6b03189841b18ad866e1271aeeec9774828", + "vault_p2_recovery": "751f07d363747c2d1c9fd8a2ac7ee3aca0c03a806f3f7553259642365ec8e56c", "vault_recovery_finish": "14cb5fba485853fee53316849e52948916cc5893657632f431e367a889fd37c7", "viral_backdoor": "00848115554ea674131f89f311707a959ad3f4647482648f3fe91ba289131f51" } diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp index 2acfd39399e1..759d4af25427 100644 --- a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp @@ -28,7 +28,7 @@ (c (list ASSERT_MY_COIN_ID my_id) (c - (list CREATE_COIN_ANNOUNCEMENT '$') + (list CREATE_COIN_ANNOUNCEMENT ()) (a delegated_puzzle delegated_solution) ) ) diff --git a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex index 56bc05e77d83..0eaf83e76dae 100644 --- a/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex +++ b/chia/wallet/puzzles/p2_singleton_via_delegated_puzzle_safe.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff17ffff04ff0bff808080808080ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ff2fff80808080ff808080ff8080808080ff808080ffff04ffff04ff10ffff04ff81bfff808080ffff04ffff04ff2cffff01ff248080ffff02ff2fff5f80808080ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 +ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff17ffff04ff0bff808080808080ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ff2fff80808080ff808080ff8080808080ff808080ffff04ffff04ff10ffff04ff81bfff808080ffff04ffff04ff2cffff01ff808080ffff02ff2fff5f80808080ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp b/chia/wallet/puzzles/vault_p2_recovery.clsp index 046faeb2bfd4..2041f6c1ee23 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp @@ -47,17 +47,14 @@ ) ) - (let - ( - (recovery_puzzlehash - (create_recovery_puzzlehash - P2_1_OF_N_MOD_HASH - FINISH_RECOVERY_MOD_HASH - P2_SECP_PUZZLEHASH - TIMELOCK - recovery_conditions - ) - ) + (assign + recovery_puzzlehash + (create_recovery_puzzlehash + P2_1_OF_N_MOD_HASH + FINISH_RECOVERY_MOD_HASH + P2_SECP_PUZZLEHASH + TIMELOCK + recovery_conditions ) (list (list AGG_SIG_ME BLS_PK (sha256tree recovery_conditions)) diff --git a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex index dbd0bee1d90f..56f61f6a7b41 100644 --- a/chia/wallet/puzzles/vault_p2_recovery.clsp.hex +++ b/chia/wallet/puzzles/vault_p2_recovery.clsp.hex @@ -1 +1 @@ -ff02ffff01ff04ffff04ffff0132ffff04ffff05ffff06ffff06ffff06ffff06ff018080808080ffff04ffff02ff08ffff04ff02ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ffff06ff018080808080808080ff80808080ffff0180808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff0180808080808080ffff04ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ffff018080ffff018080808080ffff04ffff04ffff0149ffff04ffff05ffff06ffff06ffff06ffff06ffff06ffff06ff0180808080808080ffff01808080ffff0180808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ffff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff0cffff04ff02ffff04ffff05ff0580ffff04ffff02ff0affff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff0cffff04ff02ffff04ff05ffff04ffff02ff0affff04ff02ffff04ff07ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff16ffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff018080 +ff02ffff01ff02ff1effff04ff02ffff04ffff06ff0180ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff5fffff04ff82017fff8080808080808080ff8080808080ffff04ffff01ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff08ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff08ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ff0bffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a8080ff02ffff03ff05ffff01ff02ffff01ff0bffff06ffff06ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ffff02ff14ffff04ff02ffff04ffff05ff0580ffff04ffff02ff1cffff04ff02ffff04ffff06ff0580ff80808080ff808080808080ff0180ffff01ff02ffff01ff06ffff05ffff01ffffa04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459aa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ffa102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222a102a8d5dd63fba471ebcb1f3e8f7c1e1879b7152a6e7298a91ce119a63400ade7c58080ff018080ff0180ffff0bffff01a102a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff02ff14ffff04ff02ffff04ff05ffff04ffff02ff1cffff04ff02ffff04ff07ff80808080ff808080808080ffff02ff0affff04ff02ffff04ff05ffff04ffff0bffff0101ffff0bffff0102ffff0bffff0101ff1780ffff0bffff0101ffff02ff0affff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff02ff08ffff04ff02ffff04ff5fff80808080ff808080808080808080ff8080808080ff04ffff04ffff0132ffff04ff5dffff04ffff02ff08ffff04ff02ffff04ff8202fdff80808080ff80808080ffff04ffff04ffff0133ffff04ff0bffff04ff82017dffff04ffff04ff0bff8080ff8080808080ffff04ffff04ffff0149ffff04ff82017dff808080ff80808080ff018080 diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 0d0efa1815c4..f66b0bd95a84 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import logging import time from dataclasses import dataclass @@ -256,7 +255,7 @@ async def _generate_unsigned_transaction( CreatePuzzleAnnouncement( Program.to([spend.coin.name(), puzzle_to_assert.get_tree_hash()]).get_tree_hash(), ).to_program(), - AssertCoinAnnouncement(asserted_id=spend.coin.name(), asserted_msg=b"$").to_program(), + AssertCoinAnnouncement(asserted_id=spend.coin.name(), asserted_msg=b"").to_program(), ] ) @@ -624,7 +623,6 @@ async def sync_vault_launcher(self) -> None: if not coin_states: raise ValueError(f"No coin found for launcher id: {self.launcher_id}.") coin_state: CoinState = coin_states[0] - # parent_state: CoinState = (await wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer))[0] assert coin_state.spent_height is not None launcher_spend = await fetch_coin_spend(uint32(coin_state.spent_height), coin_state.coin, peer) @@ -676,7 +674,7 @@ async def sync_vault_launcher(self) -> None: lineage_proof, uint32(coin_state.spent_height) if coin_state.spent_height else uint32(0), pending=False, - custom_data=bytes(json.dumps(vault_info.to_json_dict()), "utf-8"), + custom_data=vault_info.stream_to_bytes(), ) async def update_vault_singleton( @@ -762,12 +760,5 @@ async def save_info(self, vault_info: VaultInfo) -> None: self._vault_info = vault_info async def update_vault_store(self, vault_info: VaultInfo, coin_spend: CoinSpend) -> None: - custom_data = bytes( - json.dumps( - { - "vault_info": vault_info.to_json_dict(), - } - ), - "utf-8", - ) + custom_data = vault_info.stream_to_bytes() await self.wallet_state_manager.singleton_store.add_spend(self.id(), coin_spend, custom_data=custom_data) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 1d223a758a20..1bbd6ead42bb 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -569,31 +569,6 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: [uint64(12381), uint64(8444), uint64(2), uint64(index)], ) - # async def gather_signing_info(self, coin_spends: list[Spend]) -> SigningInstructions: - # pks: List[bytes] = [] - # signing_targets: List[SigningTarget] = [] - # for coin_spend in coin_spends: - # _coin_spend = coin_spend.as_coin_spend() - # # Get AGG_SIG conditions - # conditions_dict = conditions_dict_for_solution( - # _coin_spend.puzzle_reveal.to_program(), - # _coin_spend.solution.to_program(), - # self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, - # ) - # # Create signature - # for pk, msg in pkm_pairs_for_conditions_dict( - # conditions_dict, _coin_spend.coin, self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - # ): - # pk_bytes = bytes(pk) - # pks.append(pk_bytes) - # fingerprint: bytes = pk.get_fingerprint().to_bytes(4, "big") - # signing_targets.append(SigningTarget(fingerprint, msg, std_hash(pk_bytes + msg))) - - # return SigningInstructions( - # await self.wallet_state_manager.key_hints_for_pubkeys(pks), - # signing_targets, - # ) - async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: From 72379295bb388c7c68b3e8eccb1aa6207417ec45 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 23 Jul 2024 10:43:11 +0100 Subject: [PATCH 222/274] replace ecdsa with cryptography --- chia/_tests/wallet/vault/test_vault_clsp.py | 43 ++++++++++++------- .../wallet/vault/test_vault_lifecycle.py | 30 ++++++++----- chia/_tests/wallet/vault/test_vault_wallet.py | 18 ++++---- chia/wallet/vault/vault_wallet.py | 14 ++++-- setup.py | 3 +- 5 files changed, 67 insertions(+), 41 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_clsp.py b/chia/_tests/wallet/vault/test_vault_clsp.py index f7ad5661d61e..4cf6c8fa12fa 100644 --- a/chia/_tests/wallet/vault/test_vault_clsp.py +++ b/chia/_tests/wallet/vault/test_vault_clsp.py @@ -1,12 +1,14 @@ from __future__ import annotations -from hashlib import sha256 from typing import Optional import pytest from chia_rs import G1Element, PrivateKey -from ecdsa import NIST256p, SigningKey -from ecdsa.util import PRNG +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from chia._tests.clvm.test_puzzles import secret_exponent_for_index from chia.consensus.default_constants import DEFAULT_CONSTANTS @@ -29,9 +31,15 @@ ACS_PH: bytes32 = ACS.get_tree_hash() # setup keys -seed = b"chia_secp" -secp_sk = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) -secp_pk = secp_sk.verifying_key.to_string("compressed") +seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 +secp_sk = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) +secp_pk = secp_sk.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + + +def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") def test_secp_hidden() -> None: @@ -92,11 +100,12 @@ def test_recovery_puzzles() -> None: escape_proof = Program.to((proof[0], proof[1][0])) delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) - signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH + sig = sign_message( + secp_sk, + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH, ) secp_solution = Program.to( - [delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] + [delegated_puzzle, delegated_solution, sig, coin_id, DEFAULT_CONSTANTS.GENESIS_CHALLENGE] ) escape_solution = Program.to([escape_proof, escape_puzzle, secp_solution]) escape_conds = conditions_dict_for_solution(recovery_puzzle, escape_solution, INFINITE_COST) @@ -109,17 +118,18 @@ def test_p2_delegated_secp() -> None: coin_id = Program.to("coin_id").get_tree_hash() delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, 1000]]) - signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH + sig = sign_message( + secp_sk, + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH, ) - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) + secp_solution = Program.to([delegated_puzzle, delegated_solution, sig, coin_id]) conds = secp_puzzle.run(secp_solution) assert conds.at("rfrf").as_atom() == ACS_PH # test that a bad secp sig fails - sig_bytes = bytearray(signed_delegated_puzzle) + sig_bytes = bytearray(sig) sig_bytes[0] ^= (sig_bytes[0] + 1) % 256 bad_signature = bytes(sig_bytes) @@ -155,10 +165,11 @@ def test_vault_root_puzzle() -> None: # secp spend path delegated_puzzle = ACS delegated_solution = Program.to([[51, ACS_PH, amount]]) - signed_delegated_puzzle = secp_sk.sign_deterministic( - delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH + sig = sign_message( + secp_sk, + delegated_puzzle.get_tree_hash() + coin_id + DEFAULT_CONSTANTS.GENESIS_CHALLENGE + DEFAULT_HIDDEN_PUZZLE_HASH, ) - secp_solution = Program.to([delegated_puzzle, delegated_solution, signed_delegated_puzzle, coin_id]) + secp_solution = Program.to([delegated_puzzle, delegated_solution, sig, coin_id]) proof = vault_merkle_tree.generate_proof(secp_puzzlehash) secp_proof = Program.to((proof[0], proof[1][0])) vault_solution = Program.to([secp_proof, secp_puzzle, secp_solution]) diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py index b782bf9fa27c..93d136db0484 100644 --- a/chia/_tests/wallet/vault/test_vault_lifecycle.py +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -1,12 +1,14 @@ from __future__ import annotations -from hashlib import sha256 from typing import Optional, Tuple import pytest from chia_rs import AugSchemeMPL, G2Element, PrivateKey -from ecdsa import NIST256p, SigningKey -from ecdsa.util import PRNG +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from chia._tests.clvm.test_puzzles import secret_exponent_for_index from chia.clvm.spend_sim import CostLogger, sim_and_client @@ -29,9 +31,9 @@ get_vault_proof, ) -seed = b"chia_secp" -SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) -SECP_PK = SECP_SK.verifying_key.to_string("compressed") +seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 +SECP_SK = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) +SECP_PK = SECP_SK.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) BLS_SK = PrivateKey.from_bytes(secret_exponent_for_index(1).to_bytes(32, "big")) BLS_PK = BLS_SK.get_g1() @@ -42,6 +44,12 @@ HIDDEN_PUZZLE_HASH = Program.to("hph").get_tree_hash() +def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + + @pytest.mark.anyio async def test_vault_inner(cost_logger: CostLogger) -> None: async with sim_and_client() as (sim, client): @@ -67,13 +75,14 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_conditions = Program.to([[51, ACS_PH, amount], [51, vault_puzzlehash, vault_coin.amount - amount]]) secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) - secp_signature = SECP_SK.sign_deterministic( + secp_signature = sign_message( + SECP_SK, construct_secp_message( secp_delegated_puzzle.get_tree_hash(), vault_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE, HIDDEN_PUZZLE_HASH, - ) + ), ) secp_solution = Program.to( @@ -155,13 +164,14 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: secp_conditions = Program.to([[51, ACS_PH, recovery_coin.amount]]) secp_delegated_puzzle = puzzle_for_conditions(secp_conditions) secp_delegated_solution = solution_for_conditions(secp_delegated_puzzle) - secp_signature = SECP_SK.sign_deterministic( + secp_signature = sign_message( + SECP_SK, construct_secp_message( secp_delegated_puzzle.get_tree_hash(), recovery_coin.name(), DEFAULT_CONSTANTS.GENESIS_CHALLENGE, HIDDEN_PUZZLE_HASH, - ) + ), ) secp_solution = Program.to( [ diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 17c7060375f9..2724c31f7dca 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -1,11 +1,11 @@ from __future__ import annotations -from hashlib import sha256 from typing import Awaitable, Callable, List import pytest -from ecdsa import NIST256p, SigningKey -from ecdsa.util import PRNG +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from chia._tests.conftest import ConsensusMode from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework @@ -21,9 +21,9 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: bool) -> None: env = wallet_environments.environments[0] - seed = b"chia_secp" - SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(seed), hashfunc=sha256) - SECP_PK = SECP_SK.verifying_key.to_string("compressed") + seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 + SECP_SK = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) + SECP_PK = SECP_SK.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) # Temporary hack so execute_signing_instructions can access the key env.wallet_state_manager.config["test_sk"] = SECP_SK @@ -216,9 +216,9 @@ async def test_vault_recovery( await setup_function(wallet_environments, with_recovery) env = wallet_environments.environments[0] assert isinstance(env.xch_wallet, Vault) - recovery_seed = b"recovery_chia_secp" - RECOVERY_SECP_SK = SigningKey.generate(curve=NIST256p, entropy=PRNG(recovery_seed), hashfunc=sha256) - RECOVERY_SECP_PK = RECOVERY_SECP_SK.verifying_key.to_string("compressed") + recovery_seed = 0x6D836489B057E59FF0E16CE2D8F876C454697B76549E11D93F8102C4140B2DC5 + RECOVERY_SECP_SK = ec.derive_private_key(recovery_seed, ec.SECP256R1(), default_backend()) + RECOVERY_SECP_PK = RECOVERY_SECP_SK.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) client = wallet_environments.environments[1].rpc_client fingerprint = (await client.get_public_keys())[0] bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index f66b0bd95a84..0466b7189a90 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -6,7 +6,9 @@ from typing import Any, Dict, List, Optional, Set, Tuple from chia_rs import G1Element, G2Element -from ecdsa.keys import SigningKey +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature from typing_extensions import Unpack from chia.protocols.wallet_protocol import CoinState @@ -372,8 +374,9 @@ async def execute_signing_instructions( self, signing_instructions: SigningInstructions, partial_allowed: bool = False ) -> List[SigningResponse]: root_pubkey = self.wallet_state_manager.observation_root - sk: SigningKey = self.wallet_state_manager.config["test_sk"] # Temporary access to private key - sk_lookup: Dict[int, SigningKey] = {root_pubkey.get_fingerprint(): sk} + # Temporary access to private key + sk: ec.EllipticCurvePrivateKey = self.wallet_state_manager.config["test_sk"] + sk_lookup: Dict[int, ec.EllipticCurvePrivateKey] = {root_pubkey.get_fingerprint(): sk} responses: List[SigningResponse] = [] # We don't need to expand path and sum hints since vault signer always uses the same keys @@ -382,9 +385,12 @@ async def execute_signing_instructions( fingerprint: int = int.from_bytes(target.fingerprint, "big") if fingerprint not in sk_lookup: raise ValueError(f"Pubkey {fingerprint} not found") + der_sig = sk_lookup[fingerprint].sign(target.message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") responses.append( SigningResponse( - sk_lookup[fingerprint].sign_deterministic(target.message), + sig, target.hook, ) ) diff --git a/setup.py b/setup.py index 1e979441cc9c..eb6b3202c2cd 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ "colorama==0.4.6", # Colorizes terminal output "colorlog==6.8.2", # Adds color to logs "concurrent-log-handler==0.9.25", # Concurrently log and rotate logs - "cryptography==42.0.5", # Python cryptography library for TLS - keyring conflict + "cryptography==43.0.0", # Python cryptography library for TLS - keyring conflict "filelock==3.14.0", # For reading and writing config multiprocess and multithread safely (non-reentrant locks) "importlib-resources==6.4.0", "keyring==25.1.0", # Store keys in MacOS Keychain, Windows Credential Locker @@ -38,7 +38,6 @@ "packaging==24.0", "psutil==5.9.4", "hsms==0.3.1", - "ecdsa==0.19.0", # For SECP ] upnp_dependencies = [ From ef7b17e260029ef3b8effdce60f21d383d5c0010 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 23 Jul 2024 13:13:42 +0100 Subject: [PATCH 223/274] mypy ignore call-arg for deterministic_signing --- chia/_tests/wallet/vault/test_vault_clsp.py | 2 +- chia/_tests/wallet/vault/test_vault_lifecycle.py | 2 +- chia/wallet/vault/vault_wallet.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_clsp.py b/chia/_tests/wallet/vault/test_vault_clsp.py index 4cf6c8fa12fa..46f6c866ed98 100644 --- a/chia/_tests/wallet/vault/test_vault_clsp.py +++ b/chia/_tests/wallet/vault/test_vault_clsp.py @@ -37,7 +37,7 @@ def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: - der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) # type: ignore[call-arg] r, s = decode_dss_signature(der_sig) return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py index 93d136db0484..f5516f651f2e 100644 --- a/chia/_tests/wallet/vault/test_vault_lifecycle.py +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -45,7 +45,7 @@ def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: - der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) # type: ignore[call-arg] r, s = decode_dss_signature(der_sig) return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 0466b7189a90..e2c5e51daeb8 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -385,7 +385,9 @@ async def execute_signing_instructions( fingerprint: int = int.from_bytes(target.fingerprint, "big") if fingerprint not in sk_lookup: raise ValueError(f"Pubkey {fingerprint} not found") - der_sig = sk_lookup[fingerprint].sign(target.message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + der_sig = sk_lookup[fingerprint].sign( + target.message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True) # type: ignore[call-arg] + ) r, s = decode_dss_signature(der_sig) sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") responses.append( From 7d3a262ca3a08d07458089e4cb36dfb8bb559270 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Tue, 23 Jul 2024 13:51:37 +0100 Subject: [PATCH 224/274] remove types-cryptography --- chia/_tests/wallet/vault/test_vault_clsp.py | 2 +- chia/_tests/wallet/vault/test_vault_lifecycle.py | 2 +- chia/ssl/create_ssl.py | 1 + chia/wallet/vault/vault_wallet.py | 4 +--- setup.py | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_clsp.py b/chia/_tests/wallet/vault/test_vault_clsp.py index 46f6c866ed98..4cf6c8fa12fa 100644 --- a/chia/_tests/wallet/vault/test_vault_clsp.py +++ b/chia/_tests/wallet/vault/test_vault_clsp.py @@ -37,7 +37,7 @@ def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: - der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) # type: ignore[call-arg] + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) r, s = decode_dss_signature(der_sig) return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py index f5516f651f2e..93d136db0484 100644 --- a/chia/_tests/wallet/vault/test_vault_lifecycle.py +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -45,7 +45,7 @@ def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes: - der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) # type: ignore[call-arg] + der_sig = private_key.sign(message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) r, s = decode_dss_signature(der_sig) return r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") diff --git a/chia/ssl/create_ssl.py b/chia/ssl/create_ssl.py index fd16332d3921..15abe3d405d1 100644 --- a/chia/ssl/create_ssl.py +++ b/chia/ssl/create_ssl.py @@ -68,6 +68,7 @@ def generate_ca_signed_cert(ca_crt: bytes, ca_key: bytes, cert_out: Path, key_ou one_day = datetime.timedelta(1, 0, 0) root_cert = x509.load_pem_x509_certificate(ca_crt, default_backend()) root_key = load_pem_private_key(ca_key, None, default_backend()) + assert isinstance(root_key, rsa.RSAPrivateKey) cert_key = rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()) new_subject = x509.Name( diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index e2c5e51daeb8..0466b7189a90 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -385,9 +385,7 @@ async def execute_signing_instructions( fingerprint: int = int.from_bytes(target.fingerprint, "big") if fingerprint not in sk_lookup: raise ValueError(f"Pubkey {fingerprint} not found") - der_sig = sk_lookup[fingerprint].sign( - target.message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True) # type: ignore[call-arg] - ) + der_sig = sk_lookup[fingerprint].sign(target.message, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) r, s = decode_dss_signature(der_sig) sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") responses.append( diff --git a/setup.py b/setup.py index eb6b3202c2cd..060960334f55 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ "aiohttp_cors==0.7.0", # For blackd "pyinstaller==6.7.0", "types-aiofiles==23.2.0.20240311", - "types-cryptography==3.3.23.2", "types-pyyaml==6.0.12.20240311", "types-setuptools==70.0.0.20240524", ] From 968293fbf34853fd9293ed91e988796ce0d4bed0 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jul 2024 12:35:46 -0700 Subject: [PATCH 225/274] Fix tests --- chia/_tests/core/util/test_keychain.py | 2 +- chia/_tests/wallet/test_main_wallet_protocol.py | 2 +- chia/_tests/wallet/vault/test_vault_wallet.py | 14 +++++--------- chia/rpc/wallet_rpc_api.py | 2 -- chia/wallet/wallet_state_manager.py | 10 +++------- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index 1badd6f5a863..66c12f4591a8 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -290,7 +290,7 @@ def test_key_data_generate(label: Optional[str]) -> None: def test_key_data_creation(label: str, key_info: KeyInfo, get_item: str, from_method: Callable[..., KeyData]) -> None: key_data = from_method(getattr(key_info, get_item), label) assert key_data.fingerprint == key_info.fingerprint - assert key_data.public_key == key_info.public_key + assert key_data.public_key == bytes(key_info.public_key) assert key_data.mnemonic == key_info.mnemonic.split() assert key_data.mnemonic_str() == key_info.mnemonic assert key_data.entropy == key_info.entropy diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py index c412025b9d82..2117db3ec259 100644 --- a/chia/_tests/wallet/test_main_wallet_protocol.py +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -268,7 +268,7 @@ async def test_main_wallet( ) ] ) - async with main_wallet.wallet_state_manager.new_action_scope() as action_scope: + async with main_wallet.wallet_state_manager.new_action_scope(push=True, sign=True) as action_scope: await main_wallet.generate_signed_transaction( uint64(1_750_000_000_001), ph, diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 0780315b1b36..8309870a3a41 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -38,10 +38,9 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b res = await client.vault_create( SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG ) - vault_tx = res[0] - assert vault_tx - eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0] + all_removals = [coin for tx in res for coin in tx.removals] + eve_coin = [item for tx in res for item in tx.additions if item not in all_removals and item.amount == 1][0] launcher_id = eve_coin.parent_coin_info vault_root = VaultRoot.from_bytes(launcher_id) await wallet_environments.process_pending_states( @@ -109,8 +108,8 @@ async def test_vault_creation( coins_to_create = 2 funding_amount = uint64(1000000000) funding_wallet = wallet_environments.environments[1].xch_wallet - async with funding_wallet.wallet_state_manager.new_action_scope() as action_scope: - for _ in range(coins_to_create): + for _ in range(coins_to_create): + async with funding_wallet.wallet_state_manager.new_action_scope(push=True, sign=True) as action_scope: await funding_wallet.generate_signed_transaction( funding_amount, p2_singleton_puzzle_hash, @@ -148,14 +147,11 @@ async def test_vault_creation( fee = uint64(100) balance_delta = 1011000099 - async with wallet.wallet_state_manager.new_action_scope(push=False, sign=False) as action_scope: + async with wallet.wallet_state_manager.new_action_scope(push=True, sign=True) as action_scope: await wallet.generate_signed_transaction( amount, recipient_ph, DEFAULT_TX_CONFIG, action_scope, primaries=primaries, fee=fee, memos=[recipient_ph] ) - await wallet_environments.environments[0].rpc_client.push_transactions( - action_scope.side_effects.transactions, sign=True - ) vault_eve_id = wallet.vault_info.coin.name() await wallet_environments.process_pending_states( diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 7832df7ee44f..22ca1d51002d 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -4642,7 +4642,6 @@ async def vault_create( action_scope: WalletActionScope, tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), - push: bool = False, ) -> EndpointResult: """ Create a new vault @@ -4668,7 +4667,6 @@ async def vault_create( timelock=timelock, fee=fee, ) - return { "transactions": None, # tx_endpoint will take care of this } diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 38310962643d..f9be4b31c1c7 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1825,13 +1825,8 @@ async def _add_coin_states( all_unconfirmed = await self.tx_store.get_all_unconfirmed() tx_records_to_confirm: List[TransactionRecord] = [] for out_tx_record in all_unconfirmed: - if out_tx_record.spend_bundle is not None: - for tx_spend in out_tx_record.spend_bundle.coin_spends: - if tx_spend.coin == coin_state.coin: - # check for a vault spend - mod, args = tx_spend.puzzle_reveal.to_program().uncurry() - if match_vault_puzzle(mod, args): - tx_records_to_confirm.append(out_tx_record) + if coin_state.coin in out_tx_record.removals: + tx_records_to_confirm.append(out_tx_record) if len(tx_records_to_confirm) > 0: for tx_record in tx_records_to_confirm: @@ -2939,3 +2934,4 @@ async def create_vault_wallet( valid_times=ConditionValidTimes(), ) ) + await self.add_interested_puzzle_hashes([launcher_coin.name()], [self.main_wallet.id()]) From 5edd81a8844b0c4e78bdc704a25d66308d16abef Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 23 Jul 2024 13:09:48 -0700 Subject: [PATCH 226/274] Fix more tests --- chia/_tests/core/util/test_keychain.py | 4 ++-- chia/_tests/wallet/vault/test_vault_wallet.py | 1 + chia/wallet/wallet_state_manager.py | 20 +++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index 66c12f4591a8..00d218bc9ea5 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -335,14 +335,14 @@ def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, PrivateK ( ( _24keyinfo.fingerprint, - G1Element(), + bytes(G1Element()), None, KeyDataSecrets(_24keyinfo.mnemonic.split(), _24keyinfo.entropy, _24keyinfo.private_key), KeyTypes.G1_ELEMENT.value, ), "public_key", ), - ((_24keyinfo.fingerprint, G1Element(), None, None, KeyTypes.G1_ELEMENT.value), "fingerprint"), + ((_24keyinfo.fingerprint, bytes(G1Element()), None, None, KeyTypes.G1_ELEMENT.value), "fingerprint"), ], ) def test_key_data_post_init( diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 8309870a3a41..e8b09e1d096f 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -358,6 +358,7 @@ async def test_vault_recovery( amount, recipient_ph, DEFAULT_TX_CONFIG, action_scope, memos=[recipient_ph] ) + # Test we can push the transaction separately await wallet_environments.environments[0].rpc_client.push_transactions( action_scope.side_effects.transactions, sign=True ) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f9be4b31c1c7..c7edeeb0f0f7 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1821,7 +1821,7 @@ async def _add_coin_states( if wallet_identifier is None: # Confirm tx records for txs which we submitted for coins which aren't in our wallet # Used for vault recovery spends - if coin_state.spent_height is not None: + if coin_state.created_height is not None and coin_state.spent_height is not None: all_unconfirmed = await self.tx_store.get_all_unconfirmed() tx_records_to_confirm: List[TransactionRecord] = [] for out_tx_record in all_unconfirmed: @@ -2396,15 +2396,15 @@ async def add_pending_transactions( additional_signing_responses != [] and additional_signing_responses is not None, ) - # Check that tx_records have additions/removals since vault txs don't have them until they're signed - for i, tx in enumerate(tx_records): - if tx.spend_bundle is not None: - if tx.additions == []: - tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) - if tx.removals == []: - assert isinstance(tx.spend_bundle, SpendBundle) - tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) - tx_records[i] = tx + # Check that tx_records have additions/removals since vault txs don't have them until they're signed + for i, tx in enumerate(tx_records): + if tx.spend_bundle is not None: + if tx.additions == []: + tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) + if tx.removals == []: + assert isinstance(tx.spend_bundle, SpendBundle) + tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) + tx_records[i] = tx if push: all_coins_names = [] From 349936d0d7145586c783f30805e14e2325dde861 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 08:43:16 -0700 Subject: [PATCH 227/274] Port vault_create to @marshal decorator --- chia/rpc/wallet_request_types.py | 20 +++++++++++++++++++- chia/rpc/wallet_rpc_api.py | 31 ++++++++++++------------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index cae18fbc4047..bdeccba9581a 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -3,9 +3,11 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Type, TypeVar +from chia_rs import G1Element + from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint32 +from chia.util.ints import uint32, uint64 from chia.util.streamable import Streamable, streamable from chia.wallet.notification_store import Notification from chia.wallet.signer_protocol import ( @@ -95,6 +97,22 @@ class TransactionEndpointResponse(Streamable): transactions: List[TransactionRecord] +@streamable +@dataclass(frozen=True) +class VaultCreate(Streamable): + secp_pk: bytes + hp_index: uint32 = uint32(0) + fee: uint64 = uint64(0) + bls_pk: Optional[G1Element] = None + timelock: Optional[uint64] = None + + +@streamable +@dataclass(frozen=True) +class VaultCreateResponse(TransactionEndpointResponse): + pass + + # TODO: The section below needs corresponding request types # TODO: The section below should be added to the API (currently only for client) @streamable diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 22ca1d51002d..7b52a3730c5d 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -31,6 +31,8 @@ GetNotificationsResponse, SubmitTransactions, SubmitTransactionsResponse, + VaultCreate, + VaultCreateResponse, ) from chia.server.outbound_message import NodeType from chia.server.ws_connection import WSChiaConnection @@ -4636,40 +4638,31 @@ async def execute_signing_instructions( # VAULT ########################################################################################## @tx_endpoint(push=False) + @marshal async def vault_create( self, - request: Dict[str, Any], + request: VaultCreate, action_scope: WalletActionScope, tx_config: TXConfig = DEFAULT_TX_CONFIG, extra_conditions: Tuple[Condition, ...] = tuple(), - ) -> EndpointResult: + ) -> VaultCreateResponse: """ Create a new vault """ - assert self.service.wallet_state_manager - secp_pk = bytes.fromhex(str(request.get("secp_pk"))) - hp_index = request.get("hp_index", 0) - hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash() - bls_str = request.get("bls_pk") - bls_pk = G1Element.from_bytes(bytes.fromhex(str(bls_str))) if bls_str else None - timelock_int = request.get("timelock") - timelock = uint64(timelock_int) if timelock_int else None - fee = uint64(request.get("fee", 0)) - genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(request.hp_index).get_tree_hash() + genesis_challenge = self.service.wallet_state_manager.constants.GENESIS_CHALLENGE await self.service.wallet_state_manager.create_vault_wallet( - secp_pk, + request.secp_pk, hidden_puzzle_hash, genesis_challenge, tx_config, action_scope, - bls_pk=bls_pk, - timelock=timelock, - fee=fee, + bls_pk=request.bls_pk, + timelock=request.timelock, + fee=request.fee, ) - return { - "transactions": None, # tx_endpoint will take care of this - } + return VaultCreateResponse([], []) # tx_endpoint will take care of filling this out async def vault_recovery(self, request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG) -> EndpointResult: """ From 6ecdea03a27677354b3d06f43ab0d2d6e8807475 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 08:53:24 -0700 Subject: [PATCH 228/274] Port vault_recovery to @marshal decorator --- chia/rpc/wallet_request_types.py | 17 +++++++++++++++++ chia/rpc/wallet_rpc_api.py | 31 +++++++++++++++++-------------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index bdeccba9581a..3672ab1301f5 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -113,6 +113,23 @@ class VaultCreateResponse(TransactionEndpointResponse): pass +@streamable +@dataclass(frozen=True) +class VaultRecovery(Streamable): + wallet_id: uint32 + secp_pk: bytes + hp_index: uint32 = uint32(0) + fee: uint64 = uint64(0) + bls_pk: Optional[G1Element] = None + timelock: Optional[uint64] = None + + +@streamable +@dataclass(frozen=True) +class VaultRecoveryResponse(TransactionEndpointResponse): + pass + + # TODO: The section below needs corresponding request types # TODO: The section below should be added to the API (currently only for client) @streamable diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 7b52a3730c5d..1bf183496d2e 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -11,7 +11,6 @@ from clvm_tools.binutils import assemble from chia.consensus.block_rewards import calculate_base_farmer_reward -from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.data_layer.data_layer_errors import LauncherCoinNotFoundError from chia.data_layer.data_layer_util import dl_verify_proof from chia.data_layer.data_layer_wallet import DataLayerWallet @@ -33,6 +32,8 @@ SubmitTransactionsResponse, VaultCreate, VaultCreateResponse, + VaultRecovery, + VaultRecoveryResponse, ) from chia.server.outbound_message import NodeType from chia.server.ws_connection import WSChiaConnection @@ -4664,21 +4665,23 @@ async def vault_create( ) return VaultCreateResponse([], []) # tx_endpoint will take care of filling this out - async def vault_recovery(self, request: Dict[str, Any], tx_config: TXConfig = DEFAULT_TX_CONFIG) -> EndpointResult: + @marshal + async def vault_recovery( + self, request: VaultRecovery, tx_config: TXConfig = DEFAULT_TX_CONFIG + ) -> VaultRecoveryResponse: """ Initiate Vault Recovery """ - wallet_id = uint32(request["wallet_id"]) - wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=Vault) - secp_pk = bytes.fromhex(str(request.get("secp_pk"))) - hp_index = request.get("hp_index", 0) - hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash() - bls_str = request.get("bls_pk") - bls_pk = G1Element.from_bytes(bytes.fromhex(str(bls_str))) if bls_str else None - timelock_int = request.get("timelock") - timelock = uint64(timelock_int) if timelock_int else None - genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE + wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Vault) + hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(request.hp_index).get_tree_hash() + genesis_challenge = self.service.wallet_state_manager.constants.GENESIS_CHALLENGE recovery_txs = await wallet.create_recovery_spends( - secp_pk, hidden_puzzle_hash, genesis_challenge, tx_config, bls_pk=bls_pk, timelock=timelock + request.secp_pk, + hidden_puzzle_hash, + genesis_challenge, + tx_config, + bls_pk=request.bls_pk, + timelock=request.timelock, ) - return {"transactions": [tx.to_json_dict_convenience(self.service.config) for tx in recovery_txs]} + # TODO: port this endpoint to tx_endpoint and action scopes + return VaultRecoveryResponse([], recovery_txs) From 1a0d8a0e912949e4f609bc4b9446a2be46452ceb Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 12:03:05 -0700 Subject: [PATCH 229/274] Port client implementations --- chia/_tests/wallet/vault/test_vault_wallet.py | 34 ++++++++----- chia/cmds/vault_funcs.py | 38 ++++++++------ chia/rpc/wallet_request_types.py | 5 ++ chia/rpc/wallet_rpc_client.py | 49 ++++--------------- 4 files changed, 59 insertions(+), 67 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index e8b09e1d096f..3a0e59face95 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -3,12 +3,14 @@ from typing import Awaitable, Callable import pytest +from chia_rs import G1Element from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from chia._tests.conftest import ConsensusMode from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework +from chia.rpc.wallet_request_types import VaultCreate, VaultRecovery from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment @@ -32,15 +34,17 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b timelock = None if with_recovery: bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] - bls_pk = bytes.fromhex(bls_pk_hex) + bls_pk = G1Element.from_bytes(bytes.fromhex(bls_pk_hex)) timelock = uint64(10) hidden_puzzle_index = uint32(0) res = await client.vault_create( - SECP_PK, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, tx_config=DEFAULT_TX_CONFIG + VaultCreate(SECP_PK, wallet_environments.tx_config, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock) ) - all_removals = [coin for tx in res for coin in tx.removals] - eve_coin = [item for tx in res for item in tx.additions if item not in all_removals and item.amount == 1][0] + all_removals = [coin for tx in res.transactions for coin in tx.removals] + eve_coin = [ + item for tx in res.transactions for item in tx.additions if item not in all_removals and item.amount == 1 + ][0] launcher_id = eve_coin.parent_coin_info vault_root = VaultRoot.from_bytes(launcher_id) await wallet_environments.process_pending_states( @@ -221,7 +225,7 @@ async def test_vault_recovery( client = wallet_environments.environments[1].rpc_client fingerprint = (await client.get_public_keys())[0] bls_pk_hex = (await client.get_private_key(fingerprint))["pk"] - bls_pk = bytes.fromhex(bls_pk_hex) + bls_pk = G1Element.from_bytes(bytes.fromhex(bls_pk_hex)) timelock = uint64(10) wallet: Vault = env.xch_wallet @@ -286,14 +290,18 @@ async def test_vault_recovery( ], ) - [initiate_tx, finish_tx] = await env.rpc_client.vault_recovery( - wallet_id=wallet.id(), - secp_pk=RECOVERY_SECP_PK, - hp_index=uint32(0), - tx_config=DEFAULT_TX_CONFIG, - bls_pk=bls_pk, - timelock=timelock, - ) + [initiate_tx, finish_tx] = ( + await env.rpc_client.vault_recovery( + VaultRecovery( + wallet_id=wallet.id(), + secp_pk=RECOVERY_SECP_PK, + hp_index=uint32(0), + tx_config=DEFAULT_TX_CONFIG, + bls_pk=bls_pk, + timelock=timelock, + ) + ) + ).transactions await wallet_environments.environments[1].rpc_client.push_transactions([initiate_tx], sign=True) diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index d98e4c9feee5..8bcae9ef5ae1 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -3,9 +3,12 @@ import json from typing import Optional, Sequence +from chia_rs import G1Element + from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client from chia.cmds.param_types import CliAmount from chia.cmds.units import units +from chia.rpc.wallet_rpc_types import VaultCreate, VaultRecovery from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 @@ -36,13 +39,15 @@ async def create_vault( assert timelock > 0 try: await wallet_client.vault_create( - bytes.fromhex(public_key), - uint32(hidden_puzzle_index), - tx_config, - bytes.fromhex(recovery_public_key) if recovery_public_key else None, - uint64(timelock) if timelock else None, - fee, - push=True, + VaultCreate( + bytes.fromhex(public_key), + uint32(hidden_puzzle_index), + tx_config, + G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, + uint64(timelock) if timelock else None, + fee, + push=True, + ) ) print("Successfully created a Vault wallet") except Exception as e: @@ -76,18 +81,21 @@ async def recover_vault( ).to_tx_config(units["chia"], config, fingerprint) try: response = await wallet_client.vault_recovery( - uint32(wallet_id), - bytes.fromhex(public_key), - uint32(hidden_puzzle_index), - tx_config, - bytes.fromhex(recovery_public_key) if recovery_public_key else None, - uint64(timelock) if timelock else None, + VaultRecovery( + uint32(wallet_id), + bytes.fromhex(public_key), + tx_config, + uint32(hidden_puzzle_index), + G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, + uint64(timelock) if timelock else None, + ) ) + # TODO: do not rely on ordering of transactions here with open(initiate_file, "w") as f: - json.dump(response[0].to_json_dict(), f, indent=4) + json.dump(response.transactions[0].to_json_dict(), f, indent=4) print(f"Initiate Recovery transaction written to: {initiate_file}") with open(finish_file, "w") as f: - json.dump(response[1].to_json_dict(), f, indent=4) + json.dump(response.transactions[1].to_json_dict(), f, indent=4) print(f"Finish Recovery transaction written to: {finish_file}") except Exception as e: print(f"Error creating recovery transactions: {e}") diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 3672ab1301f5..04da30b4c7d6 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -21,6 +21,7 @@ from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable +from chia.wallet.util.tx_config import TXConfig from chia.wallet.vc_wallet.vc_store import VCRecord _T_OfferEndpointResponse = TypeVar("_T_OfferEndpointResponse", bound="_OfferEndpointResponse") @@ -101,8 +102,10 @@ class TransactionEndpointResponse(Streamable): @dataclass(frozen=True) class VaultCreate(Streamable): secp_pk: bytes + tx_config: TXConfig hp_index: uint32 = uint32(0) fee: uint64 = uint64(0) + push: Optional[bool] = None bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None @@ -118,8 +121,10 @@ class VaultCreateResponse(TransactionEndpointResponse): class VaultRecovery(Streamable): wallet_id: uint32 secp_pk: bytes + tx_config: TXConfig hp_index: uint32 = uint32(0) fee: uint64 = uint64(0) + push: Optional[bool] = None bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 921c93c8093a..4fb1dd6bc966 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -42,6 +42,10 @@ SubmitTransactions, SubmitTransactionsResponse, TakeOfferResponse, + VaultCreate, + VaultCreateResponse, + VaultRecovery, + VaultRecoveryResponse, VCMintResponse, VCRevokeResponse, VCSpendResponse, @@ -1701,45 +1705,12 @@ async def execute_signing_instructions( async def vault_create( self, - secp_pk: bytes, - hp_index: uint32, - tx_config: TXConfig, - bls_pk: Optional[bytes] = None, - timelock: Optional[uint64] = None, - fee: uint64 = uint64(0), - push: bool = True, - ) -> List[TransactionRecord]: - response = await self.fetch( - "vault_create", - { - "secp_pk": secp_pk.hex(), - "hp_index": hp_index, - "bls_pk": bls_pk.hex() if bls_pk else None, - "timelock": timelock, - "fee": fee, - "push": push, - **tx_config.to_json_dict(), - }, - ) - return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + args: VaultCreate, + ) -> VaultCreateResponse: + return VaultCreateResponse.from_json_dict(await self.fetch("vault_create", args.to_json_dict())) async def vault_recovery( self, - wallet_id: uint32, - secp_pk: bytes, - hp_index: uint32, - tx_config: TXConfig, - bls_pk: Optional[bytes] = None, - timelock: Optional[uint64] = None, - ) -> List[TransactionRecord]: - response = await self.fetch( - "vault_recovery", - { - "wallet_id": wallet_id, - "secp_pk": secp_pk.hex(), - "hp_index": hp_index, - "bls_pk": bls_pk.hex() if bls_pk else None, - "timelock": timelock, - }, - ) - return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]] + args: VaultRecovery, + ) -> VaultRecoveryResponse: + return VaultRecoveryResponse.from_json_dict(await self.fetch("vault_recovery", args.to_json_dict())) From bf83cb663382923508c6e5a0f76c746971c42329 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 12:30:20 -0700 Subject: [PATCH 230/274] Extract out tx_endpoint commonalities --- chia/_tests/wallet/vault/test_vault_wallet.py | 8 ++++++- chia/cmds/vault_funcs.py | 24 +++++++++---------- chia/rpc/wallet_request_types.py | 22 +++++++++-------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 3a0e59face95..2972221f4211 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -38,7 +38,13 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b timelock = uint64(10) hidden_puzzle_index = uint32(0) res = await client.vault_create( - VaultCreate(SECP_PK, wallet_environments.tx_config, hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock) + VaultCreate( + secp_pk=SECP_PK, + tx_config=wallet_environments.tx_config, + hp_index=hidden_puzzle_index, + bls_pk=bls_pk, + timelock=timelock, + ) ) all_removals = [coin for tx in res.transactions for coin in tx.removals] diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index 8bcae9ef5ae1..b29eb78bb07a 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -40,12 +40,12 @@ async def create_vault( try: await wallet_client.vault_create( VaultCreate( - bytes.fromhex(public_key), - uint32(hidden_puzzle_index), - tx_config, - G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, - uint64(timelock) if timelock else None, - fee, + secp_pk=bytes.fromhex(public_key), + hp_index=uint32(hidden_puzzle_index), + tx_config=tx_config, + bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, + timelock=uint64(timelock) if timelock else None, + fee=fee, push=True, ) ) @@ -82,12 +82,12 @@ async def recover_vault( try: response = await wallet_client.vault_recovery( VaultRecovery( - uint32(wallet_id), - bytes.fromhex(public_key), - tx_config, - uint32(hidden_puzzle_index), - G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, - uint64(timelock) if timelock else None, + wallet_id=uint32(wallet_id), + secp_pk=bytes.fromhex(public_key), + tx_config=tx_config, + hp_index=uint32(hidden_puzzle_index), + bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, + timelock=uint64(timelock) if timelock else None, ) ) # TODO: do not rely on ordering of transactions here diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 04da30b4c7d6..3391129dfc70 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -91,6 +91,14 @@ class ExecuteSigningInstructionsResponse(Streamable): signing_responses: List[SigningResponse] +@streamable +@dataclass(frozen=True, kw_only=True) +class TransactionEndpointRequest(Streamable): + tx_config: TXConfig + fee: uint64 = uint64(0) + push: Optional[bool] = None + + @streamable @dataclass(frozen=True) class TransactionEndpointResponse(Streamable): @@ -99,13 +107,10 @@ class TransactionEndpointResponse(Streamable): @streamable -@dataclass(frozen=True) -class VaultCreate(Streamable): +@dataclass(frozen=True, kw_only=True) +class VaultCreate(TransactionEndpointRequest): secp_pk: bytes - tx_config: TXConfig hp_index: uint32 = uint32(0) - fee: uint64 = uint64(0) - push: Optional[bool] = None bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None @@ -117,14 +122,11 @@ class VaultCreateResponse(TransactionEndpointResponse): @streamable -@dataclass(frozen=True) -class VaultRecovery(Streamable): +@dataclass(frozen=True, kw_only=True) +class VaultRecovery(TransactionEndpointRequest): wallet_id: uint32 secp_pk: bytes - tx_config: TXConfig hp_index: uint32 = uint32(0) - fee: uint64 = uint64(0) - push: Optional[bool] = None bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None From 1211dff6fe71803f0bce28584494518609d7f89d Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 13:25:54 -0700 Subject: [PATCH 231/274] Raise on inproper fee value --- chia/rpc/wallet_rpc_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 1bf183496d2e..8e4a3eca5377 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -4672,6 +4672,8 @@ async def vault_recovery( """ Initiate Vault Recovery """ + if request.fee != 0: + raise ValueError("Recovery endpoint cannot add fees because it assumes your vault is currently inacessible") wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Vault) hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(request.hp_index).get_tree_hash() genesis_challenge = self.service.wallet_state_manager.constants.GENESIS_CHALLENGE From 0fb2c4628744c0b3b642af79205c2d46eb535397 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 13:20:33 -0700 Subject: [PATCH 232/274] Move TXConfig outside of request type --- chia/_tests/wallet/vault/test_vault_wallet.py | 9 +++++---- chia/cmds/vault_funcs.py | 8 ++++---- chia/rpc/wallet_request_types.py | 2 -- chia/rpc/wallet_rpc_client.py | 10 ++++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 2972221f4211..84d078b56e39 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -40,11 +40,12 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b res = await client.vault_create( VaultCreate( secp_pk=SECP_PK, - tx_config=wallet_environments.tx_config, hp_index=hidden_puzzle_index, bls_pk=bls_pk, timelock=timelock, - ) + push=True, + ), + tx_config=wallet_environments.tx_config, ) all_removals = [coin for tx in res.transactions for coin in tx.removals] @@ -302,10 +303,10 @@ async def test_vault_recovery( wallet_id=wallet.id(), secp_pk=RECOVERY_SECP_PK, hp_index=uint32(0), - tx_config=DEFAULT_TX_CONFIG, bls_pk=bls_pk, timelock=timelock, - ) + ), + tx_config=wallet_environments.tx_config, ) ).transactions diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index b29eb78bb07a..998c87241dd3 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -42,12 +42,12 @@ async def create_vault( VaultCreate( secp_pk=bytes.fromhex(public_key), hp_index=uint32(hidden_puzzle_index), - tx_config=tx_config, bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, timelock=uint64(timelock) if timelock else None, fee=fee, push=True, - ) + ), + tx_config=tx_config, ) print("Successfully created a Vault wallet") except Exception as e: @@ -84,11 +84,11 @@ async def recover_vault( VaultRecovery( wallet_id=uint32(wallet_id), secp_pk=bytes.fromhex(public_key), - tx_config=tx_config, hp_index=uint32(hidden_puzzle_index), bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, timelock=uint64(timelock) if timelock else None, - ) + ), + tx_config=tx_config, ) # TODO: do not rely on ordering of transactions here with open(initiate_file, "w") as f: diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 3391129dfc70..c6f02efb7637 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -21,7 +21,6 @@ from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable -from chia.wallet.util.tx_config import TXConfig from chia.wallet.vc_wallet.vc_store import VCRecord _T_OfferEndpointResponse = TypeVar("_T_OfferEndpointResponse", bound="_OfferEndpointResponse") @@ -94,7 +93,6 @@ class ExecuteSigningInstructionsResponse(Streamable): @streamable @dataclass(frozen=True, kw_only=True) class TransactionEndpointRequest(Streamable): - tx_config: TXConfig fee: uint64 = uint64(0) push: Optional[bool] = None diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 4fb1dd6bc966..15e2c15f99cf 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1706,11 +1706,17 @@ async def execute_signing_instructions( async def vault_create( self, args: VaultCreate, + tx_config: TXConfig, ) -> VaultCreateResponse: - return VaultCreateResponse.from_json_dict(await self.fetch("vault_create", args.to_json_dict())) + return VaultCreateResponse.from_json_dict( + await self.fetch("vault_create", {**args.to_json_dict(), **tx_config.to_json_dict()}) + ) async def vault_recovery( self, args: VaultRecovery, + tx_config: TXConfig, ) -> VaultRecoveryResponse: - return VaultRecoveryResponse.from_json_dict(await self.fetch("vault_recovery", args.to_json_dict())) + return VaultRecoveryResponse.from_json_dict( + await self.fetch("vault_recovery", {**args.to_json_dict(), **tx_config.to_json_dict()}) + ) From 5c39a6c800dd9fd7f14b53ae93d3a03d71ac69af Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 24 Jul 2024 14:48:51 -0700 Subject: [PATCH 233/274] Special case kw_only --- chia/rpc/wallet_request_types.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index c6f02efb7637..d69d7b762cc7 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -1,9 +1,11 @@ from __future__ import annotations +import sys from dataclasses import dataclass from typing import Any, Dict, List, Optional, Type, TypeVar from chia_rs import G1Element +from typing_extensions import dataclass_transform from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle @@ -24,6 +26,15 @@ from chia.wallet.vc_wallet.vc_store import VCRecord _T_OfferEndpointResponse = TypeVar("_T_OfferEndpointResponse", bound="_OfferEndpointResponse") +_T_KW_Dataclass = TypeVar("_T_KW_Dataclass") + + +@dataclass_transform(frozen_default=True, kw_only_default=True) +def kw_only_dataclass(cls: Type[Any]) -> Type[Any]: + if sys.version_info < (3, 10): + return dataclass(frozen=True)(cls) + else: + return dataclass(frozen=True, kw_only=True)(cls) @streamable @@ -91,7 +102,7 @@ class ExecuteSigningInstructionsResponse(Streamable): @streamable -@dataclass(frozen=True, kw_only=True) +@kw_only_dataclass class TransactionEndpointRequest(Streamable): fee: uint64 = uint64(0) push: Optional[bool] = None @@ -105,7 +116,7 @@ class TransactionEndpointResponse(Streamable): @streamable -@dataclass(frozen=True, kw_only=True) +@kw_only_dataclass class VaultCreate(TransactionEndpointRequest): secp_pk: bytes hp_index: uint32 = uint32(0) @@ -120,7 +131,7 @@ class VaultCreateResponse(TransactionEndpointResponse): @streamable -@dataclass(frozen=True, kw_only=True) +@kw_only_dataclass class VaultRecovery(TransactionEndpointRequest): wallet_id: uint32 secp_pk: bytes From e43817c10e7f88a34d09af7f7c913da6cad12467 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jul 2024 09:47:12 -0700 Subject: [PATCH 234/274] Fix cmd tests --- chia/_tests/cmds/wallet/test_vault.py | 91 +++++++-------------------- chia/cmds/vault_funcs.py | 2 +- 2 files changed, 22 insertions(+), 71 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py index 87c56697bf0f..fbf2fc6586b0 100644 --- a/chia/_tests/cmds/wallet/test_vault.py +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -1,17 +1,13 @@ from __future__ import annotations from pathlib import Path -from typing import List, Optional, Tuple +from typing import Tuple -from chia_rs import Coin, G1Element, G2Element +from chia_rs import G1Element from chia._tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert -from chia._tests.cmds.wallet.test_consts import FINGERPRINT_ARG, WALLET_ID_ARG, get_bytes32 -from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint8, uint32, uint64 -from chia.wallet.conditions import ConditionValidTimes -from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.util.transaction_type import TransactionType +from chia._tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, STD_UTX, WALLET_ID_ARG +from chia.rpc.wallet_request_types import VaultCreate, VaultCreateResponse, VaultRecovery, VaultRecoveryResponse from chia.wallet.util.tx_config import TXConfig @@ -22,39 +18,15 @@ def test_vault_create(capsys: object, get_test_cli_clients: Tuple[TestRpcClients class CreateVaultRpcClient(TestWalletRpcClient): async def vault_create( self, - secp_pk: bytes, - hp_index: uint32, + args: VaultCreate, tx_config: TXConfig, - bls_pk: Optional[bytes] = None, - timelock: Optional[uint64] = None, - fee: uint64 = uint64(0), - push: bool = True, - ) -> List[TransactionRecord]: - tx_rec = TransactionRecord( - confirmed_at_height=uint32(1), - created_at_time=uint64(1234), - to_puzzle_hash=get_bytes32(1), - amount=uint64(12345678), - fee_amount=uint64(1234567), - confirmed=False, - sent=uint32(0), - spend_bundle=SpendBundle([], G2Element()), - additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], - removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], - wallet_id=uint32(1), - sent_to=[("aaaaa", uint8(1), None)], - trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), - name=get_bytes32(2), - memos=[(get_bytes32(3), [bytes([4] * 32)])], - valid_times=ConditionValidTimes(), - ) - return [tx_rec] + ) -> VaultCreateResponse: + return VaultCreateResponse([STD_UTX], [STD_TX]) inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client - pk = get_bytes32(0).hex() - recovery_pk = get_bytes32(1).hex() + pk = bytes(G1Element()).hex() + recovery_pk = bytes(G1Element()).hex() timelock = "100" hidden_puzzle_index = "10" fee = "0.1" @@ -77,45 +49,22 @@ async def vault_create( run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) -def test_vault_recovery(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: +def test_vault_recovery(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path], tmp_path: Path) -> None: test_rpc_clients, root_dir = get_test_cli_clients # set RPC clients class CreateVaultRpcClient(TestWalletRpcClient): async def vault_recovery( self, - wallet_id: uint32, - secp_pk: bytes, - hp_index: uint32, + args: VaultRecovery, tx_config: TXConfig, - bls_pk: Optional[G1Element] = None, - timelock: Optional[uint64] = None, - ) -> List[TransactionRecord]: - tx_rec = TransactionRecord( - confirmed_at_height=uint32(1), - created_at_time=uint64(1234), - to_puzzle_hash=get_bytes32(1), - amount=uint64(12345678), - fee_amount=uint64(1234567), - confirmed=False, - sent=uint32(0), - spend_bundle=SpendBundle([], G2Element()), - additions=[Coin(get_bytes32(1), get_bytes32(2), uint64(12345678))], - removals=[Coin(get_bytes32(2), get_bytes32(4), uint64(12345678))], - wallet_id=uint32(1), - sent_to=[("aaaaa", uint8(1), None)], - trade_id=None, - type=uint32(TransactionType.OUTGOING_TX.value), - name=get_bytes32(2), - memos=[(get_bytes32(3), [bytes([4] * 32)])], - valid_times=ConditionValidTimes(), - ) - return [tx_rec, tx_rec] + ) -> VaultRecoveryResponse: + return VaultRecoveryResponse([STD_UTX, STD_UTX], [STD_TX, STD_TX]) inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client - pk = get_bytes32(0).hex() - recovery_pk = get_bytes32(1).hex() + pk = bytes(G1Element()).hex() + recovery_pk = bytes(G1Element()).hex() timelock = "100" hidden_puzzle_index = "10" command_args = [ @@ -130,12 +79,14 @@ async def vault_recovery( "-i", hidden_puzzle_index, "-ri", - "recovery_init.json", + str(tmp_path / "recovery_init.json"), "-rf", - "recovery_finish.json", + str(tmp_path / "recovery_finish.json"), ] assert_list = [ - "Initiate Recovery transaction written to: recovery_init.json", - "Finish Recovery transaction written to: recovery_finish.json", + "Initiate Recovery transaction written to:", + "recovery_init.json", + "Finish Recovery transaction written to:", + "recovery_finish.json", ] run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG, WALLET_ID_ARG], assert_list) diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index 998c87241dd3..21b172364102 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -8,7 +8,7 @@ from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client from chia.cmds.param_types import CliAmount from chia.cmds.units import units -from chia.rpc.wallet_rpc_types import VaultCreate, VaultRecovery +from chia.rpc.wallet_request_types import VaultCreate, VaultRecovery from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 From 8204e28b4245eac1ca1d4ad85e857b46393a5088 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jul 2024 10:15:22 -0700 Subject: [PATCH 235/274] Fix < 3.10 kw_only compatibility --- chia/rpc/wallet_request_types.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index d69d7b762cc7..2a0967da20a1 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from dataclasses import dataclass +from dataclasses import MISSING, dataclass, field from typing import Any, Dict, List, Optional, Type, TypeVar from chia_rs import G1Element @@ -101,6 +101,9 @@ class ExecuteSigningInstructionsResponse(Streamable): signing_responses: List[SigningResponse] +# When inheriting from this class you must set any non default arguments with: +# field(default=MISSING) # type: ignore[assignment] +# (this is for < 3.10 compatibility) @streamable @kw_only_dataclass class TransactionEndpointRequest(Streamable): @@ -118,7 +121,7 @@ class TransactionEndpointResponse(Streamable): @streamable @kw_only_dataclass class VaultCreate(TransactionEndpointRequest): - secp_pk: bytes + secp_pk: bytes = field(default=MISSING) # type: ignore[assignment] hp_index: uint32 = uint32(0) bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None @@ -133,8 +136,8 @@ class VaultCreateResponse(TransactionEndpointResponse): @streamable @kw_only_dataclass class VaultRecovery(TransactionEndpointRequest): - wallet_id: uint32 - secp_pk: bytes + wallet_id: uint32 = field(default=MISSING) # type: ignore[assignment] + secp_pk: bytes = field(default=MISSING) # type: ignore[assignment] hp_index: uint32 = uint32(0) bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None From ee18f827300c985113b4925dab37463b13dc6945 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jul 2024 10:25:13 -0700 Subject: [PATCH 236/274] Fix < 3.10 kw_only compatibility --- chia/rpc/wallet_request_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 2a0967da20a1..f278acf1d525 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -121,7 +121,7 @@ class TransactionEndpointResponse(Streamable): @streamable @kw_only_dataclass class VaultCreate(TransactionEndpointRequest): - secp_pk: bytes = field(default=MISSING) # type: ignore[assignment] + secp_pk: bytes = field(default=MISSING, default_factory=MISSING) # type: ignore[call-overload] hp_index: uint32 = uint32(0) bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None @@ -136,8 +136,8 @@ class VaultCreateResponse(TransactionEndpointResponse): @streamable @kw_only_dataclass class VaultRecovery(TransactionEndpointRequest): - wallet_id: uint32 = field(default=MISSING) # type: ignore[assignment] - secp_pk: bytes = field(default=MISSING) # type: ignore[assignment] + wallet_id: uint32 = field(default=MISSING, default_factory=MISSING) # type: ignore[call-overload] + secp_pk: bytes = field(default=MISSING, default_factory=MISSING) # type: ignore[call-overload] hp_index: uint32 = uint32(0) bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None From b07359f512ec522fef1e4e22773f022c6a3e60e9 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jul 2024 10:32:45 -0700 Subject: [PATCH 237/274] Fix < 3.10 kw_only compatibility --- chia/rpc/wallet_request_types.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index f278acf1d525..2c925ed01e05 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from dataclasses import MISSING, dataclass, field +from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Type, TypeVar from chia_rs import G1Element @@ -37,6 +37,10 @@ def kw_only_dataclass(cls: Type[Any]) -> Type[Any]: return dataclass(frozen=True, kw_only=True)(cls) +def default_raise() -> Any: # pragma: no cover + raise RuntimeError("This should be impossible to hit and is just for < 3.10 compatibility") + + @streamable @dataclass(frozen=True) class GetNotifications(Streamable): @@ -102,7 +106,7 @@ class ExecuteSigningInstructionsResponse(Streamable): # When inheriting from this class you must set any non default arguments with: -# field(default=MISSING) # type: ignore[assignment] +# field(default_factory=default_raise) # (this is for < 3.10 compatibility) @streamable @kw_only_dataclass @@ -121,7 +125,7 @@ class TransactionEndpointResponse(Streamable): @streamable @kw_only_dataclass class VaultCreate(TransactionEndpointRequest): - secp_pk: bytes = field(default=MISSING, default_factory=MISSING) # type: ignore[call-overload] + secp_pk: bytes = field(default_factory=default_raise) hp_index: uint32 = uint32(0) bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None @@ -136,8 +140,8 @@ class VaultCreateResponse(TransactionEndpointResponse): @streamable @kw_only_dataclass class VaultRecovery(TransactionEndpointRequest): - wallet_id: uint32 = field(default=MISSING, default_factory=MISSING) # type: ignore[call-overload] - secp_pk: bytes = field(default=MISSING, default_factory=MISSING) # type: ignore[call-overload] + wallet_id: uint32 = field(default_factory=default_raise) + secp_pk: bytes = field(default_factory=default_raise) hp_index: uint32 = uint32(0) bls_pk: Optional[G1Element] = None timelock: Optional[uint64] = None From 7ce5a25e811d67aa23294200961228637255f439 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jul 2024 10:43:26 -0700 Subject: [PATCH 238/274] pylint --- chia/rpc/wallet_request_types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 2c925ed01e05..c7bf07a87c69 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -1,3 +1,5 @@ +# pylint: disable=invalid-field-call + from __future__ import annotations import sys From 0862967727f386e33742b2c0abb925835dbc071b Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jul 2024 14:54:14 -0700 Subject: [PATCH 239/274] pragma: no cover --- chia/rpc/wallet_request_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index c7bf07a87c69..0454b3033339 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -34,7 +34,7 @@ @dataclass_transform(frozen_default=True, kw_only_default=True) def kw_only_dataclass(cls: Type[Any]) -> Type[Any]: if sys.version_info < (3, 10): - return dataclass(frozen=True)(cls) + return dataclass(frozen=True)(cls) # pragma: no cover else: return dataclass(frozen=True, kw_only=True)(cls) From 0d705b2310797c3c3f71707ce837a51b64778eaf Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 30 Jul 2024 08:05:23 -0700 Subject: [PATCH 240/274] Add @tx_out_cmd to vault_create and dedup transaction output code --- chia/_tests/cmds/wallet/test_vault.py | 3 +-- chia/cmds/cmds_util.py | 10 +++++++--- chia/cmds/vault.py | 11 ++++++++--- chia/cmds/vault_funcs.py | 23 +++++++++++------------ 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py index fbf2fc6586b0..a8c49f5105e1 100644 --- a/chia/_tests/cmds/wallet/test_vault.py +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -84,9 +84,8 @@ async def vault_recovery( str(tmp_path / "recovery_finish.json"), ] assert_list = [ - "Initiate Recovery transaction written to:", + "Writing transactions to file ", "recovery_init.json", - "Finish Recovery transaction written to:", "recovery_finish.json", ] run_cli_command_and_assert(capsys, root_dir, command_args + [FINGERPRINT_ARG, WALLET_ID_ARG], assert_list) diff --git a/chia/cmds/cmds_util.py b/chia/cmds/cmds_util.py index 2c82fb9a3dcc..1a3f8b2011b0 100644 --- a/chia/cmds/cmds_util.py +++ b/chia/cmds/cmds_util.py @@ -335,13 +335,17 @@ class TransactionBundle(Streamable): txs: List[TransactionRecord] +def write_transactions_to_file(txs: List[TransactionRecord], transaction_file: str) -> None: + print(f"Writing transactions to file {transaction_file}:") + with open(Path(transaction_file), "wb") as file: + file.write(bytes(TransactionBundle(txs))) + + def tx_out_cmd(func: Callable[..., List[TransactionRecord]]) -> Callable[..., None]: def original_cmd(transaction_file: Optional[str] = None, **kwargs: Any) -> None: txs: List[TransactionRecord] = func(**kwargs) if transaction_file is not None: - print(f"Writing transactions to file {transaction_file}:") - with open(Path(transaction_file), "wb") as file: - file.write(bytes(TransactionBundle(txs))) + write_transactions_to_file(txs, transaction_file) return click.option( "--push/--no-push", help="Push the transaction to the network", type=bool, is_flag=True, default=True diff --git a/chia/cmds/vault.py b/chia/cmds/vault.py index 86cc0434abd5..7082b702a2f4 100644 --- a/chia/cmds/vault.py +++ b/chia/cmds/vault.py @@ -1,14 +1,16 @@ from __future__ import annotations import asyncio -from typing import Optional, Sequence +from typing import List, Optional, Sequence import click from chia.cmds import options +from chia.cmds.cmds_util import tx_out_cmd from chia.cmds.param_types import AmountParamType, Bytes32ParamType, CliAmount, cli_amount_none from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint64 +from chia.wallet.transaction_record import TransactionRecord @click.group("vault", help="Manage your vault") @@ -88,6 +90,7 @@ def vault_cmd(ctx: click.Context) -> None: is_flag=True, default=False, ) +@tx_out_cmd def vault_create_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -101,10 +104,11 @@ def vault_create_cmd( max_coin_amount: CliAmount, coins_to_exclude: Sequence[bytes32], reuse: bool, -) -> None: + push: bool, +) -> List[TransactionRecord]: from .vault_funcs import create_vault - asyncio.run( + return asyncio.run( create_vault( wallet_rpc_port, fingerprint, @@ -118,6 +122,7 @@ def vault_create_cmd( max_coin_amount=max_coin_amount, excluded_coin_ids=coins_to_exclude, reuse_puzhash=True if reuse else None, + push=push, ) ) diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index 21b172364102..bb0a868e5d51 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -1,16 +1,16 @@ from __future__ import annotations -import json -from typing import Optional, Sequence +from typing import List, Optional, Sequence from chia_rs import G1Element -from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client +from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client, write_transactions_to_file from chia.cmds.param_types import CliAmount from chia.cmds.units import units from chia.rpc.wallet_request_types import VaultCreate, VaultRecovery from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 +from chia.wallet.transaction_record import TransactionRecord async def create_vault( @@ -26,7 +26,8 @@ async def create_vault( max_coin_amount: CliAmount, excluded_coin_ids: Sequence[bytes32], reuse_puzhash: Optional[bool], -) -> None: + push: bool, +) -> List[TransactionRecord]: async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config): assert hidden_puzzle_index >= 0 tx_config = CMDTXConfigLoader( @@ -38,20 +39,22 @@ async def create_vault( if timelock is not None: assert timelock > 0 try: - await wallet_client.vault_create( + res = await wallet_client.vault_create( VaultCreate( secp_pk=bytes.fromhex(public_key), hp_index=uint32(hidden_puzzle_index), bls_pk=G1Element.from_bytes(bytes.fromhex(recovery_public_key)) if recovery_public_key else None, timelock=uint64(timelock) if timelock else None, fee=fee, - push=True, + push=push, ), tx_config=tx_config, ) print("Successfully created a Vault wallet") + return res.transactions except Exception as e: print(f"Failed to create a new Vault: {e}") + return [] async def recover_vault( @@ -91,11 +94,7 @@ async def recover_vault( tx_config=tx_config, ) # TODO: do not rely on ordering of transactions here - with open(initiate_file, "w") as f: - json.dump(response.transactions[0].to_json_dict(), f, indent=4) - print(f"Initiate Recovery transaction written to: {initiate_file}") - with open(finish_file, "w") as f: - json.dump(response.transactions[1].to_json_dict(), f, indent=4) - print(f"Finish Recovery transaction written to: {finish_file}") + write_transactions_to_file(response.transactions[0:1], initiate_file) + write_transactions_to_file(response.transactions[1:2], finish_file) except Exception as e: print(f"Error creating recovery transactions: {e}") From d993b86929de6d77422c5e22fc3e7ed4cfde60f5 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley <5695735+geoffwalmsley@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:03:25 +0100 Subject: [PATCH 241/274] convert make_solution to async (#18368) * convert make_solution to async * add optional action_scope to make_solution signature --- chia/_tests/wallet/cat_wallet/test_cat_wallet.py | 2 +- chia/_tests/wallet/test_main_wallet_protocol.py | 5 +++-- chia/_tests/wallet/vault/test_vault_wallet.py | 12 +----------- chia/data_layer/data_layer_wallet.py | 4 ++-- chia/wallet/cat_wallet/cat_wallet.py | 8 ++++---- chia/wallet/cat_wallet/dao_cat_wallet.py | 6 +++--- chia/wallet/did_wallet/did_wallet.py | 10 +++++----- chia/wallet/nft_wallet/nft_wallet.py | 12 ++++++------ chia/wallet/puzzles/tails.py | 12 +++++++++--- chia/wallet/vault/vault_wallet.py | 3 ++- chia/wallet/vc_wallet/cr_cat_wallet.py | 8 ++++---- chia/wallet/vc_wallet/vc_wallet.py | 2 +- chia/wallet/wallet.py | 7 ++++--- chia/wallet/wallet_protocol.py | 3 ++- chia/wallet/wallet_state_manager.py | 2 +- 15 files changed, 48 insertions(+), 48 deletions(-) diff --git a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py index 6c1e435f56e2..62026ff814a8 100644 --- a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py +++ b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py @@ -1271,7 +1271,7 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non coin=new_coin, limitations_program_hash=ACS_TAIL_HASH, inner_puzzle=await cat_wallet.inner_puzzle_for_cat_puzhash(new_coin.puzzle_hash), - inner_solution=wallet.make_solution( + inner_solution=await wallet.make_solution( primaries=[Payment(wallet_ph, uint64(tx_amount), [wallet_ph])], conditions=( UnknownCondition( diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py index 2117db3ec259..09190f4aadb5 100644 --- a/chia/_tests/wallet/test_main_wallet_protocol.py +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -96,7 +96,7 @@ async def generate_signed_transaction( make_spend( coin, ACS, - self.make_solution(condition_list, extra_conditions, fee) if i == 0 else Program.to([]), + (await self.make_solution(condition_list, extra_conditions, fee) if i == 0 else Program.to([])), ) for i, coin in enumerate(coins) ], @@ -164,11 +164,12 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: # pragma async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: # pragma: no cover return None - def make_solution( + async def make_solution( self, primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + action_scope: Optional[WalletActionScope] = None, **kwargs: Any, ) -> Program: condition_list: List[Condition] = [CreateCoin(p.puzzle_hash, p.amount, p.memos) for p in primaries] diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 84d078b56e39..ae7d6a41b0eb 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -14,7 +14,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.wallet.payment import Payment -from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG +from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.vault.vault_info import VaultInfo from chia.wallet.vault.vault_root import VaultRoot from chia.wallet.vault.vault_wallet import Vault @@ -196,16 +196,6 @@ async def test_vault_creation( assert vault_info == wallet.vault_info assert vault_info.recovery_info == wallet.vault_info.recovery_info - # test make_solution - coin = (await wallet.select_coins(uint64(100), DEFAULT_COIN_SELECTION_CONFIG)).pop() - wallet.make_solution(primaries, coin_id=coin.name()) - with pytest.raises(ValueError): - wallet.make_solution(primaries) - - # test match_hinted_coin - matched = await wallet.match_hinted_coin(wallet.vault_info.coin, wallet.vault_info.inner_puzzle_hash) - assert matched - @pytest.mark.parametrize( "wallet_environments", diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index 822409c9ad88..37382a00f24b 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -542,7 +542,7 @@ async def create_update_state_spend( ], ), ) - inner_sol: Program = self.standard_wallet.make_solution( + inner_sol: Program = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, CreateCoinAnnouncement(b"$")) if fee > 0 else extra_conditions, ) @@ -753,7 +753,7 @@ async def delete_mirror( parent_inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(inner_puzzle_derivation.pubkey) new_puzhash: bytes32 = await self.standard_wallet.get_puzzle_hash(new=not tx_config.reuse_puzhash) excess_fee: int = fee - mirror_coin.amount - inner_sol: Program = self.standard_wallet.make_solution( + inner_sol: Program = await self.standard_wallet.make_solution( primaries=[Payment(new_puzhash, uint64(mirror_coin.amount - fee))] if excess_fee < 0 else [], conditions=(*extra_conditions, CreateCoinAnnouncement(b"$")) if excess_fee > 0 else extra_conditions, ) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 0a46f040995d..e1953972194c 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -711,7 +711,7 @@ async def generate_unsigned_spendbundle( action_scope, extra_conditions=(announcement.corresponding_assertion(),), ) - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, announcement), ) @@ -723,7 +723,7 @@ async def generate_unsigned_spendbundle( action_scope, ) assert xch_announcement is not None - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, xch_announcement, announcement), ) @@ -731,12 +731,12 @@ async def generate_unsigned_spendbundle( # TODO: what about when they are equal? raise Exception("Equality not handled") else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, announcement), ) else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=[], conditions=(announcement.corresponding_assertion(),) ) inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash) diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index a9c668337b8f..4253a644179d 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -298,7 +298,7 @@ async def create_vote_spend( ) ] message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash() - inner_solution = self.standard_wallet.make_solution( + inner_solution = await self.standard_wallet.make_solution( primaries=primaries, conditions=(CreatePuzzleAnnouncement(message),), ) @@ -321,7 +321,7 @@ async def create_vote_spend( ) ) message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash() - inner_solution = self.standard_wallet.make_solution( + inner_solution = await self.standard_wallet.make_solution( primaries=primaries, conditions=(CreatePuzzleAnnouncement(message),), ) @@ -423,7 +423,7 @@ async def exit_vote_state( ), ] total_amt += coin.amount - inner_solution = self.standard_wallet.make_solution( + inner_solution = await self.standard_wallet.make_solution( primaries=primaries, ) # Create the solution using only the values needed for exiting the lockup mode (my_id = 0) diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index bad8875bbeef..8c3c88d14ed8 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -578,7 +578,7 @@ async def create_update_spend( assert uncurried is not None p2_puzzle = uncurried[0] # innerpuz solution is (mode, p2_solution) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[ Payment( puzzle_hash=new_inner_puzzle.get_tree_hash(), @@ -694,7 +694,7 @@ async def transfer_did( launcher_id=self.did_info.origin_coin.name(), metadata=did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), ) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(new_did_puzhash, uint64(coin.amount), [new_puzhash])], conditions=(*extra_conditions, CreateCoinAnnouncement(coin.name())), ) @@ -783,7 +783,7 @@ async def create_message_spend( launcher_id=self.did_info.origin_coin.name(), metadata=did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), ) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(puzzle_hash=new_innerpuzzle_hash, amount=uint64(coin.amount), memos=[p2_ph])], conditions=extra_conditions, ) @@ -918,7 +918,7 @@ async def create_attestment( assert uncurried is not None p2_puzzle = uncurried[0] # innerpuz solution is (mode, p2_solution) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[ Payment(innerpuz.get_tree_hash(), uint64(coin.amount), [p2_puzzle.get_tree_hash()]), Payment(innermessage, uint64(0)), @@ -1313,7 +1313,7 @@ async def generate_eve_spend( assert uncurried is not None p2_puzzle = uncurried[0] # innerpuz solution is (mode p2_solution) - p2_solution = self.standard_wallet.make_solution( + p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(innerpuz.get_tree_hash(), uint64(coin.amount), [p2_puzzle.get_tree_hash()])], conditions=extra_conditions, ) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 82fe0574f250..311a1df37b21 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -711,7 +711,7 @@ async def generate_unsigned_spendbundle( ), ) - innersol: Program = self.standard_wallet.make_solution( + innersol: Program = await self.standard_wallet.make_solution( primaries=payments, conditions=(*extra_conditions, CreateCoinAnnouncement(coin_name)) if fee > 0 else extra_conditions, ) @@ -1394,7 +1394,7 @@ async def mint_from_did( if len(xch_coins) > 1: xch_extra_conditions += (CreateCoinAnnouncement(message),) - solution: Program = self.standard_wallet.make_solution( + solution: Program = await self.standard_wallet.make_solution( primaries=[xch_payment], fee=fee, conditions=xch_extra_conditions, @@ -1407,14 +1407,14 @@ async def mint_from_did( for xch_coin in xch_coins_iter: puzzle = await self.standard_wallet.puzzle_for_puzzle_hash(xch_coin.puzzle_hash) - solution = self.standard_wallet.make_solution( + solution = await self.standard_wallet.make_solution( primaries=[], conditions=(AssertCoinAnnouncement(primary_announcement_hash),) ) xch_spends.append(make_spend(xch_coin, puzzle, solution)) xch_spend = SpendBundle(xch_spends, G2Element()) # Create the DID spend using the announcements collected when making the intermediate launcher coins - did_p2_solution = self.standard_wallet.make_solution( + did_p2_solution = await self.standard_wallet.make_solution( primaries=primaries, conditions=( *extra_conditions, @@ -1652,7 +1652,7 @@ async def mint_from_xch( extra_conditions += tuple(AssertCoinAnnouncement(ann) for ann in coin_announcements) extra_conditions += tuple(AssertPuzzleAnnouncement(ann) for ann in puzzle_assertions) - solution: Program = self.standard_wallet.make_solution( + solution: Program = await self.standard_wallet.make_solution( primaries=[xch_payment] + primaries, fee=fee, conditions=extra_conditions, @@ -1660,7 +1660,7 @@ async def mint_from_xch( primary_announcement = AssertCoinAnnouncement(asserted_id=xch_coin.name(), asserted_msg=message) first = False else: - solution = self.standard_wallet.make_solution(primaries=[], conditions=(primary_announcement,)) + solution = await self.standard_wallet.make_solution(primaries=[], conditions=(primary_announcement,)) xch_spends.append(make_spend(xch_coin, puzzle, solution)) # Collect up all the coin spends and sign them diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py index a8519ad87719..21a2a3580a28 100644 --- a/chia/wallet/puzzles/tails.py +++ b/chia/wallet/puzzles/tails.py @@ -120,7 +120,11 @@ async def generate_issuance_bundle( inner_tree_hash = cat_inner.get_tree_hash() inner_solution = wallet.standard_wallet.add_condition_to_solution( Program.to([51, 0, -113, tail, []]), - wallet.standard_wallet.make_solution(primaries=[Payment(inner_tree_hash, amount, [inner_tree_hash])]), + ( + await wallet.standard_wallet.make_solution( + primaries=[Payment(inner_tree_hash, amount, [inner_tree_hash])] + ) + ), ) eve_spend = unsigned_spend_bundle_for_spendable_cats( CAT_MOD, @@ -301,8 +305,10 @@ async def generate_issuance_bundle( payment = Payment(cat_inner.get_tree_hash(), amount) inner_solution = wallet.standard_wallet.add_condition_to_solution( Program.to([51, 0, -113, tail, []]), - wallet.standard_wallet.make_solution( - primaries=[payment], + ( + await wallet.standard_wallet.make_solution( + primaries=[payment], + ) ), ) eve_spend = unsigned_spend_bundle_for_spendable_cats( diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 28bc6802b06d..3c081c45013c 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -409,11 +409,12 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: return None - def make_solution( + async def make_solution( self, primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + action_scope: Optional[WalletActionScope] = None, **kwargs: Any, ) -> Program: assert fee >= 0 diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index 715eb17cb27a..efbd0500d642 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -517,7 +517,7 @@ async def _generate_unsigned_spendbundle( action_scope, extra_conditions=(announcement.corresponding_assertion(),), ) - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, announcement), ) @@ -529,7 +529,7 @@ async def _generate_unsigned_spendbundle( action_scope, ) assert xch_announcement is not None - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, xch_announcement, announcement), ) @@ -537,12 +537,12 @@ async def _generate_unsigned_spendbundle( # TODO: what about when they are equal? raise Exception("Equality not handled") else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=primaries, conditions=(*extra_conditions, announcement), ) else: - innersol = self.standard_wallet.make_solution( + innersol = await self.standard_wallet.make_solution( primaries=[], conditions=(announcement.corresponding_assertion(),), ) diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index 9053697bb933..716c85176acc 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -299,7 +299,7 @@ async def generate_signed_transaction( else: magic_condition = vc_record.vc.standard_magic_condition() extra_conditions = (*extra_conditions, UnknownCondition.from_program(magic_condition)) - innersol: Program = self.standard_wallet.make_solution( + innersol: Program = await self.standard_wallet.make_solution( primaries=primaries, conditions=extra_conditions, ) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 4e1e818132be..32fcf48e44f0 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -214,11 +214,12 @@ 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( + async def make_solution( self, primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + action_scope: Optional[WalletActionScope] = None, **kwargs: Any, ) -> Program: assert fee >= 0 @@ -357,7 +358,7 @@ async def _generate_unsigned_transaction( 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( + solution: Program = await self.make_solution( primaries=primaries, fee=fee, conditions=(*extra_conditions, CreateCoinAnnouncement(message)), @@ -379,7 +380,7 @@ async def _generate_unsigned_transaction( if coin.name() == origin_id: continue puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) - solution = self.make_solution(primaries=[], conditions=(primary_announcement,)) + solution = await self.make_solution(primaries=[], conditions=(primary_announcement,)) solution = decorator_manager.solve(puzzle, [], solution) spends.append( make_spend( diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 7b99d2483baa..744365206a8d 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -147,11 +147,12 @@ async def create_tandem_xch_tx( extra_conditions: Tuple[Condition, ...] = tuple(), ) -> None: ... - def make_solution( + async def make_solution( self, primaries: List[Payment], conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), + action_scope: Optional[WalletActionScope] = None, ) -> Program: ... async def get_puzzle(self, new: bool) -> Program: ... diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index c7edeeb0f0f7..7df53fec586d 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1033,7 +1033,7 @@ async def spend_clawback_coins( # 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( + inner_solution: Program = await self.main_wallet.make_solution( primaries=[ Payment( derivation_record.puzzle_hash, From 6d3195a4d654051b55649696abef0456a9ea279e Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 31 Jul 2024 22:30:09 +0100 Subject: [PATCH 242/274] make action_scope mandatory for make_solution --- .../wallet/cat_wallet/test_cat_wallet.py | 82 ++++++++++--------- .../wallet/test_main_wallet_protocol.py | 8 +- chia/data_layer/data_layer_wallet.py | 2 + chia/wallet/cat_wallet/cat_wallet.py | 5 +- chia/wallet/cat_wallet/dao_cat_wallet.py | 4 + chia/wallet/dao_wallet/dao_wallet.py | 10 ++- chia/wallet/did_wallet/did_wallet.py | 13 ++- chia/wallet/nft_wallet/nft_wallet.py | 10 ++- chia/wallet/vault/vault_wallet.py | 2 +- chia/wallet/vc_wallet/cr_cat_wallet.py | 4 + chia/wallet/vc_wallet/vc_wallet.py | 1 + chia/wallet/wallet.py | 7 +- chia/wallet/wallet_protocol.py | 2 +- chia/wallet/wallet_state_manager.py | 1 + 14 files changed, 99 insertions(+), 52 deletions(-) diff --git a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py index 62026ff814a8..ccc1f510f45a 100644 --- a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py +++ b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py @@ -1261,46 +1261,48 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non assert isinstance(cat_wallet, CATWallet) # Let's test that continuing to melt this CAT results in the correct balance changes - for _ in range(0, 5): - tx_amount -= 1 - new_coin = (await cat_wallet.get_cat_spendable_coins())[0].coin - new_spend = unsigned_spend_bundle_for_spendable_cats( - CAT_MOD, - [ - SpendableCAT( - coin=new_coin, - limitations_program_hash=ACS_TAIL_HASH, - inner_puzzle=await cat_wallet.inner_puzzle_for_cat_puzhash(new_coin.puzzle_hash), - inner_solution=await wallet.make_solution( - primaries=[Payment(wallet_ph, uint64(tx_amount), [wallet_ph])], - conditions=( - UnknownCondition( - opcode=Program.to(51), - args=[Program.to(None), Program.to(-113), Program.to(ACS_TAIL), Program.to(None)], + async with wallet.wallet_state_manager.new_action_scope(push=False) as action_scope: + for _ in range(0, 5): + tx_amount -= 1 + new_coin = (await cat_wallet.get_cat_spendable_coins())[0].coin + new_spend = unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + [ + SpendableCAT( + coin=new_coin, + limitations_program_hash=ACS_TAIL_HASH, + inner_puzzle=await cat_wallet.inner_puzzle_for_cat_puzhash(new_coin.puzzle_hash), + inner_solution=await wallet.make_solution( + primaries=[Payment(wallet_ph, uint64(tx_amount), [wallet_ph])], + action_scope=action_scope, + conditions=( + UnknownCondition( + opcode=Program.to(51), + args=[Program.to(None), Program.to(-113), Program.to(ACS_TAIL), Program.to(None)], + ), ), ), - ), - extra_delta=-1, - ) - ], - ) - signed_spend, _ = await env.wallet_state_manager.sign_bundle(new_spend.coin_spends) - await env.rpc_client.push_tx(signed_spend) - await time_out_assert(10, simulator.tx_id_in_mempool, True, signed_spend.name()) - - await wallet_environments.process_pending_states( - [ - WalletStateTransition( - pre_block_balance_updates={}, - post_block_balance_updates={ - "xch": {}, - "cat": { - "confirmed_wallet_balance": -1, - "unconfirmed_wallet_balance": -1, - "spendable_balance": -1, - "max_send_amount": -1, + extra_delta=-1, + ) + ], + ) + signed_spend, _ = await env.wallet_state_manager.sign_bundle(new_spend.coin_spends) + await env.rpc_client.push_tx(signed_spend) + await time_out_assert(10, simulator.tx_id_in_mempool, True, signed_spend.name()) + + await wallet_environments.process_pending_states( + [ + WalletStateTransition( + pre_block_balance_updates={}, + post_block_balance_updates={ + "xch": {}, + "cat": { + "confirmed_wallet_balance": -1, + "unconfirmed_wallet_balance": -1, + "spendable_balance": -1, + "max_send_amount": -1, + }, }, - }, - ) - ] - ) + ) + ] + ) diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py index 09190f4aadb5..763435cd220c 100644 --- a/chia/_tests/wallet/test_main_wallet_protocol.py +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -96,7 +96,11 @@ async def generate_signed_transaction( make_spend( coin, ACS, - (await self.make_solution(condition_list, extra_conditions, fee) if i == 0 else Program.to([])), + ( + await self.make_solution(condition_list, action_scope, extra_conditions, fee) + if i == 0 + else Program.to([]) + ), ) for i, coin in enumerate(coins) ], @@ -167,9 +171,9 @@ async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: # pragma: async def make_solution( self, primaries: List[Payment], + action_scope: WalletActionScope, conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), - action_scope: Optional[WalletActionScope] = None, **kwargs: Any, ) -> Program: condition_list: List[Condition] = [CreateCoin(p.puzzle_hash, p.amount, p.memos) for p in primaries] diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index 37382a00f24b..6e068d4d733d 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -544,6 +544,7 @@ async def create_update_state_spend( ) inner_sol: Program = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(b"$")) if fee > 0 else extra_conditions, ) db_layer_sol = Program.to([inner_sol]) @@ -755,6 +756,7 @@ async def delete_mirror( excess_fee: int = fee - mirror_coin.amount inner_sol: Program = await self.standard_wallet.make_solution( primaries=[Payment(new_puzhash, uint64(mirror_coin.amount - fee))] if excess_fee < 0 else [], + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(b"$")) if excess_fee > 0 else extra_conditions, ) mirror_spend = CoinSpend( diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index e1953972194c..8e45e4eef060 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -713,6 +713,7 @@ async def generate_unsigned_spendbundle( ) innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) elif regular_chia_to_claim > fee: # pragma: no cover @@ -725,6 +726,7 @@ async def generate_unsigned_spendbundle( assert xch_announcement is not None innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, xch_announcement, announcement), ) else: @@ -733,11 +735,12 @@ async def generate_unsigned_spendbundle( else: innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) else: innersol = await self.standard_wallet.make_solution( - primaries=[], conditions=(announcement.corresponding_assertion(),) + primaries=[], action_scope=action_scope, conditions=(announcement.corresponding_assertion(),) ) inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash) lineage_proof = await self.get_lineage_proof_for_coin(coin) diff --git a/chia/wallet/cat_wallet/dao_cat_wallet.py b/chia/wallet/cat_wallet/dao_cat_wallet.py index 4253a644179d..c8f582c5b9f3 100644 --- a/chia/wallet/cat_wallet/dao_cat_wallet.py +++ b/chia/wallet/cat_wallet/dao_cat_wallet.py @@ -257,6 +257,7 @@ async def create_vote_spend( amount: uint64, proposal_id: bytes32, is_yes_vote: bool, + action_scope: WalletActionScope, proposal_puzzle: Optional[Program] = None, ) -> SpendBundle: coins: List[LockedCoinInfo] = await self.advanced_select_coins(amount, proposal_id) @@ -300,6 +301,7 @@ async def create_vote_spend( message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash() inner_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(CreatePuzzleAnnouncement(message),), ) else: @@ -323,6 +325,7 @@ async def create_vote_spend( message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash() inner_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(CreatePuzzleAnnouncement(message),), ) if is_yes_vote: @@ -425,6 +428,7 @@ async def exit_vote_state( total_amt += coin.amount inner_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, ) # Create the solution using only the values needed for exiting the lockup mode (my_id = 0) solution = Program.to( diff --git a/chia/wallet/dao_wallet/dao_wallet.py b/chia/wallet/dao_wallet/dao_wallet.py index 786d6fb01217..71c6b23c8db9 100644 --- a/chia/wallet/dao_wallet/dao_wallet.py +++ b/chia/wallet/dao_wallet/dao_wallet.py @@ -934,6 +934,7 @@ async def generate_new_proposal( proposed_puzzle_reveal=proposed_puzzle, launcher_coin=launcher_coin, vote_amount=vote_amount, + action_scope=action_scope, ) full_spend = SpendBundle.aggregate([eve_spend, launcher_sb]) @@ -970,6 +971,7 @@ async def generate_proposal_eve_spend( proposed_puzzle_reveal: Program, launcher_coin: Coin, vote_amount: uint64, + action_scope: WalletActionScope, ) -> SpendBundle: cat_wallet: CATWallet = self.wallet_state_manager.wallets[self.dao_info.cat_wallet_id] cat_tail = cat_wallet.cat_info.limitations_program_hash @@ -979,7 +981,7 @@ async def generate_proposal_eve_spend( assert dao_cat_wallet is not None dao_cat_spend = await dao_cat_wallet.create_vote_spend( - vote_amount, launcher_coin.name(), True, proposal_puzzle=dao_proposal_puzzle + vote_amount, launcher_coin.name(), True, action_scope, proposal_puzzle=dao_proposal_puzzle ) vote_amounts = [] vote_coins = [] @@ -1060,7 +1062,11 @@ async def generate_proposal_vote_spend( vote_amount = await dao_cat_wallet.get_votable_balance(proposal_id) assert vote_amount is not None dao_cat_spend = await dao_cat_wallet.create_vote_spend( - vote_amount, proposal_id, is_yes_vote, proposal_puzzle=proposal_info.current_innerpuz + vote_amount, + proposal_id, + is_yes_vote, + action_scope=action_scope, + proposal_puzzle=proposal_info.current_innerpuz, ) vote_amounts = [] vote_coins = [] diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 8c3c88d14ed8..21fb705e2fc8 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -586,6 +586,7 @@ async def create_update_spend( memos=[p2_puzzle.get_tree_hash()], ) ], + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(coin.name())), ) innersol: Program = Program.to([1, p2_solution]) @@ -696,6 +697,7 @@ async def transfer_did( ) p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(new_did_puzhash, uint64(coin.amount), [new_puzhash])], + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(coin.name())), ) # Need to include backup list reveal here, even we are don't recover @@ -785,6 +787,7 @@ async def create_message_spend( ) p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(puzzle_hash=new_innerpuzzle_hash, amount=uint64(coin.amount), memos=[p2_ph])], + action_scope=action_scope, conditions=extra_conditions, ) # innerpuz solution is (mode p2_solution) @@ -923,6 +926,7 @@ async def create_attestment( Payment(innerpuz.get_tree_hash(), uint64(coin.amount), [p2_puzzle.get_tree_hash()]), Payment(innermessage, uint64(0)), ], + action_scope=action_scope, conditions=extra_conditions, ) innersol = Program.to([1, p2_solution]) @@ -1274,7 +1278,12 @@ async def generate_new_decentralised_id( metadata=self.did_info.metadata, ) await self.save_info(did_info) - eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner) + eve_spend = await self.generate_eve_spend( + eve_coin, + did_full_puz, + did_inner, + action_scope, + ) full_spend = SpendBundle.aggregate([eve_spend, launcher_sb]) assert self.did_info.origin_coin is not None assert self.did_info.current_inner is not None @@ -1306,6 +1315,7 @@ async def generate_eve_spend( coin: Coin, full_puzzle: Program, innerpuz: Program, + action_scope: WalletActionScope, extra_conditions: Tuple[Condition, ...] = tuple(), ): assert self.did_info.origin_coin is not None @@ -1315,6 +1325,7 @@ async def generate_eve_spend( # innerpuz solution is (mode p2_solution) p2_solution = await self.standard_wallet.make_solution( primaries=[Payment(innerpuz.get_tree_hash(), uint64(coin.amount), [p2_puzzle.get_tree_hash()])], + action_scope=action_scope, conditions=extra_conditions, ) innersol = Program.to([1, p2_solution]) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 311a1df37b21..3b1269e4d89b 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -713,6 +713,7 @@ async def generate_unsigned_spendbundle( innersol: Program = await self.standard_wallet.make_solution( primaries=payments, + action_scope=action_scope, conditions=(*extra_conditions, CreateCoinAnnouncement(coin_name)) if fee > 0 else extra_conditions, ) @@ -1396,6 +1397,7 @@ async def mint_from_did( solution: Program = await self.standard_wallet.make_solution( primaries=[xch_payment], + action_scope=action_scope, fee=fee, conditions=xch_extra_conditions, ) @@ -1408,7 +1410,7 @@ async def mint_from_did( for xch_coin in xch_coins_iter: puzzle = await self.standard_wallet.puzzle_for_puzzle_hash(xch_coin.puzzle_hash) solution = await self.standard_wallet.make_solution( - primaries=[], conditions=(AssertCoinAnnouncement(primary_announcement_hash),) + primaries=[], action_scope=action_scope, conditions=(AssertCoinAnnouncement(primary_announcement_hash),) ) xch_spends.append(make_spend(xch_coin, puzzle, solution)) xch_spend = SpendBundle(xch_spends, G2Element()) @@ -1416,6 +1418,7 @@ async def mint_from_did( # Create the DID spend using the announcements collected when making the intermediate launcher coins did_p2_solution = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=( *extra_conditions, did_coin_announcement, @@ -1654,13 +1657,16 @@ async def mint_from_xch( solution: Program = await self.standard_wallet.make_solution( primaries=[xch_payment] + primaries, + action_scope=action_scope, fee=fee, conditions=extra_conditions, ) primary_announcement = AssertCoinAnnouncement(asserted_id=xch_coin.name(), asserted_msg=message) first = False else: - solution = await self.standard_wallet.make_solution(primaries=[], conditions=(primary_announcement,)) + solution = await self.standard_wallet.make_solution( + primaries=[], action_scope=action_scope, conditions=(primary_announcement,) + ) xch_spends.append(make_spend(xch_coin, puzzle, solution)) # Collect up all the coin spends and sign them diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 3c081c45013c..da6d77c56aaa 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -412,9 +412,9 @@ async def sum_hint_for_pubkey(self, pk: bytes) -> Optional[SumHint]: async def make_solution( self, primaries: List[Payment], + action_scope: WalletActionScope, conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), - action_scope: Optional[WalletActionScope] = None, **kwargs: Any, ) -> Program: assert fee >= 0 diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index efbd0500d642..d7085df915d0 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -519,6 +519,7 @@ async def _generate_unsigned_spendbundle( ) innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) elif regular_chia_to_claim > fee: @@ -531,6 +532,7 @@ async def _generate_unsigned_spendbundle( assert xch_announcement is not None innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, xch_announcement, announcement), ) else: @@ -539,11 +541,13 @@ async def _generate_unsigned_spendbundle( else: innersol = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=(*extra_conditions, announcement), ) else: innersol = await self.standard_wallet.make_solution( primaries=[], + action_scope=action_scope, conditions=(announcement.corresponding_assertion(),), ) inner_derivation_record = ( diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index 716c85176acc..27543d0f8a30 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -301,6 +301,7 @@ async def generate_signed_transaction( extra_conditions = (*extra_conditions, UnknownCondition.from_program(magic_condition)) innersol: Program = await self.standard_wallet.make_solution( primaries=primaries, + action_scope=action_scope, conditions=extra_conditions, ) did_announcement, coin_spend, vc = vc_record.vc.do_spend(inner_puzzle, innersol, new_proof_hash) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 32fcf48e44f0..cb32855674ac 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -217,9 +217,9 @@ async def get_new_puzzlehash(self) -> bytes32: async def make_solution( self, primaries: List[Payment], + action_scope: WalletActionScope, conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), - action_scope: Optional[WalletActionScope] = None, **kwargs: Any, ) -> Program: assert fee >= 0 @@ -360,6 +360,7 @@ async def _generate_unsigned_transaction( puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) solution: Program = await self.make_solution( primaries=primaries, + action_scope=action_scope, fee=fee, conditions=(*extra_conditions, CreateCoinAnnouncement(message)), ) @@ -380,7 +381,9 @@ async def _generate_unsigned_transaction( if coin.name() == origin_id: continue puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) - solution = await self.make_solution(primaries=[], conditions=(primary_announcement,)) + solution = await self.make_solution( + primaries=[], action_scope=action_scope, conditions=(primary_announcement,) + ) solution = decorator_manager.solve(puzzle, [], solution) spends.append( make_spend( diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index 744365206a8d..2150886cf719 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -150,9 +150,9 @@ async def create_tandem_xch_tx( async def make_solution( self, primaries: List[Payment], + action_scope: WalletActionScope, conditions: Tuple[Condition, ...] = tuple(), fee: uint64 = uint64(0), - action_scope: Optional[WalletActionScope] = None, ) -> Program: ... async def get_puzzle(self, new: bool) -> Program: ... diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 7df53fec586d..3dc1a0293b7f 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1041,6 +1041,7 @@ async def spend_clawback_coins( memos, # Forward memo of the first coin ) ], + action_scope=action_scope, conditions=( extra_conditions if len(coin_spends) > 0 or fee == 0 From 0118843707d90b0ea632df57679f4c38c0775ab7 Mon Sep 17 00:00:00 2001 From: Geoff Walmsley Date: Wed, 31 Jul 2024 22:51:31 +0100 Subject: [PATCH 243/274] missed a couple --- chia/wallet/puzzles/tails.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py index 21a2a3580a28..9421e1f98f6f 100644 --- a/chia/wallet/puzzles/tails.py +++ b/chia/wallet/puzzles/tails.py @@ -122,7 +122,8 @@ async def generate_issuance_bundle( Program.to([51, 0, -113, tail, []]), ( await wallet.standard_wallet.make_solution( - primaries=[Payment(inner_tree_hash, amount, [inner_tree_hash])] + primaries=[Payment(inner_tree_hash, amount, [inner_tree_hash])], + action_scope=action_scope, ) ), ) @@ -308,6 +309,7 @@ async def generate_issuance_bundle( ( await wallet.standard_wallet.make_solution( primaries=[payment], + action_scope=action_scope, ) ), ) From 1e2ac8a02f54469a947d2272e6443dab63d4f992 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 6 Aug 2024 12:15:50 -0700 Subject: [PATCH 244/274] Add cost logging --- .../wallet/vault/test_vault_lifecycle.py | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py index 93d136db0484..dfe0a7cdb8a0 100644 --- a/chia/_tests/wallet/vault/test_vault_lifecycle.py +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -96,7 +96,10 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: proof = get_vault_proof(vault_merkle_tree, secp_puzzlehash) vault_solution_secp = Program.to([proof, secp_puzzle, secp_solution]) - vault_spendbundle = SpendBundle([make_spend(vault_coin, vault_puzzle, vault_solution_secp)], G2Element()) + vault_spendbundle = cost_logger.add_cost( + "Standard spend w/ 1 extra CC", + SpendBundle([make_spend(vault_coin, vault_puzzle, vault_solution_secp)], G2Element()), + ) result: Tuple[MempoolInclusionStatus, Optional[Err]] = await client.push_tx(vault_spendbundle) assert result[0] == MempoolInclusionStatus.SUCCESS @@ -111,14 +114,17 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: recovery_solution = Program.to([vault_coin.amount, recovery_conditions]) recovery_proof = get_vault_proof(vault_merkle_tree, p2_recovery_puzzlehash) vault_solution_recovery = Program.to([recovery_proof, p2_recovery_puzzle, recovery_solution]) - vault_spendbundle = SpendBundle( - [make_spend(vault_coin, vault_puzzle, vault_solution_recovery)], - AugSchemeMPL.sign( - BLS_SK, - ( - recovery_conditions.get_tree_hash() - + vault_coin.name() - + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA + vault_spendbundle = cost_logger.add_cost( + "Recovery initiation", + SpendBundle( + [make_spend(vault_coin, vault_puzzle, vault_solution_recovery)], + AugSchemeMPL.sign( + BLS_SK, + ( + recovery_conditions.get_tree_hash() + + vault_coin.name() + + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA + ), ), ), ) @@ -141,7 +147,9 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: proof = get_vault_proof(recovery_merkle_tree, recovery_finish_puzzlehash) recovery_finish_solution = Program.to([]) recovery_solution = Program.to([proof, recovery_finish_puzzle, recovery_finish_solution]) - finish_spendbundle = SpendBundle([make_spend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + finish_spendbundle = cost_logger.add_cost( + "Finish recovery", SpendBundle([make_spend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + ) result = await client.push_tx(finish_spendbundle) assert result[1] == Err.ASSERT_SECONDS_RELATIVE_FAILED @@ -184,6 +192,8 @@ async def test_vault_inner(cost_logger: CostLogger) -> None: ) recovery_solution = Program.to([proof, secp_puzzle, secp_solution]) - escape_spendbundle = SpendBundle([make_spend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + escape_spendbundle = cost_logger.add_cost( + "Escape recovery", SpendBundle([make_spend(recovery_coin, recovery_puzzle, recovery_solution)], G2Element()) + ) result = await client.push_tx(escape_spendbundle) assert result[0] == MempoolInclusionStatus.SUCCESS From c381426d584ef0caf8581d2b16f1bde78d2c41b7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 7 Aug 2024 14:20:48 -0700 Subject: [PATCH 245/274] Fix tests --- chia/_tests/wallet/rpc/test_wallet_rpc.py | 5 ----- chia/util/observation_root.py | 3 ++- chia/wallet/wallet_node.py | 6 +++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index b463e6fad99c..79f17a57330f 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -1740,11 +1740,6 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn sk_dict = await client.get_private_key(pks[1]) assert sk_dict["fingerprint"] == pks[1] - assert await client.log_in(1234567890) == ( - 'RPC response failure: {"error": "fingerprint 1234567890 not found' - ' in keychain or keychain is empty", "success": false}' - ) - # test hardened keys await _check_delete_key(client=client, wallet_node=wallet_node, farmer_fp=pks[0], pool_fp=pks[1], observer=False) diff --git a/chia/util/observation_root.py b/chia/util/observation_root.py index 95a3901046db..1ea9c36b4d37 100644 --- a/chia/util/observation_root.py +++ b/chia/util/observation_root.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import Protocol +from typing import Protocol, runtime_checkable +@runtime_checkable class ObservationRoot(Protocol): def get_fingerprint(self) -> int: ... diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 6d02e676fa9a..05077f6427b0 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -419,7 +419,7 @@ async def _start_with_fingerprint( multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self._weight_proof_handler = WalletWeightProofHandler(self.constants, multiprocessing_context) self.synced_peers = set() - public_key = None + observation_root = None private_key = await self.get_key(fingerprint, private=True, find_a_default=False) if private_key is None: observation_root = await self.get_key(fingerprint, private=False, find_a_default=False) @@ -427,7 +427,7 @@ async def _start_with_fingerprint( assert isinstance(private_key, PrivateKey) observation_root = private_key.get_g1() - if public_key is None: + if observation_root is None: private_key = await self.get_key(None, private=True, find_a_default=True) if private_key is not None: assert isinstance(private_key, PrivateKey) @@ -435,8 +435,8 @@ async def _start_with_fingerprint( else: self.log_out() return False - assert isinstance(observation_root, G1Element) # override with private key fetched in case it's different from what was passed + assert isinstance(observation_root, ObservationRoot) if fingerprint is None: fingerprint = observation_root.get_fingerprint() if self.config.get("enable_profiler", False): From a6df3ea0c490b949190ec8e116ba903f6ce11967 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 13 Aug 2024 07:12:10 -0700 Subject: [PATCH 246/274] mypy --- chia/_tests/wallet/cat_wallet/test_cat_wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py index 03fcde87f4aa..c382213e1bca 100644 --- a/chia/_tests/wallet/cat_wallet/test_cat_wallet.py +++ b/chia/_tests/wallet/cat_wallet/test_cat_wallet.py @@ -1234,7 +1234,7 @@ async def test_cat_melt_balance(wallet_environments: WalletTestFramework) -> Non assert isinstance(cat_wallet, CATWallet) # Let's test that continuing to melt this CAT results in the correct balance changes - async with wallet.wallet_state_manager.new_action_scope(push=False) as action_scope: + async with wallet.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=False) as action_scope: for _ in range(0, 5): tx_amount -= 1 new_coin = (await cat_wallet.get_cat_spendable_coins())[0].coin From 7d6cce1ccb2cc62be6ba7d7d1aa1bf4c6daffe18 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 19 Jul 2024 13:41:31 -0700 Subject: [PATCH 247/274] Add SecretInfo to key scheme protocols --- chia/util/observation_root.py | 4 ++++ chia/util/secret_info.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 chia/util/secret_info.py diff --git a/chia/util/observation_root.py b/chia/util/observation_root.py index 1ea9c36b4d37..57bc96ebd1f7 100644 --- a/chia/util/observation_root.py +++ b/chia/util/observation_root.py @@ -11,3 +11,7 @@ def __bytes__(self) -> bytes: ... @classmethod def from_bytes(cls, blob: bytes) -> ObservationRoot: ... + + +class Signature(Protocol): + def __bytes__(self) -> bytes: ... diff --git a/chia/util/secret_info.py b/chia/util/secret_info.py new file mode 100644 index 000000000000..2f60f2dd43a3 --- /dev/null +++ b/chia/util/secret_info.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import Optional, Protocol, Type, TypeVar + +from chia.util.observation_root import ObservationRoot, Signature + +_T_ObservationRoot = TypeVar("_T_ObservationRoot", bound=ObservationRoot) + + +class SecretInfo(Protocol[_T_ObservationRoot]): + def __bytes__(self) -> bytes: ... + @classmethod + def from_bytes(cls: Type[_T_SecretInfo], blob: bytes) -> _T_SecretInfo: ... + def public_key(self) -> _T_ObservationRoot: ... + def derive_hardened(self: _T_SecretInfo, index: int) -> _T_SecretInfo: ... + def derive_unhardened(self: _T_SecretInfo, index: int) -> _T_SecretInfo: ... + @classmethod + def from_seed(cls: Type[_T_SecretInfo], seed: bytes) -> _T_SecretInfo: ... + def sign(self, msg: bytes, final_pk: Optional[_T_ObservationRoot] = None) -> Signature: ... + + +_T_SecretInfo = TypeVar("_T_SecretInfo", bound=SecretInfo[ObservationRoot]) From 27dda226ff651f40956f2080a84092c6e14982d6 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 13 Aug 2024 08:12:42 -0700 Subject: [PATCH 248/274] Deduplicate keychain code a bit --- chia/cmds/passphrase_funcs.py | 1 - chia/legacy/keyring.py | 7 ++- chia/util/keychain.py | 98 +++++++++++++++-------------------- 3 files changed, 45 insertions(+), 61 deletions(-) diff --git a/chia/cmds/passphrase_funcs.py b/chia/cmds/passphrase_funcs.py index f3b85590fa4c..4e24804d4fd5 100644 --- a/chia/cmds/passphrase_funcs.py +++ b/chia/cmds/passphrase_funcs.py @@ -20,7 +20,6 @@ colorama.Fore.YELLOW + colorama.Style.BRIGHT + "(Unlock Keyring)" + colorama.Style.RESET_ALL + " Passphrase: " ) # noqa: E501 FAILED_ATTEMPT_DELAY = 0.5 -MAX_KEYS = 100 MAX_RETRIES = 3 SAVE_MASTER_PASSPHRASE_WARNING = ( colorama.Fore.YELLOW diff --git a/chia/legacy/keyring.py b/chia/legacy/keyring.py index 1821ce98e061..8ff1a021e15d 100644 --- a/chia/legacy/keyring.py +++ b/chia/legacy/keyring.py @@ -25,7 +25,7 @@ from chia.cmds.cmds_util import prompt_yes_no from chia.util.errors import KeychainUserNotFound -from chia.util.keychain import KeyData, KeyDataSecrets, KeyTypes, get_private_key_user +from chia.util.keychain import MAX_KEYS, KeyData, KeyDataSecrets, KeyTypes, get_private_key_user LegacyKeyring = Union[MacKeyring, WinKeyring, CryptFileKeyring] @@ -33,7 +33,6 @@ CURRENT_KEY_VERSION = "1.8" DEFAULT_USER = f"user-chia-{CURRENT_KEY_VERSION}" # e.g. user-chia-1.8 DEFAULT_SERVICE = f"chia-{DEFAULT_USER}" # e.g. chia-user-chia-1.8 -MAX_KEYS = 100 # casting to compensate for a combination of mypy and keyring issues @@ -90,7 +89,7 @@ def get_key_data(keyring: LegacyKeyring, index: int) -> KeyData: def get_keys(keyring: LegacyKeyring) -> List[KeyData]: keys: List[KeyData] = [] - for index in range(MAX_KEYS + 1): + for index in range(MAX_KEYS): try: keys.append(get_key_data(keyring, index)) except KeychainUserNotFound: @@ -114,7 +113,7 @@ def print_keys(keyring: LegacyKeyring) -> None: def remove_keys(keyring: LegacyKeyring) -> None: removed = 0 - for index in range(MAX_KEYS + 1): + for index in range(MAX_KEYS): try: keyring.delete_password(DEFAULT_SERVICE, get_private_key_user(DEFAULT_USER, index)) removed += 1 diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 9c312563de2c..3a965a7e9bc2 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -7,7 +7,7 @@ from functools import cached_property from hashlib import pbkdf2_hmac from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union, overload +from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple, Type, TypeVar, Union, overload import importlib_resources from bitstring import BitArray # pyright: reportMissingImports=false @@ -37,7 +37,7 @@ CURRENT_KEY_VERSION = "1.8" DEFAULT_USER = f"user-chia-{CURRENT_KEY_VERSION}" # e.g. user-chia-1.8 DEFAULT_SERVICE = f"chia-{DEFAULT_USER}" # e.g. chia-user-chia-1.8 -MAX_KEYS = 100 +MAX_KEYS = 101 MIN_PASSPHRASE_LEN = 8 @@ -486,29 +486,34 @@ def delete_label(self, fingerprint: int) -> None: """ self.keyring_wrapper.keyring.delete_label(fingerprint) + def _iterate_through_key_datas( + self, include_secrets: bool = True, skip_public_only: bool = False + ) -> Iterator[KeyData]: + for index in range(MAX_KEYS): + try: + key_data = self._get_key_data(index, include_secrets=include_secrets) + if key_data is None or (skip_public_only and key_data.secrets is None): + continue + yield key_data + except KeychainUserNotFound: + pass + return None + def get_first_private_key(self) -> Optional[Tuple[PrivateKey, bytes]]: """ Returns the first key in the keychain that has one of the passed in passphrases. """ - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index) - return key_data.private_key, key_data.entropy - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(skip_public_only=True): + return key_data.private_key, key_data.entropy return None def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[Tuple[PrivateKey, bytes]]: """ Return first private key which have the given public key fingerprint. """ - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index) - if key_data.fingerprint == fingerprint: - return key_data.private_key, key_data.entropy - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(skip_public_only=True): + if key_data.fingerprint == fingerprint: + return key_data.private_key, key_data.entropy return None def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: @@ -517,25 +522,18 @@ def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: A tuple of key, and entropy bytes (i.e. mnemonic) is returned for each key. """ all_keys: List[Tuple[PrivateKey, bytes]] = [] - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index) - all_keys.append((key_data.private_key, key_data.entropy)) - except (KeychainUserNotFound, KeychainSecretsMissing): - pass + for key_data in self._iterate_through_key_datas(skip_public_only=True): + all_keys.append((key_data.private_key, key_data.entropy)) return all_keys def get_key(self, fingerprint: int, include_secrets: bool = False) -> KeyData: """ Return the KeyData of the first key which has the given public key fingerprint. """ - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index, include_secrets) - if key_data.observation_root.get_fingerprint() == fingerprint: - return key_data - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(include_secrets=include_secrets, skip_public_only=False): + if key_data.observation_root.get_fingerprint() == fingerprint: + return key_data + raise KeychainFingerprintNotFound(fingerprint) def get_keys(self, include_secrets: bool = False) -> List[KeyData]: @@ -543,12 +541,9 @@ def get_keys(self, include_secrets: bool = False) -> List[KeyData]: Returns the KeyData of all keys which can be retrieved. """ all_keys: List[KeyData] = [] - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index, include_secrets) - all_keys.append(key_data) - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(include_secrets=include_secrets, skip_public_only=False): + all_keys.append(key_data) + return all_keys def get_all_public_keys(self) -> List[ObservationRoot]: @@ -556,24 +551,18 @@ def get_all_public_keys(self) -> List[ObservationRoot]: Returns all public keys. """ all_keys: List[ObservationRoot] = [] - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index) - all_keys.append(key_data.observation_root) - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(skip_public_only=False): + all_keys.append(key_data.observation_root) + return all_keys def get_all_public_keys_of_type(self, key_type: Type[_T_ObservationRoot]) -> List[_T_ObservationRoot]: all_keys: List[_T_ObservationRoot] = [] - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index) - if key_data.key_type == TYPES_TO_KEY_TYPES[key_type]: - assert isinstance(key_data.observation_root, key_type) - all_keys.append(key_data.observation_root) - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(skip_public_only=False): + if key_data.key_type == TYPES_TO_KEY_TYPES[key_type]: + assert isinstance(key_data.observation_root, key_type) + all_keys.append(key_data.observation_root) + return all_keys def get_first_public_key(self) -> Optional[G1Element]: @@ -588,10 +577,11 @@ def delete_key_by_fingerprint(self, fingerprint: int) -> int: Deletes all keys which have the given public key fingerprint and returns how many keys were removed. """ removed = 0 - for index in range(MAX_KEYS + 1): + # We duplicate ._iterate_through_key_datas due to needing the index + for index in range(MAX_KEYS): try: key_data = self._get_key_data(index, include_secrets=False) - if key_data.fingerprint == fingerprint: + if key_data is not None and key_data.fingerprint == fingerprint: try: self.keyring_wrapper.keyring.delete_label(key_data.fingerprint) except (KeychainException, NotImplementedError): @@ -623,12 +613,8 @@ def delete_all_keys(self) -> None: """ Deletes all keys from the keychain. """ - for index in range(MAX_KEYS + 1): - try: - key_data = self._get_key_data(index) - self.delete_key_by_fingerprint(key_data.fingerprint) - except KeychainUserNotFound: - pass + for key_data in self._iterate_through_key_datas(include_secrets=False, skip_public_only=False): + self.delete_key_by_fingerprint(key_data.fingerprint) @staticmethod def is_keyring_locked() -> bool: From 5755cf6be0c5603bdca4af40ce7964a7ad0175f8 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 13 Aug 2024 10:57:57 -0700 Subject: [PATCH 249/274] Implement secp256r1 wrappers --- chia/_tests/util/test_secp256r1.py | 25 ++++++++ chia/util/key_types.py | 93 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 chia/_tests/util/test_secp256r1.py create mode 100644 chia/util/key_types.py diff --git a/chia/_tests/util/test_secp256r1.py b/chia/_tests/util/test_secp256r1.py new file mode 100644 index 000000000000..e1c36a43746a --- /dev/null +++ b/chia/_tests/util/test_secp256r1.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import pytest + +from chia.util.ints import uint32 +from chia.util.key_types import Secp256r1PrivateKey, Secp256r1PublicKey +from chia.util.keychain import generate_mnemonic, mnemonic_to_seed + + +def test_key_drivers() -> None: + """ + This tests that the chia.util.key_types drivers for these keys works properly, it does not test the sanity of the + underlying library. + """ + mnemonic = generate_mnemonic() + sk = Secp256r1PrivateKey.from_seed(mnemonic_to_seed(mnemonic)) + assert Secp256r1PrivateKey.from_bytes(bytes(sk)) == sk + with pytest.raises(NotImplementedError): + sk.derive_hardened(1) + with pytest.raises(NotImplementedError): + sk.derive_unhardened(1) + + pk = sk.public_key() + assert Secp256r1PublicKey.from_bytes(bytes(pk)) == pk + assert pk.get_fingerprint() < uint32.MAXIMUM diff --git a/chia/util/key_types.py b/chia/util/key_types.py new file mode 100644 index 000000000000..4b6b84bd827e --- /dev/null +++ b/chia/util/key_types.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + +from chia.util.hash import std_hash + + +# A wrapper for VerifyingKey that conforms to the ObservationRoot protocol +@dataclass(frozen=True) +class Secp256r1PublicKey: + _public_key: ec.EllipticCurvePublicKey + + def get_fingerprint(self) -> int: + hash_bytes = std_hash(bytes(self)) + return int.from_bytes(hash_bytes[0:4], "big") + + def __bytes__(self) -> bytes: + return self._public_key.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + @classmethod + def from_bytes(cls, blob: bytes) -> Secp256r1PublicKey: + pk = serialization.load_der_public_key(blob) + if isinstance(pk, ec.EllipticCurvePublicKey): + return Secp256r1PublicKey(pk) + else: + raise ValueError("Could not load EllipticCurvePublicKey provided blob") + + def derive_unhardened(self, index: int) -> Secp256r1PublicKey: + raise NotImplementedError("SECP keys do not support derivation") + + +@dataclass(frozen=True) +class Secp256r1Signature: + _buf: bytes + + def __bytes__(self) -> bytes: + return self._buf + + +# A wrapper for SigningKey that conforms to the SecretInfo protocol +@dataclass(frozen=True) +class Secp256r1PrivateKey: + _private_key: ec.EllipticCurvePrivateKey + + def __eq__(self, other: object) -> bool: + return isinstance(other, Secp256r1PrivateKey) and self.public_key() == other.public_key() + + def __bytes__(self) -> bytes: + return self._private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + @classmethod + def from_bytes(cls, blob: bytes) -> Secp256r1PrivateKey: + sk = serialization.load_der_private_key(blob, password=None) + if isinstance(sk, ec.EllipticCurvePrivateKey): + return Secp256r1PrivateKey(sk) + else: + raise ValueError("Could not load EllipticCurvePrivateKey provided blob") + + def public_key(self) -> Secp256r1PublicKey: + return Secp256r1PublicKey(self._private_key.public_key()) + + @classmethod + def from_seed(cls, seed: bytes) -> Secp256r1PrivateKey: + return Secp256r1PrivateKey( + ec.derive_private_key(int.from_bytes(std_hash(seed), "big"), ec.SECP256R1(), default_backend()) + ) + + def sign(self, msg: bytes, final_pk: Optional[Secp256r1PublicKey] = None) -> Secp256r1Signature: + if final_pk is not None: + raise ValueError("SECP256r1 does not support signature aggregation") + der_sig = self._private_key.sign(msg, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) + r, s = decode_dss_signature(der_sig) + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + return Secp256r1Signature(sig) + + def derive_hardened(self, index: int) -> Secp256r1PrivateKey: + raise NotImplementedError("SECP keys do not support derivation") + + def derive_unhardened(self, index: int) -> Secp256r1PrivateKey: + raise NotImplementedError("SECP keys do not support derivation") From 586cab79176e9712097da24c543d4e9981c09dac Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 13 Aug 2024 13:14:43 -0700 Subject: [PATCH 250/274] Remove `PrivateKey` type from wallet --- .../wallet/test_main_wallet_protocol.py | 4 +- chia/_tests/wallet/test_signer_protocol.py | 3 +- chia/_tests/wallet/test_wallet.py | 8 ++-- chia/_tests/wallet/test_wallet_node.py | 2 +- .../wallet/test_wallet_state_manager.py | 8 ++-- chia/pools/pool_wallet.py | 4 +- chia/rpc/wallet_rpc_api.py | 6 +-- chia/wallet/derivation_record.py | 7 ++++ chia/wallet/vault/vault_wallet.py | 5 ++- chia/wallet/wallet.py | 38 ++++++++++--------- chia/wallet/wallet_protocol.py | 3 +- chia/wallet/wallet_puzzle_store.py | 2 +- chia/wallet/wallet_state_manager.py | 33 +++++++++------- 13 files changed, 75 insertions(+), 48 deletions(-) diff --git a/chia/_tests/wallet/test_main_wallet_protocol.py b/chia/_tests/wallet/test_main_wallet_protocol.py index fbdca533f861..1aed08b625dc 100644 --- a/chia/_tests/wallet/test_main_wallet_protocol.py +++ b/chia/_tests/wallet/test_main_wallet_protocol.py @@ -129,7 +129,7 @@ async def generate_signed_transaction( ) ) - def puzzle_for_pk(self, pubkey: G1Element) -> Program: # pragma: no cover + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: # pragma: no cover raise ValueError("This won't work") async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: @@ -183,7 +183,7 @@ async def make_solution( async def get_puzzle(self, new: bool) -> Program: # pragma: no cover return ACS - def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: # pragma: no cover + def puzzle_hash_for_pk(self, pubkey: ObservationRoot) -> bytes32: # pragma: no cover raise ValueError("This won't work") def require_derivation_paths(self) -> bool: diff --git a/chia/_tests/wallet/test_signer_protocol.py b/chia/_tests/wallet/test_signer_protocol.py index 7e7323382953..4d3f3b91df64 100644 --- a/chia/_tests/wallet/test_signer_protocol.py +++ b/chia/_tests/wallet/test_signer_protocol.py @@ -312,7 +312,8 @@ async def test_p2dohp_wallet_signer_protocol(wallet_environments: WalletTestFram @pytest.mark.anyio async def test_p2blsdohp_execute_signing_instructions(wallet_environments: WalletTestFramework) -> None: wallet: MainWalletProtocol = wallet_environments.environments[0].xch_wallet - root_sk: PrivateKey = wallet.wallet_state_manager.get_master_private_key() + root_sk = wallet.wallet_state_manager.get_master_private_key() + assert isinstance(root_sk, PrivateKey) root_pk: G1Element = root_sk.get_g1() root_fingerprint: bytes = root_pk.get_fingerprint().to_bytes(4, "big") diff --git a/chia/_tests/wallet/test_wallet.py b/chia/_tests/wallet/test_wallet.py index 3ab7954c32ac..e1a7a2eabbd9 100644 --- a/chia/_tests/wallet/test_wallet.py +++ b/chia/_tests/wallet/test_wallet.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple import pytest -from chia_rs import AugSchemeMPL, G1Element, G2Element +from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia._tests.util.time_out_assert import time_out_assert @@ -1875,9 +1875,11 @@ async def test_address_sliding_window(self, wallet_environments: WalletTestFrame peak = full_node_api.full_node.blockchain.get_peak_height() assert peak is not None - puzzle_hashes = [] + puzzle_hashes: List[bytes32] = [] for i in range(211): - pubkey = master_sk_to_wallet_sk(wallet.wallet_state_manager.get_master_private_key(), uint32(i)).get_g1() + sk = wallet.wallet_state_manager.get_master_private_key() + assert isinstance(sk, PrivateKey) + pubkey = master_sk_to_wallet_sk(sk, uint32(i)).public_key() puzzle: Program = wallet.puzzle_for_pk(pubkey) puzzle_hash: bytes32 = puzzle.get_tree_hash() puzzle_hashes.append(puzzle_hash) diff --git a/chia/_tests/wallet/test_wallet_node.py b/chia/_tests/wallet/test_wallet_node.py index 6b4a2bd1348d..63638a9fa594 100644 --- a/chia/_tests/wallet/test_wallet_node.py +++ b/chia/_tests/wallet/test_wallet_node.py @@ -649,7 +649,7 @@ async def test_get_last_used_fingerprint_if_exists( assert node.wallet_state_manager.private_key is not None assert ( await node.get_last_used_fingerprint_if_exists() - == node.wallet_state_manager.private_key.get_g1().get_fingerprint() + == node.wallet_state_manager.private_key.public_key().get_fingerprint() ) await node.keychain_proxy.delete_all_keys() assert await node.get_last_used_fingerprint_if_exists() is None diff --git a/chia/_tests/wallet/test_wallet_state_manager.py b/chia/_tests/wallet/test_wallet_state_manager.py index cb83bbb1e231..ef2b77f3b9e9 100644 --- a/chia/_tests/wallet/test_wallet_state_manager.py +++ b/chia/_tests/wallet/test_wallet_state_manager.py @@ -4,7 +4,7 @@ from typing import AsyncIterator, List import pytest -from chia_rs import G2Element +from chia_rs import G2Element, PrivateKey from chia._tests.environments.wallet import WalletTestFramework from chia._tests.util.setup_nodes import OldSimulatorsAndWallets @@ -68,11 +68,13 @@ async def test_get_private_key(simulator_and_wallet: OldSimulatorsAndWallets, ha wallet_state_manager: WalletStateManager = wallet_node.wallet_state_manager derivation_index = uint32(10000) conversion_method = master_sk_to_wallet_sk if hardened else master_sk_to_wallet_sk_unhardened - expected_private_key = conversion_method(wallet_state_manager.get_master_private_key(), derivation_index) + sk = wallet_state_manager.get_master_private_key() + assert isinstance(sk, PrivateKey) + expected_private_key = conversion_method(sk, derivation_index) record = DerivationRecord( derivation_index, bytes32(b"0" * 32), - expected_private_key.get_g1(), + bytes(expected_private_key.public_key()), WalletType.STANDARD_WALLET, uint32(1), hardened, diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index cb350112f17c..612d15899682 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -439,9 +439,11 @@ async def create_new_pool_wallet_transaction( return p2_singleton_puzzle_hash, launcher_coin_id async def _get_owner_key_cache(self) -> Tuple[PrivateKey, uint32]: + private_key = self.wallet_state_manager.get_master_private_key() + assert isinstance(private_key, PrivateKey) if self._owner_sk_and_index is None: self._owner_sk_and_index = find_owner_sk( - [self.wallet_state_manager.get_master_private_key()], + [private_key], (await self.get_current_state()).current.owner_pubkey, ) assert self._owner_sk_and_index is not None diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 789b1df6830b..dd2e25fcd3bb 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -975,9 +975,9 @@ async def create_new_wallet( 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.get_master_private_key(), uint32(max_pwi + 1) - ) + master_sk = self.service.wallet_state_manager.get_master_private_key() + assert isinstance(master_sk, PrivateKey), "Pooling only works with BLS keys at this time" + owner_sk: PrivateKey = master_sk_to_singleton_owner_sk(master_sk, uint32(max_pwi + 1)) owner_pk: G1Element = owner_sk.get_g1() initial_target_state = initial_pool_state_from_dict( diff --git a/chia/wallet/derivation_record.py b/chia/wallet/derivation_record.py index eec3776ff1ec..e470b386de69 100644 --- a/chia/wallet/derivation_record.py +++ b/chia/wallet/derivation_record.py @@ -28,3 +28,10 @@ class DerivationRecord: def pubkey(self) -> G1Element: assert isinstance(self._pubkey, G1Element) return self._pubkey + + @property + def pubkey_bytes(self) -> bytes: + if isinstance(self._pubkey, G1Element): + return bytes(self._pubkey) + else: + return self._pubkey diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 722e34da59ad..c5d7644a76f2 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -20,6 +20,7 @@ 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.observation_root import ObservationRoot from chia.wallet.coin_selection import select_coins from chia.wallet.conditions import ( AssertCoinAnnouncement, @@ -314,7 +315,7 @@ async def _generate_unsigned_transaction( return all_spends - def puzzle_for_pk(self, pubkey: G1Element) -> Program: + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: raise NotImplementedError("vault wallet") async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: @@ -438,7 +439,7 @@ async def get_puzzle(self, new: bool) -> Program: ) return puzzle - def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: + def puzzle_hash_for_pk(self, pubkey: ObservationRoot) -> bytes32: raise ValueError("This won't work") def require_derivation_paths(self) -> bool: diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index e280524a7622..aaceb6250e30 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -17,6 +17,7 @@ from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 +from chia.util.observation_root import ObservationRoot from chia.util.streamable import Streamable from chia.wallet.coin_selection import select_coins from chia.wallet.conditions import ( @@ -169,10 +170,12 @@ async def get_pending_change_balance(self) -> uint64: def require_derivation_paths(self) -> bool: return True - def puzzle_for_pk(self, pubkey: G1Element) -> Program: + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: + assert isinstance(pubkey, G1Element), "Standard wallet cannot support non-BLS keys yet" return puzzle_for_pk(pubkey) - def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: + def puzzle_hash_for_pk(self, pubkey: ObservationRoot) -> bytes32: + assert isinstance(pubkey, G1Element), "Standard wallet cannot support non-BLS keys yet" return puzzle_hash_for_pk(pubkey) async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: @@ -397,6 +400,7 @@ async def sign_message(self, message: str, puzzle_hash: bytes32, mode: SigningMo # 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) + assert isinstance(private, PrivateKey) synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) synthetic_pk = synthetic_secret_key.get_g1() if mode == SigningMode.CHIP_0002_HEX_INPUT: @@ -557,7 +561,9 @@ async def path_hint_for_pubkey(self, pk: bytes) -> Optional[PathHint]: root_fingerprint: bytes = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") if index is None: # Pool wallet may have a secret key here - if self.wallet_state_manager.private_key is not None: + if self.wallet_state_manager.private_key is not None and isinstance( + self.wallet_state_manager.private_key, PrivateKey + ): for pool_wallet_index in range(MAX_POOL_WALLETS): try_owner_sk = master_sk_to_singleton_owner_sk( self.wallet_state_manager.private_key, uint32(pool_wallet_index) @@ -578,20 +584,20 @@ async def execute_signing_instructions( ) -> List[SigningResponse]: assert isinstance(self.wallet_state_manager.observation_root, G1Element) root_pubkey: G1Element = self.wallet_state_manager.observation_root - pk_lookup: Dict[int, G1Element] = ( - {root_pubkey.get_fingerprint(): root_pubkey} if self.wallet_state_manager.private_key is not None else {} - ) - sk_lookup: Dict[int, PrivateKey] = ( - {root_pubkey.get_fingerprint(): self.wallet_state_manager.get_master_private_key()} - if self.wallet_state_manager.private_key is not None - else {} - ) + pk_lookup: Dict[int, G1Element] = {} + sk_lookup: Dict[int, PrivateKey] = {} aggregate_responses_at_end: bool = True responses: List[SigningResponse] = [] # TODO: expand path hints and sum hints recursively (a sum hint can give a new key to path hint) # Next, expand our pubkey set with path hints if self.wallet_state_manager.private_key is not None: + root_secret_key = self.wallet_state_manager.get_master_private_key() + assert isinstance(root_secret_key, PrivateKey) + root_fingerprint = root_pubkey.get_fingerprint() + pk_lookup[root_fingerprint] = root_pubkey + sk_lookup[root_fingerprint] = root_secret_key + for path_hint in signing_instructions.key_hints.path_hints: if int.from_bytes(path_hint.root_fingerprint, "big") != root_pubkey.get_fingerprint(): if not partial_allowed: @@ -600,12 +606,10 @@ async def execute_signing_instructions( continue else: path = [int(step) for step in path_hint.path] - derive_child_sk = _derive_path(self.wallet_state_manager.get_master_private_key(), path) - derive_child_sk_unhardened = _derive_path_unhardened( - self.wallet_state_manager.get_master_private_key(), path - ) - derive_child_pk = derive_child_sk.get_g1() - derive_child_pk_unhardened = derive_child_sk_unhardened.get_g1() + derive_child_sk = _derive_path(root_secret_key, path) + derive_child_sk_unhardened = _derive_path_unhardened(root_secret_key, path) + derive_child_pk = derive_child_sk.public_key() + derive_child_pk_unhardened = derive_child_sk_unhardened.public_key() pk_lookup[derive_child_pk.get_fingerprint()] = derive_child_pk pk_lookup[derive_child_pk_unhardened.get_fingerprint()] = derive_child_pk_unhardened sk_lookup[derive_child_pk.get_fingerprint()] = derive_child_sk diff --git a/chia/wallet/wallet_protocol.py b/chia/wallet/wallet_protocol.py index c3e9b2b284d6..0c7b3cb3bc4a 100644 --- a/chia/wallet/wallet_protocol.py +++ b/chia/wallet/wallet_protocol.py @@ -12,6 +12,7 @@ from chia.types.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64, uint128 +from chia.util.observation_root import ObservationRoot from chia.wallet.conditions import Condition from chia.wallet.derivation_record import DerivationRecord from chia.wallet.nft_wallet.nft_info import NFTCoinInfo @@ -114,7 +115,7 @@ async def generate_signed_transaction( **kwargs: Unpack[GSTOptionalArgs], ) -> None: ... - def puzzle_for_pk(self, pubkey: G1Element) -> Program: ... + def puzzle_for_pk(self, pubkey: ObservationRoot) -> Program: ... async def puzzle_for_puzzle_hash(self, puzzle_hash: bytes32) -> Program: ... diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index 2afe00bd156b..fd99e19abe26 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -84,7 +84,7 @@ async def add_derivation_paths(self, records: List[DerivationRecord]) -> None: sql_records.append( ( record.index, - bytes(record._pubkey).hex(), + record.pubkey_bytes.hex(), record.puzzle_hash.hex(), record.wallet_type, record.wallet_id, diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 899b1ec25089..3120abf4ac6d 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -60,6 +60,7 @@ from chia.util.lru_cache import LRUCache from chia.util.observation_root import ObservationRoot from chia.util.path import path_from_root +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, UInt32Range, UInt64Range, VersionedBlob from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_info import CATCoinData, CATInfo, CRCATInfo @@ -202,7 +203,7 @@ class WalletStateManager: main_wallet: MainWalletProtocol wallets: Dict[uint32, WalletProtocol[Any]] - private_key: Optional[PrivateKey] + private_key: Optional[SecretInfo[Any]] observation_root: ObservationRoot trade_manager: TradeManager @@ -225,7 +226,7 @@ class WalletStateManager: @staticmethod async def create( - private_key: Optional[PrivateKey], + private_key: Optional[SecretInfo[Any]], config: Dict[str, Any], db_path: Path, constants: ConsensusConstants, @@ -293,7 +294,7 @@ async def create( else: self.observation_root = observation_root else: - calculated_root_public_key: G1Element = private_key.get_g1() + calculated_root_public_key: ObservationRoot = private_key.public_key() if observation_root is not None: assert observation_root == calculated_root_public_key self.observation_root = calculated_root_public_key @@ -392,13 +393,17 @@ def get_public_key_unhardened(self, index: uint32) -> G1Element: raise ValueError("Public key derivation is not supported for non-G1Element keys") return master_pk_to_wallet_pk_unhardened(self.observation_root, index) - async def get_private_key(self, puzzle_hash: bytes32) -> PrivateKey: + async def get_private_key(self, puzzle_hash: bytes32) -> SecretInfo[Any]: 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()}") + sk = self.get_master_private_key() + # This will need to work when other key types are derivable but for now we will just sanitize and move on + assert isinstance(sk, PrivateKey) if record.hardened: - return master_sk_to_wallet_sk(self.get_master_private_key(), record.index) - return master_sk_to_wallet_sk_unhardened(self.get_master_private_key(), record.index) + return master_sk_to_wallet_sk(sk, record.index) + + return master_sk_to_wallet_sk_unhardened(sk, record.index) async def get_public_key(self, puzzle_hash: bytes32) -> bytes: record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash) @@ -410,7 +415,7 @@ async def get_public_key(self, puzzle_hash: bytes32) -> bytes: pk_bytes = bytes(record._pubkey) return pk_bytes - def get_master_private_key(self) -> PrivateKey: + def get_master_private_key(self) -> SecretInfo[Any]: if self.private_key is None: # pragma: no cover raise ValueError("Wallet is currently in observer mode and access to private key is denied") @@ -485,16 +490,18 @@ async def create_more_puzzle_hashes( hardened_keys: Dict[int, G1Element] = {} unhardened_keys: Dict[int, G1Element] = {} - if self.private_key is not None: - # Hardened - intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) - for index in range(start_index, last_index): - hardened_keys[index] = _derive_path(intermediate_sk, [index]).get_g1() - # This function shoul work for other types of observation roots too # However to generalize this function beyond pubkeys is beyond the scope of current work # So we're just going to sanitize and move on assert isinstance(self.observation_root, G1Element) + if self.private_key is not None: + assert isinstance(self.private_key, PrivateKey) + + if self.private_key is not None: + # Hardened + intermediate_sk = master_sk_to_wallet_sk_intermediate(self.private_key) + for index in range(start_index, last_index): + hardened_keys[index] = _derive_path(intermediate_sk, [index]).public_key() # Unhardened intermediate_pk_un = master_pk_to_wallet_pk_unhardened_intermediate(self.observation_root) From 03243b0fef402e22e715846c7a52b7b111bf5ae4 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 13 Aug 2024 14:15:54 -0700 Subject: [PATCH 251/274] Add Secp256r1 to KeyTypes --- chia/_tests/core/util/test_keychain.py | 28 ++++++++++++++++++-------- chia/util/keychain.py | 20 ++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index 00d218bc9ea5..88a8497ad296 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -3,7 +3,7 @@ import json import random from dataclasses import dataclass, replace -from typing import Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple import importlib_resources import pytest @@ -21,6 +21,7 @@ KeychainSecretsMissing, ) from chia.util.ints import uint32 +from chia.util.key_types import Secp256r1PrivateKey from chia.util.keychain import ( Keychain, KeyData, @@ -33,6 +34,7 @@ mnemonic_to_seed, ) from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo from chia.wallet.vault.vault_root import VaultRoot @@ -522,17 +524,27 @@ async def test_delete_drops_labels(get_temp_keyring: Keychain, delete_all: bool) @pytest.mark.parametrize("key_type", [e.value for e in KeyTypes]) -def test_key_type_support(key_type: str) -> None: +@pytest.mark.parametrize("key_info", [_24keyinfo, _12keyinfo]) +def test_key_type_support(key_type: str, key_info: KeyInfo) -> None: """ The purpose of this test is to make sure that whenever KeyTypes is updated, all relevant functionality is also updated with it. """ launcher_id = bytes32(b"1" * 32) vault_root = VaultRoot(launcher_id) - generate_test_key_for_key_type: Dict[str, Tuple[int, bytes, ObservationRoot]] = { - KeyTypes.G1_ELEMENT.value: (G1Element().get_fingerprint(), bytes(G1Element()), G1Element()), - KeyTypes.VAULT_LAUNCHER.value: (uint32(vault_root.get_fingerprint()), vault_root.launcher_id, vault_root), + secp_sk = Secp256r1PrivateKey.from_seed(mnemonic_to_seed(key_info.mnemonic)) + secp_pk = secp_sk.public_key() + generate_test_key_for_key_type: Dict[str, Tuple[int, ObservationRoot, Optional[SecretInfo[Any]]]] = { + KeyTypes.G1_ELEMENT.value: ( + G1Element().get_fingerprint(), + G1Element(), + key_info.private_key, + ), + KeyTypes.SECP_256_R1.value: (secp_pk.get_fingerprint(), secp_pk, secp_sk), + KeyTypes.VAULT_LAUNCHER.value: (vault_root.get_fingerprint(), vault_root, None), } - obr_fingerprint, obr_bytes, obr = generate_test_key_for_key_type[key_type] - assert KeyData(uint32(obr_fingerprint), obr_bytes, None, None, key_type).observation_root == obr - assert KeyTypes.parse_observation_root(obr_bytes, KeyTypes(key_type)) == obr + obr_fingerprint, obr, secret_info = generate_test_key_for_key_type[key_type] + assert KeyData(uint32(obr_fingerprint), bytes(obr), None, None, key_type).observation_root == obr + assert KeyTypes.parse_observation_root(bytes(obr), KeyTypes(key_type)) == obr + if secret_info is not None: + assert KeyTypes.parse_secret_info(bytes(secret_info), KeyTypes(key_type)) == secret_info diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 9c312563de2c..34f3aa87c6f8 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -29,8 +29,10 @@ from chia.util.file_keyring import Key from chia.util.hash import std_hash from chia.util.ints import uint32 +from chia.util.key_types import Secp256r1PrivateKey, Secp256r1PublicKey from chia.util.keyring_wrapper import KeyringWrapper from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, streamable from chia.wallet.vault.vault_root import VaultRoot @@ -233,6 +235,7 @@ def mnemonic_str(self) -> str: class KeyTypes(str, Enum): G1_ELEMENT = "G1 Element" VAULT_LAUNCHER = "Vault Launcher" + SECP_256_R1 = "SECP256r1" @classmethod def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: @@ -240,16 +243,33 @@ def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTy return G1Element.from_bytes(pk_bytes) if key_type == cls.VAULT_LAUNCHER: return VaultRoot(pk_bytes) + elif key_type == cls.SECP_256_R1: + return Secp256r1PublicKey.from_bytes(pk_bytes) else: # pragma: no cover # mypy should prevent this from ever running raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") + @classmethod + def parse_secret_info(cls: Type[KeyTypes], sk_bytes: bytes, key_type: KeyTypes) -> SecretInfo[Any]: + if key_type == cls.G1_ELEMENT: + return PrivateKey.from_bytes(sk_bytes) + elif key_type == cls.SECP_256_R1: + return Secp256r1PrivateKey.from_bytes(sk_bytes) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info") + TYPES_TO_KEY_TYPES: Dict[Type[ObservationRoot], KeyTypes] = { G1Element: KeyTypes.G1_ELEMENT, VaultRoot: KeyTypes.VAULT_LAUNCHER, + Secp256r1PublicKey: KeyTypes.SECP_256_R1, } KEY_TYPES_TO_TYPES: Dict[KeyTypes, Type[ObservationRoot]] = {v: k for k, v in TYPES_TO_KEY_TYPES.items()} +PUBLIC_TYPES_TO_PRIVATE_TYPES: Dict[Type[ObservationRoot], Type[SecretInfo[Any]]] = { + G1Element: PrivateKey, + Secp256r1PublicKey: Secp256r1PrivateKey, +} @final From 82296e55372160e5fde906b56512face7bcd7e19 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 10 Jul 2024 14:46:34 -0700 Subject: [PATCH 252/274] Make ._get_key_data return `Optional[KeyData]` --- chia/util/keychain.py | 49 +++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 62b0ad1a9594..9f500a4f35c3 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -359,7 +359,7 @@ def __init__(self, user: Optional[str] = None, service: Optional[str] = None): self.keyring_wrapper = keyring_wrapper - def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: + def _get_key_data(self, index: int, include_secrets: bool = True) -> Optional[KeyData]: """ Returns the parsed keychain contents for a specific 'user' (key index). The content is represented by the class `KeyData`. @@ -369,32 +369,35 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> KeyData: if key is None or len(key.secret) == 0: raise KeychainUserNotFound(self.service, user) str_bytes = key.secret - pk = str_bytes - entropy = None - if len(str_bytes) == 32: - observation_root: ObservationRoot = VaultRoot.from_bytes(str_bytes) + + if key.metadata is None or key.metadata.get("type", KeyTypes.G1_ELEMENT.value) == KeyTypes.G1_ELEMENT.value: + pk_bytes: bytes = str_bytes[: G1Element.SIZE] + observation_root: ObservationRoot = G1Element.from_bytes(pk_bytes) fingerprint = observation_root.get_fingerprint() - key_type = KeyTypes.VAULT_LAUNCHER.value - elif len(str_bytes) == 48: + if len(str_bytes) == G1Element.SIZE + 32: + entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] + else: + entropy = None + + return KeyData( + fingerprint=uint32(fingerprint), + public_key=pk_bytes, + label=self.keyring_wrapper.keyring.get_label(fingerprint), + secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, + key_type=KeyTypes.G1_ELEMENT.value, + ) + elif key.metadata.get("type", KeyTypes.G1_ELEMENT.value) == KeyTypes.VAULT_LAUNCHER.value: observation_root = G1Element.from_bytes(str_bytes) fingerprint = observation_root.get_fingerprint() - key_type = KeyTypes.G1_ELEMENT.value - elif len(str_bytes) > G1Element.SIZE: - pk = str_bytes[: G1Element.SIZE] - observation_root = G1Element.from_bytes(pk) - fingerprint = observation_root.get_fingerprint() - entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] - key_type = KeyTypes.G1_ELEMENT.value + return KeyData( + fingerprint=uint32(fingerprint), + public_key=str_bytes, + label=self.keyring_wrapper.keyring.get_label(fingerprint), + secrets=None, + key_type=KeyTypes.VAULT_LAUNCHER.value, + ) else: - raise ValueError(f"Public key must be either 32 or 48 bytes, got {len(pk)} bytes") - - return KeyData( - fingerprint=uint32(fingerprint), - public_key=pk, - label=self.keyring_wrapper.keyring.get_label(fingerprint), - secrets=KeyDataSecrets.from_entropy(entropy) if include_secrets and entropy is not None else None, - key_type=key_type, - ) + return None def _get_free_private_key_index(self) -> int: """ From 58eb0a0eec06677474bd25c862b8d916bbb0d9df Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 11 Jul 2024 09:46:38 -0700 Subject: [PATCH 253/274] Generalize KeyDataSecrets --- chia/_tests/core/util/test_keychain.py | 8 +-- chia/util/keychain.py | 79 +++++++++++++++----------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index 88a8497ad296..0c6cf9bc1ebf 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -321,12 +321,12 @@ def test_key_data_without_secrets(key_info: KeyInfo) -> None: @pytest.mark.parametrize( "input_data, data_type", [ - ((_24keyinfo.mnemonic.split()[:-1], _24keyinfo.entropy, _24keyinfo.private_key), "mnemonic"), - ((_24keyinfo.mnemonic.split(), KeyDataSecrets.generate().entropy, _24keyinfo.private_key), "entropy"), - ((_24keyinfo.mnemonic.split(), _24keyinfo.entropy, KeyDataSecrets.generate().private_key), "private_key"), + ((mnemonic.split()[:-1], _24keyinfo.entropy, _24keyinfo.private_key), "mnemonic"), + ((mnemonic.split(), KeyDataSecrets.generate().entropy, _24keyinfo.private_key), "entropy"), + ((mnemonic.split(), _24keyinfo.entropy, KeyDataSecrets.generate().secret_info_bytes), "private_key"), ], ) -def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, PrivateKey], data_type: str) -> None: +def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, bytes], data_type: str) -> None: with pytest.raises(KeychainKeyDataMismatch, match=data_type): KeyDataSecrets(*input_data) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 9f500a4f35c3..3e04fb028c6c 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -190,13 +190,48 @@ def get_private_key_user(user: str, index: int) -> str: return f"wallet-{user}-{index}" +class KeyTypes(str, Enum): + G1_ELEMENT = "G1 Element" + VAULT_LAUNCHER = "Vault Launcher" + SECP_256_R1 = "SECP256r1" + + @classmethod + def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: + if key_type == cls.G1_ELEMENT: + return G1Element.from_bytes(pk_bytes) + if key_type == cls.VAULT_LAUNCHER: + return VaultRoot(pk_bytes) + elif key_type == cls.SECP_256_R1: + return Secp256r1PublicKey.from_bytes(pk_bytes) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") + + @classmethod + def parse_secret_info(cls: Type[KeyTypes], sk_bytes: bytes, key_type: KeyTypes) -> SecretInfo[Any]: + if key_type == cls.G1_ELEMENT: + return PrivateKey.from_bytes(sk_bytes) + elif key_type == cls.SECP_256_R1: + return Secp256r1PrivateKey.from_bytes(sk_bytes) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info") + + @final @streamable @dataclass(frozen=True) class KeyDataSecrets(Streamable): mnemonic: List[str] entropy: bytes - private_key: PrivateKey + secret_info_bytes: bytes + key_type: str = KeyTypes.G1_ELEMENT.value + + @property + def private_key(self) -> SecretInfo[ObservationRoot]: + return PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[KeyTypes(self.key_type)]].from_bytes( + self.secret_info_bytes + ) def __post_init__(self) -> None: # This is redundant if `from_*` methods are used but its to make sure there can't be an `KeyDataSecrets` @@ -209,15 +244,23 @@ def __post_init__(self) -> None: raise KeychainKeyDataMismatch("mnemonic") from e if bytes_from_mnemonic(mnemonic_str) != self.entropy: raise KeychainKeyDataMismatch("entropy") - if AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic_str)) != self.private_key: + if ( + PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[KeyTypes(self.key_type)]].from_seed( + mnemonic_to_seed(mnemonic_str) + ) + != self.private_key + ): raise KeychainKeyDataMismatch("private_key") @classmethod - def from_mnemonic(cls, mnemonic: str) -> KeyDataSecrets: + def from_mnemonic(cls, mnemonic: str, key_type: KeyTypes = KeyTypes.G1_ELEMENT) -> KeyDataSecrets: return cls( mnemonic=mnemonic.split(), entropy=bytes_from_mnemonic(mnemonic), - private_key=AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic)), + secret_info_bytes=bytes( + PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[key_type]].from_seed(mnemonic_to_seed(mnemonic)) + ), + key_type=key_type, ) @classmethod @@ -232,34 +275,6 @@ def mnemonic_str(self) -> str: return " ".join(self.mnemonic) -class KeyTypes(str, Enum): - G1_ELEMENT = "G1 Element" - VAULT_LAUNCHER = "Vault Launcher" - SECP_256_R1 = "SECP256r1" - - @classmethod - def parse_observation_root(cls: Type[KeyTypes], pk_bytes: bytes, key_type: KeyTypes) -> ObservationRoot: - if key_type == cls.G1_ELEMENT: - return G1Element.from_bytes(pk_bytes) - if key_type == cls.VAULT_LAUNCHER: - return VaultRoot(pk_bytes) - elif key_type == cls.SECP_256_R1: - return Secp256r1PublicKey.from_bytes(pk_bytes) - else: # pragma: no cover - # mypy should prevent this from ever running - raise RuntimeError("Not all key types have been handled in KeyTypes.parse_observation_root") - - @classmethod - def parse_secret_info(cls: Type[KeyTypes], sk_bytes: bytes, key_type: KeyTypes) -> SecretInfo[Any]: - if key_type == cls.G1_ELEMENT: - return PrivateKey.from_bytes(sk_bytes) - elif key_type == cls.SECP_256_R1: - return Secp256r1PrivateKey.from_bytes(sk_bytes) - else: # pragma: no cover - # mypy should prevent this from ever running - raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info") - - TYPES_TO_KEY_TYPES: Dict[Type[ObservationRoot], KeyTypes] = { G1Element: KeyTypes.G1_ELEMENT, VaultRoot: KeyTypes.VAULT_LAUNCHER, From de9f542e3141a25cdb2e3f4dbc2638fd08a60ffe Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 19 Jul 2024 14:09:45 -0700 Subject: [PATCH 254/274] Generalize rest of keychain --- chia/_tests/core/daemon/test_daemon.py | 4 +- chia/_tests/core/data_layer/test_data_rpc.py | 2 +- chia/_tests/core/util/test_keychain.py | 10 +-- chia/_tests/wallet/rpc/test_wallet_rpc.py | 4 +- chia/_tests/wallet/test_wallet_node.py | 12 +-- chia/cmds/init_funcs.py | 9 +- chia/cmds/keys.py | 13 +++ chia/cmds/keys_funcs.py | 51 +++++++---- chia/cmds/sim_funcs.py | 7 +- chia/daemon/keychain_proxy.py | 61 ++++++++----- chia/daemon/keychain_server.py | 4 +- chia/daemon/server.py | 11 ++- chia/farmer/farmer.py | 7 +- chia/plotting/check_plots.py | 4 +- chia/plotting/create_plots.py | 15 ++-- chia/rpc/wallet_rpc_api.py | 34 ++++--- chia/simulator/block_tools.py | 18 ++-- chia/simulator/simulator_test_tools.py | 3 +- chia/util/keychain.py | 94 +++++++++++++------- chia/wallet/wallet_node.py | 44 ++++++--- 20 files changed, 267 insertions(+), 140 deletions(-) diff --git a/chia/_tests/core/daemon/test_daemon.py b/chia/_tests/core/daemon/test_daemon.py index 0640867cd85b..348434d26e24 100644 --- a/chia/_tests/core/daemon/test_daemon.py +++ b/chia/_tests/core/daemon/test_daemon.py @@ -10,7 +10,7 @@ import pytest from aiohttp import WSMessage from aiohttp.web_ws import WebSocketResponse -from chia_rs import G1Element +from chia_rs import G1Element, PrivateKey from pytest_mock import MockerFixture from chia._tests.util.misc import Marks, datacases @@ -205,6 +205,8 @@ async def get_keys_for_plotting(self, request: Dict[str, Any]) -> Dict[str, Any] "hammer stable page grunt venture purse canyon discover " "egg vivid spare immune awake code announce message" ) +assert isinstance(test_key_data.private_key, PrivateKey) +assert isinstance(test_key_data_2.private_key, PrivateKey) success_response_data = { "success": True, diff --git a/chia/_tests/core/data_layer/test_data_rpc.py b/chia/_tests/core/data_layer/test_data_rpc.py index 176a99746bf4..da6252066f66 100644 --- a/chia/_tests/core/data_layer/test_data_rpc.py +++ b/chia/_tests/core/data_layer/test_data_rpc.py @@ -2373,7 +2373,7 @@ async def test_wallet_log_in_changes_active_fingerprint( mnemonic = create_mnemonic() assert wallet_rpc_api.service.local_keychain is not None private_key, _ = wallet_rpc_api.service.local_keychain.add_key(mnemonic_or_pk=mnemonic) - secondary_fingerprint: int = private_key.get_g1().get_fingerprint() + secondary_fingerprint: int = private_key.public_key().get_fingerprint() await wallet_rpc_api.log_in(request={"fingerprint": primary_fingerprint}) diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index 0c6cf9bc1ebf..922d983174ea 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -98,7 +98,7 @@ def test_basic_add_delete( entropy = bytes_from_mnemonic(mnemonic) assert bytes_to_mnemonic(entropy) == mnemonic mnemonic_2 = generate_mnemonic() - fingerprint_2 = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic_2)).get_g1().get_fingerprint() + fingerprint_2 = AugSchemeMPL.key_gen(mnemonic_to_seed(mnemonic_2)).public_key().get_fingerprint() # misspelled words in the mnemonic bad_mnemonic = mnemonic.split(" ") @@ -130,7 +130,7 @@ def test_basic_add_delete( seed_2 = mnemonic_to_seed(mnemonic) seed_key_2 = AugSchemeMPL.key_gen(seed_2) - kc.delete_key_by_fingerprint(seed_key_2.get_g1().get_fingerprint()) + kc.delete_key_by_fingerprint(seed_key_2.public_key().get_fingerprint()) assert kc._get_free_private_key_index() == 0 assert len(kc.get_all_private_keys()) == 1 @@ -201,7 +201,7 @@ def test_bip39_eip2333_test_vector(self, empty_temp_file_keyring: TempKeyring): tv_master_int = 8075452428075949470768183878078858156044736575259233735633523546099624838313 tv_child_int = 18507161868329770878190303689452715596635858303241878571348190917018711023613 assert master_sk == PrivateKey.from_bytes(tv_master_int.to_bytes(32, "big")) - child_sk = AugSchemeMPL.derive_child_sk(master_sk, 0) + child_sk = master_sk.derive_hardened(0) assert child_sk == PrivateKey.from_bytes(tv_child_int.to_bytes(32, "big")) def test_bip39_test_vectors(self): @@ -279,8 +279,8 @@ def test_key_data_generate(label: Optional[str]) -> None: key_data = KeyData.generate(label) assert key_data.private_key == AugSchemeMPL.key_gen(mnemonic_to_seed(key_data.mnemonic_str())) assert key_data.entropy == bytes_from_mnemonic(key_data.mnemonic_str()) - assert key_data.observation_root == key_data.private_key.get_g1() - assert key_data.fingerprint == key_data.private_key.get_g1().get_fingerprint() + assert key_data.observation_root == key_data.private_key.public_key() + assert key_data.fingerprint == key_data.private_key.public_key().get_fingerprint() assert key_data.label == label diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 79f17a57330f..61017ec58c0e 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -10,7 +10,7 @@ import aiosqlite import pytest -from chia_rs import G2Element +from chia_rs import G2Element, PrivateKey from chia._tests.util.time_out_assert import time_out_assert, time_out_assert_not_none from chia._tests.wallet.test_wallet_coin_store import ( @@ -1663,10 +1663,12 @@ async def _check_delete_key( sk = await wallet_node.get_key_for_fingerprint(farmer_fp, private=True) assert sk is not None + assert isinstance(sk, PrivateKey) farmer_ph = create_puzzlehash_for_pk(create_sk(sk, uint32(0)).get_g1()) sk = await wallet_node.get_key_for_fingerprint(pool_fp, private=True) assert sk is not None + assert isinstance(sk, PrivateKey) pool_ph = create_puzzlehash_for_pk(create_sk(sk, uint32(0)).get_g1()) with lock_and_load_config(wallet_node.root_path, "config.yaml") as test_config: diff --git a/chia/_tests/wallet/test_wallet_node.py b/chia/_tests/wallet/test_wallet_node.py index 63638a9fa594..8c93457bab25 100644 --- a/chia/_tests/wallet/test_wallet_node.py +++ b/chia/_tests/wallet/test_wallet_node.py @@ -42,7 +42,7 @@ async def test_get_private_key(root_path_populated_with_config: Path, get_temp_k config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) sk, _ = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() key = await node.get_key(fingerprint) @@ -58,7 +58,7 @@ async def test_get_private_key_default_key(root_path_populated_with_config: Path config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants, keychain) sk, _ = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() # Add a couple more keys keychain.add_key(generate_mnemonic()) @@ -165,7 +165,7 @@ def test_log_in(root_path_populated_with_config: Path, get_temp_keyring: Keychai config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) sk, _ = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() node.log_in(fingerprint) @@ -191,7 +191,7 @@ def patched_update_last_used_fingerprint(self: Self) -> None: config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) sk, _ = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() # Expect log_in to succeed, even though we can't write the last used fingerprint node.log_in(fingerprint) @@ -208,7 +208,7 @@ def test_log_out(root_path_populated_with_config: Path, get_temp_keyring: Keycha config = load_config(root_path, "config.yaml", "wallet") node = WalletNode(config, root_path, test_constants) sk, _ = keychain.add_key(generate_mnemonic()) - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() node.log_in(fingerprint) @@ -655,7 +655,7 @@ async def test_get_last_used_fingerprint_if_exists( assert await node.get_last_used_fingerprint_if_exists() is None sk_2, _ = await node.keychain_proxy.add_key(generate_mnemonic()) - fingerprint_2: int = sk_2.get_g1().get_fingerprint() + fingerprint_2: int = sk_2.public_key().get_fingerprint() node._close() await node._await_closed(shutting_down=False) diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index d1b8ac383137..d7016cbfa1cf 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional import yaml +from chia_rs import PrivateKey from chia.cmds.configure import configure from chia.consensus.coinbase import create_puzzlehash_for_pk @@ -64,13 +65,13 @@ def dict_add_new_default(updated: Dict[str, Any], default: Dict[str, Any], do_no def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: if keychain is None: keychain = Keychain() - all_sks = keychain.get_all_private_keys() + all_sks: List[PrivateKey] = [sk for sk, _ in keychain.get_all_private_keys() if isinstance(sk, PrivateKey)] if len(all_sks) == 0: print("No keys are present in the keychain. Generate them with 'chia keys generate'") return None with lock_and_load_config(new_root, "config.yaml") as config: - pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk, _ in all_sks] + pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk in all_sks] all_targets = [] stop_searching_for_farmer = "xch_target_address" not in config["farmer"] stop_searching_for_pool = "xch_target_address" not in config["pool"] @@ -79,7 +80,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: prefix = config["network_overrides"]["config"][selected]["address_prefix"] intermediates = {} - for sk, _ in all_sks: + for sk in all_sks: intermediates[bytes(sk)] = { "observer": master_sk_to_wallet_sk_unhardened_intermediate(sk), "non-observer": master_sk_to_wallet_sk_intermediate(sk), @@ -88,7 +89,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: for i in range(number_of_ph_to_search): if stop_searching_for_farmer and stop_searching_for_pool and i > 0: break - for sk, _ in all_sks: + for sk in all_sks: intermediate_n = intermediates[bytes(sk)]["non-observer"] intermediate_o = intermediates[bytes(sk)]["observer"] diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index 62f25c5131c0..e69debda9e73 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -3,6 +3,7 @@ from typing import Optional, Tuple import click +from chia_rs import PrivateKey from chia.cmds import options @@ -328,6 +329,10 @@ def search_cmd( if fingerprint is None and filename is not None: sk = resolve_derivation_master_key(filename) + if not isinstance(sk, PrivateKey): + print("Cannot derive from non-BLS keys") + return + found: bool = search_derive( ctx.obj["root_path"], fingerprint, @@ -379,6 +384,10 @@ def wallet_address_cmd( if fingerprint is None and filename is not None: sk = resolve_derivation_master_key(filename) + if not isinstance(sk, PrivateKey): + print("Cannot derive from non-BLS keys") + return + derive_wallet_address( ctx.obj["root_path"], fingerprint, index, count, prefix, non_observer_derivation, show_hd_path, sk ) @@ -450,6 +459,10 @@ def child_key_cmd( if fingerprint is None and filename is not None: sk = resolve_derivation_master_key(filename) + if not isinstance(sk, PrivateKey): + print("Cannot derive from non-BLS keys") + return + derive_child_key( fingerprint, key_type, diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 31d29173c50e..7617131619ae 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -26,6 +26,7 @@ mnemonic_to_seed, ) from chia.util.keyring_wrapper import KeyringWrapper +from chia.util.secret_info import SecretInfo from chia.wallet.derive_keys import ( master_pk_to_wallet_pk_unhardened, master_sk_to_farmer_sk, @@ -87,7 +88,7 @@ def add_key_info(mnemonic_or_pk: str, label: Optional[str]) -> None: try: if check_mnemonic_validity(mnemonic_or_pk): sk, _ = Keychain().add_key(mnemonic_or_pk, label, private=True) - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() print(f"Added private key with public key fingerprint {fingerprint}") else: pk, _ = Keychain().add_key(mnemonic_or_pk, label, private=False) @@ -177,7 +178,7 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: else: # pragma: no cover # TODO: Add test coverage once vault wallet exists key["observation_root"] = key_data.public_key.hex() - if sk is not None: + if sk is not None and isinstance(sk, PrivateKey): key["farmer_pk"] = bytes(master_sk_to_farmer_sk(sk).get_g1()).hex() key["pool_pk"] = bytes(master_sk_to_pool_sk(sk).get_g1()).hex() else: @@ -185,11 +186,12 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["pool_pk"] = None if isinstance(key_data.observation_root, G1Element): + assert isinstance(key_data, PrivateKey) if non_observer_derivation: if sk is None: first_wallet_pk: Optional[G1Element] = None else: - first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).get_g1() + first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).public_key() else: first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.observation_root, uint32(0)) @@ -203,13 +205,15 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: if show_mnemonic and sk is not None: key["master_sk"] = bytes(sk).hex() - key["farmer_sk"] = bytes(master_sk_to_farmer_sk(sk)).hex() - key["wallet_sk"] = bytes(master_sk_to_wallet_sk(sk, uint32(0))).hex() + if isinstance(sk, PrivateKey): + key["farmer_sk"] = bytes(master_sk_to_farmer_sk(sk)).hex() + key["wallet_sk"] = bytes(master_sk_to_wallet_sk(sk, uint32(0))).hex() key["mnemonic"] = bytes_to_mnemonic(key_data.entropy) else: key["master_sk"] = None - key["farmer_sk"] = None - key["wallet_sk"] = None + if isinstance(key_data.observation_root, G1Element): + key["farmer_sk"] = None + key["wallet_sk"] = None key["mnemonic"] = None return key @@ -306,15 +310,19 @@ class DerivationType(Enum): return (current_pk, current_sk, "m/" + "/".join(path) + "/") -def sign(message: str, private_key: PrivateKey, hd_path: str, as_bytes: bool, json_output: bool) -> None: - sk = derive_pk_and_sk_from_hd_path(private_key.get_g1(), hd_path, master_sk=private_key)[1] +def sign(message: str, private_key: SecretInfo[Any], hd_path: str, as_bytes: bool, json_output: bool) -> None: + if hd_path != "m": + if not isinstance(private_key, PrivateKey): + print("Cannot derive non-BLS keys") + return + sk = derive_pk_and_sk_from_hd_path(private_key.public_key(), hd_path, master_sk=private_key)[1] assert sk is not None data = bytes.fromhex(message) if as_bytes else bytes(message, "utf-8") signing_mode: SigningMode = ( SigningMode.BLS_MESSAGE_AUGMENTATION_HEX_INPUT if as_bytes else SigningMode.BLS_MESSAGE_AUGMENTATION_UTF8_INPUT ) - pubkey_hex: str = bytes(sk.get_g1()).hex() - signature_hex: str = bytes(AugSchemeMPL.sign(sk, data)).hex() + pubkey_hex: str = bytes(sk.public_key()).hex() + signature_hex: str = bytes(sk.sign(data)).hex() if json_output: print( json.dumps( @@ -511,7 +519,8 @@ def search_derive( if fingerprint is None and private_key is None: public_keys: List[G1Element] = Keychain().get_all_public_keys_of_type(G1Element) private_keys: List[Optional[PrivateKey]] = [ - data.private_key if data.secrets is not None else None for data in Keychain().get_keys(include_secrets=True) + data.private_key if data.secrets is not None and isinstance(data.private_key, PrivateKey) else None + for data in Keychain().get_keys(include_secrets=True) ] elif fingerprint is None: assert private_key is not None @@ -521,7 +530,13 @@ def search_derive( master_key_data = Keychain().get_key(fingerprint, include_secrets=True) if isinstance(master_key_data.observation_root, G1Element): public_keys = [master_key_data.observation_root] - private_keys = [master_key_data.private_key if master_key_data.secrets is not None else None] + private_keys = [ + ( + master_key_data.private_key + if master_key_data.secrets is not None and isinstance(master_key_data, PrivateKey) + else None + ) + ] else: # pragma: no cover # TODO: Add test coverage once vault wallet exists print("Cannot currently derive paths from non-BLS keys") @@ -666,6 +681,7 @@ def derive_wallet_address( print("Need a private key for non observer derivation of wallet addresses") return elif non_observer_derivation: + assert isinstance(key_data.private_key, PrivateKey) sk = key_data.private_key else: sk = None @@ -730,6 +746,7 @@ def derive_child_key( # TODO: Add coverage when vault wallet exists print("Cannot currently derive from non-BLS keys") return + assert isinstance(key_data.private_key, PrivateKey) current_pk: G1Element = key_data.observation_root current_sk: Optional[PrivateKey] = key_data.private_key if key_data.secrets is not None else None else: @@ -805,17 +822,17 @@ def derive_child_key( print(f"{key_type_str} private key {i}{hd_path}: {private_key_string_repr(sk)}") -def private_key_for_fingerprint(fingerprint: int) -> Optional[PrivateKey]: +def private_key_for_fingerprint(fingerprint: int) -> Optional[SecretInfo[Any]]: unlock_keyring() private_keys = Keychain().get_all_private_keys() for sk, _ in private_keys: - if sk.get_g1().get_fingerprint() == fingerprint: + if sk.public_key().get_fingerprint() == fingerprint: return sk return None -def get_private_key_with_fingerprint_or_prompt(fingerprint: Optional[int]) -> Optional[PrivateKey]: +def get_private_key_with_fingerprint_or_prompt(fingerprint: Optional[int]) -> Optional[SecretInfo[Any]]: """ Get a private key with the specified fingerprint. If fingerprint is not specified, prompt the user to select a key. @@ -857,7 +874,7 @@ def private_key_from_mnemonic_seed_file(filename: Path) -> PrivateKey: return AugSchemeMPL.key_gen(seed) -def resolve_derivation_master_key(fingerprint_or_filename: Optional[Union[int, str, Path]]) -> PrivateKey: +def resolve_derivation_master_key(fingerprint_or_filename: Optional[Union[int, str, Path]]) -> SecretInfo[Any]: """ Given a key fingerprint of file containing a mnemonic seed, return the private key. """ diff --git a/chia/cmds/sim_funcs.py b/chia/cmds/sim_funcs.py index 888b9aee9bb5..d14819180cb3 100644 --- a/chia/cmds/sim_funcs.py +++ b/chia/cmds/sim_funcs.py @@ -35,6 +35,8 @@ def get_ph_from_fingerprint(fingerprint: int, key_id: int = 1) -> bytes32: if priv_key_and_entropy is None: raise Exception("Fingerprint not found") private_key = priv_key_and_entropy[0] + if not isinstance(private_key, PrivateKey): + raise ValueError("Can only use BLS keys for the simulator") sk_for_wallet_id: PrivateKey = master_sk_to_wallet_sk(private_key, uint32(key_id)) puzzle_hash: bytes32 = create_puzzlehash_for_pk(sk_for_wallet_id.get_g1()) return puzzle_hash @@ -153,6 +155,9 @@ def display_key_info(fingerprint: int, prefix: str) -> None: print(f"Fingerprint {fingerprint} not found") return sk, seed = private_key_and_seed + if not isinstance(sk, PrivateKey): + print("Can only use BLS keys for the simulator") + return print("\nFingerprint:", sk.get_g1().get_fingerprint()) print("Master public key (m):", sk.get_g1()) print("Farmer public key (m/12381/8444/0/0):", master_sk_to_farmer_sk(sk).get_g1()) @@ -179,7 +184,7 @@ def generate_and_return_fingerprint(mnemonic: Optional[str] = None) -> int: mnemonic = generate_mnemonic() try: sk, _ = Keychain().add_key(mnemonic, None) - fingerprint: int = sk.get_g1().get_fingerprint() + fingerprint: int = sk.public_key().get_fingerprint() except KeychainFingerprintExists as e: fingerprint = e.fingerprint print(f"Fingerprint: {fingerprint} for provided private key already exists.") diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 0ae800fd3349..180d6023d3b2 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Literal, Optional, Tuple, Union, overload from aiohttp import ClientConnectorError, ClientSession -from chia_rs import AugSchemeMPL, PrivateKey +from chia_rs import AugSchemeMPL from chia.cmds.init_funcs import check_keys from chia.daemon.client import DaemonProxy @@ -32,6 +32,7 @@ ) from chia.util.keychain import Keychain, KeyData, KeyTypes, bytes_to_mnemonic, mnemonic_to_seed from chia.util.observation_root import ObservationRoot +from chia.util.secret_info import SecretInfo from chia.util.ws_message import WsRpcMessage @@ -172,15 +173,23 @@ def handle_error(self, response: WsRpcMessage) -> None: raise Exception(f"{error}") @overload - async def add_key(self, mnemonic_or_pk: str) -> Tuple[PrivateKey, KeyTypes]: ... + async def add_key(self, mnemonic_or_pk: str) -> Tuple[SecretInfo[Any], KeyTypes]: ... @overload - async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[PrivateKey, KeyTypes]: ... + async def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[SecretInfo[Any], KeyTypes]: ... + + @overload + async def add_key(self, mnemonic_or_pk: str, *, key_type: KeyTypes) -> Tuple[SecretInfo[Any], KeyTypes]: ... @overload async def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True] - ) -> Tuple[PrivateKey, KeyTypes]: ... + ) -> Tuple[SecretInfo[Any], KeyTypes]: ... + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True], key_type: KeyTypes + ) -> Tuple[SecretInfo[Any], KeyTypes]: ... @overload async def add_key( @@ -188,18 +197,22 @@ async def add_key( ) -> Tuple[ObservationRoot, KeyTypes]: ... @overload - async def add_key( - self, mnemonic_or_pk: str, label: Optional[str], private: bool - ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: ... + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False], key_type: KeyTypes + ) -> Tuple[ObservationRoot, KeyTypes]: ... - async def add_key( - self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True - ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: + # Mypy doesn't seem to think the implementation handles a coupld of the cases above. As far as I can see, it does + # and I can't figure out why mypy thinks it doesn't -Quex + async def add_key( # type: ignore[misc] + self, + mnemonic_or_pk: str, + label: Optional[str] = None, + private: bool = True, + key_type: KeyTypes = KeyTypes.G1_ELEMENT, + ) -> Tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: """ Forwards to Keychain.add_key() """ - key: Union[PrivateKey, ObservationRoot] - key_type: KeyTypes if self.use_local_keychain(): key, key_type = self.keychain.add_key(mnemonic_or_pk, label, private) else: @@ -210,7 +223,7 @@ async def add_key( key_type = KeyTypes(response["data"]["key_type"]) if private: seed = mnemonic_to_seed(mnemonic_or_pk) - key = AugSchemeMPL.key_gen(seed) + key = KeyTypes.parse_secret_info_from_seed(seed, key_type) else: key = KeyTypes.parse_observation_root(hexstr_to_bytes(mnemonic_or_pk), key_type) else: @@ -259,11 +272,11 @@ async def delete_key_by_fingerprint(self, fingerprint: int) -> None: if not success: self.handle_error(response) - async def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: + async def get_all_private_keys(self) -> List[Tuple[SecretInfo[Any], bytes]]: """ Forwards to Keychain.get_all_private_keys() """ - keys: List[Tuple[PrivateKey, bytes]] = [] + keys: List[Tuple[SecretInfo[Any], bytes]] = [] if self.use_local_keychain(): keys = self.keychain.get_all_private_keys() else: @@ -296,17 +309,17 @@ async def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: return keys - async def get_first_private_key(self) -> Optional[PrivateKey]: + async def get_first_private_key(self, key_type: Optional[KeyTypes] = None) -> Optional[SecretInfo[Any]]: """ Forwards to Keychain.get_first_private_key() """ - key: Optional[PrivateKey] = None + key: Optional[SecretInfo[Any]] = None if self.use_local_keychain(): - sk_ent = self.keychain.get_first_private_key() + sk_ent = self.keychain.get_first_private_key(key_type=key_type) if sk_ent: key = sk_ent[0] else: - response, success = await self.get_response_for_request("get_first_private_key", {}) + response, success = await self.get_response_for_request("get_first_private_key", {"type": key_type}) if success: private_key = response["data"].get("private_key", None) if private_key is None: @@ -335,12 +348,12 @@ async def get_first_private_key(self) -> Optional[PrivateKey]: return key @overload - async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]: ... + async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[SecretInfo[Any]]: ... @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: Literal[True] - ) -> Optional[PrivateKey]: ... + ) -> Optional[SecretInfo[Any]]: ... @overload async def get_key_for_fingerprint( @@ -350,15 +363,15 @@ async def get_key_for_fingerprint( @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool - ) -> Optional[Union[PrivateKey, ObservationRoot]]: ... + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool = True - ) -> Optional[Union[PrivateKey, ObservationRoot]]: + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: """ Locates and returns a private key matching the provided fingerprint """ - key: Optional[Union[PrivateKey, ObservationRoot]] = None + key: Optional[Union[SecretInfo[Any], ObservationRoot]] = None if self.use_local_keychain(): keys = self.keychain.get_keys(include_secrets=private) if len(keys) == 0: diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index d4876eff9e06..5db2ac1726e8 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -321,7 +321,7 @@ async def get_all_private_keys(self, request: Dict[str, Any]) -> Dict[str, Any]: private_keys = self.get_keychain_for_request(request).get_all_private_keys() for sk, entropy in private_keys: - all_keys.append({"pk": bytes(sk.get_g1()).hex(), "entropy": entropy.hex()}) + all_keys.append({"pk": bytes(sk.public_key()).hex(), "entropy": entropy.hex()}) return {"success": True, "private_keys": all_keys} @@ -334,7 +334,7 @@ async def get_first_private_key(self, request: Dict[str, Any]) -> Dict[str, Any] if sk_ent is None: return {"success": False, "error": KEYCHAIN_ERR_NO_KEYS} - pk_str = bytes(sk_ent[0].get_g1()).hex() + pk_str = bytes(sk_ent[0].public_key()).hex() ent_str = sk_ent[1].hex() key = {"pk": pk_str, "entropy": ent_str} diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 00a3b94a47c2..4c8de237a911 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -20,7 +20,7 @@ from types import FrameType from typing import Any, AsyncIterator, Dict, List, Optional, Set, TextIO, Tuple -from chia_rs import G1Element +from chia_rs import G1Element, PrivateKey from typing_extensions import Protocol from chia import __version__ @@ -662,7 +662,9 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict wallet_addresses_by_fingerprint = {} for key in keys: - if not isinstance(key.observation_root, G1Element): + if not isinstance(key.observation_root, G1Element) or ( + key.secrets is not None and not isinstance(key.private_key, PrivateKey) + ): continue # pragma: no cover address_entries = [] @@ -672,7 +674,8 @@ async def get_wallet_addresses(self, websocket: WebSocketResponse, request: Dict for i in range(index, index + count): if non_observer_derivation: - sk = master_sk_to_wallet_sk(key.private_key, uint32(i)) + # This ifs above are too complex for mypy but do rule out the possibility of a non-PrivateKey here + sk = master_sk_to_wallet_sk(key.private_key, uint32(i)) # type: ignore[arg-type] pk = sk.get_g1() else: pk = master_pk_to_wallet_pk_unhardened(key.observation_root, uint32(i)) @@ -697,7 +700,7 @@ async def get_keys_for_plotting(self, websocket: WebSocketResponse, request: Dic keys_for_plot: Dict[uint32, Any] = {} for key in keys: - if key.secrets is None: + if key.secrets is None or not isinstance(key.private_key, PrivateKey): continue sk = key.private_key farmer_public_key: G1Element = master_sk_to_farmer_sk(sk).get_g1() diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 5d9a6c7c5ed9..c695db8176ab 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -231,7 +231,12 @@ async def ensure_keychain_proxy(self) -> KeychainProxy: async def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: keychain_proxy = await self.ensure_keychain_proxy() - return await keychain_proxy.get_all_private_keys() + all_keys = [] + for key, entropy in await keychain_proxy.get_all_private_keys(): + if not isinstance(key, PrivateKey): + continue + all_keys.append((key, entropy)) + return all_keys async def setup_keys(self) -> bool: no_keys_error_str = "No keys exist. Please run 'chia keys generate' or open the UI." diff --git a/chia/plotting/check_plots.py b/chia/plotting/check_plots.py index b374e1c7ec79..b34de14b0029 100644 --- a/chia/plotting/check_plots.py +++ b/chia/plotting/check_plots.py @@ -8,7 +8,7 @@ from time import sleep, time from typing import List, Optional -from chia_rs import G1Element +from chia_rs import G1Element, PrivateKey from chiapos import Verifier from chia.plotting.manager import PlotManager @@ -119,7 +119,7 @@ def check_plots( # for keychain access, KeychainProxy/connect_to_keychain should be used instead of Keychain. kc: Keychain = Keychain() plot_manager.set_public_keys( - [master_sk_to_farmer_sk(sk).get_g1() for sk, _ in kc.get_all_private_keys()], + [master_sk_to_farmer_sk(sk).get_g1() for sk, _ in kc.get_all_private_keys() if isinstance(sk, PrivateKey)], [G1Element.from_bytes(bytes.fromhex(pk)) for pk in config["farmer"]["pool_public_keys"]], ) plot_manager.start_refreshing() diff --git a/chia/plotting/create_plots.py b/chia/plotting/create_plots.py index c6321d792cb5..a1639a826e2e 100644 --- a/chia/plotting/create_plots.py +++ b/chia/plotting/create_plots.py @@ -3,7 +3,7 @@ import logging from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from chia_rs import AugSchemeMPL, G1Element, PrivateKey from chiapos import DiskPlotter @@ -17,7 +17,8 @@ ) from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash -from chia.util.keychain import Keychain +from chia.util.keychain import Keychain, KeyTypes +from chia.util.secret_info import SecretInfo from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_local_sk, master_sk_to_pool_sk log = logging.getLogger(__name__) @@ -95,25 +96,27 @@ async def resolve(self) -> PlotKeys: return self.resolved_keys async def get_sk(self, keychain_proxy: Optional[KeychainProxy] = None) -> Optional[PrivateKey]: - sk: Optional[PrivateKey] = None + sk: Optional[SecretInfo[Any]] = None if keychain_proxy: try: if self.alt_fingerprint is not None: sk = await keychain_proxy.get_key_for_fingerprint(self.alt_fingerprint) else: - sk = await keychain_proxy.get_first_private_key() + sk = await keychain_proxy.get_first_private_key(KeyTypes.G1_ELEMENT) except Exception as e: log.error(f"Keychain proxy failed with error: {e}") else: - sk_ent: Optional[Tuple[PrivateKey, bytes]] = None + sk_ent: Optional[Tuple[SecretInfo[Any], bytes]] = None keychain: Keychain = Keychain() if self.alt_fingerprint is not None: sk_ent = keychain.get_private_key_by_fingerprint(self.alt_fingerprint) else: - sk_ent = keychain.get_first_private_key() + sk_ent = keychain.get_first_private_key(KeyTypes.G1_ELEMENT) if sk_ent: sk = sk_ent[0] + if not isinstance(sk, PrivateKey): + raise ValueError("Cannot create plots with non-BLS key") return sk async def get_farmer_public_key(self, keychain_proxy: Optional[KeychainProxy] = None) -> G1Element: diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index dd2e25fcd3bb..757e857d2132 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -50,8 +50,9 @@ from chia.util.errors import KeychainIsLocked from chia.util.hash import std_hash from chia.util.ints import uint16, uint32, uint64 -from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic +from chia.util.keychain import KeyTypes, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, UInt32Range, streamable from chia.util.ws_message import WsRpcMessage, create_payload_dict from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS @@ -406,7 +407,8 @@ async def get_logged_in_fingerprint(self, request: Dict[str, Any]) -> EndpointRe async def get_public_keys(self, request: Dict[str, Any]) -> EndpointResult: try: fingerprints = [ - sk.get_g1().get_fingerprint() for (sk, seed) in await self.service.keychain_proxy.get_all_private_keys() + sk.public_key().get_fingerprint() + for (sk, seed) in await self.service.keychain_proxy.get_all_private_keys() ] except KeychainIsLocked: return {"keyring_is_locked": True} @@ -418,11 +420,11 @@ async def get_public_keys(self, request: Dict[str, Any]) -> EndpointResult: else: return {"public_key_fingerprints": fingerprints} - async def _get_private_key(self, fingerprint: int) -> Tuple[Optional[PrivateKey], Optional[bytes]]: + async def _get_private_key(self, fingerprint: int) -> Tuple[Optional[SecretInfo[Any]], 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: + if sk.public_key().get_fingerprint() == fingerprint: return sk, seed except Exception as e: log.error(f"Failed to get private key by fingerprint: {e}") @@ -433,16 +435,19 @@ async def get_private_key(self, request: Dict[str, Any]) -> EndpointResult: 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 { + response = { "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(), + "pk": bytes(sk.public_key()).hex(), "seed": s, }, } + if isinstance(sk, PrivateKey): + response["private_key"] = { + "farmer_pk": bytes(master_sk_to_farmer_sk(sk).get_g1()).hex(), + "pool_pk": bytes(master_sk_to_pool_sk(sk).get_g1()).hex(), + } return {"success": False, "private_key": {"fingerprint": fingerprint}} async def generate_mnemonic(self, request: Dict[str, Any]) -> EndpointResult: @@ -455,7 +460,9 @@ async def add_key(self, request: Dict[str, Any]) -> EndpointResult: # Adding a key from 24 word mnemonic mnemonic = request["mnemonic"] try: - sk, _ = await self.service.keychain_proxy.add_key(" ".join(mnemonic)) + sk, _ = await self.service.keychain_proxy.add_key( + " ".join(mnemonic), KeyTypes(request["key_type"]) if "key_type" in request else None + ) except KeyError as e: return { "success": False, @@ -465,7 +472,7 @@ async def add_key(self, request: Dict[str, Any]) -> EndpointResult: except Exception as e: return {"success": False, "error": str(e)} - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() await self._stop_wallet() # Makes sure the new key is added to config properly @@ -549,9 +556,10 @@ async def check_delete_key(self, request: Dict[str, Any]) -> EndpointResult: 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 isinstance(sk, PrivateKey): + 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() diff --git a/chia/simulator/block_tools.py b/chia/simulator/block_tools.py index eb3ec0d5eba2..3a5b4fceb984 100644 --- a/chia/simulator/block_tools.py +++ b/chia/simulator/block_tools.py @@ -316,20 +316,22 @@ async def setup_keys(self, fingerprint: Optional[int] = None, reward_ph: Optiona await keychain_proxy.delete_all_keys() self.farmer_master_sk_entropy = std_hash(b"block_tools farmer key") # both entropies are only used here self.pool_master_sk_entropy = std_hash(b"block_tools pool key") - self.farmer_master_sk = ( - await keychain_proxy.add_key(bytes_to_mnemonic(self.farmer_master_sk_entropy)) - )[0] - self.pool_master_sk = ( + farmer_master_sk = (await keychain_proxy.add_key(bytes_to_mnemonic(self.farmer_master_sk_entropy)))[0] + assert isinstance(farmer_master_sk, PrivateKey) + self.farmer_master_sk = farmer_master_sk + pool_master_sk = ( await keychain_proxy.add_key( bytes_to_mnemonic(self.pool_master_sk_entropy), ) )[0] + assert isinstance(pool_master_sk, PrivateKey) + self.pool_master_sk = pool_master_sk else: sk = await keychain_proxy.get_key_for_fingerprint(fingerprint) - assert sk is not None + assert sk is not None and isinstance(sk, PrivateKey) self.farmer_master_sk = sk sk = await keychain_proxy.get_key_for_fingerprint(fingerprint) - assert sk is not None + assert sk is not None and isinstance(sk, PrivateKey) self.pool_master_sk = sk self.farmer_pk = master_sk_to_farmer_sk(self.farmer_master_sk).get_g1() @@ -346,7 +348,9 @@ async def setup_keys(self, fingerprint: Optional[int] = None, reward_ph: Optiona self.farmer_ph = reward_ph self.pool_ph = reward_ph if self.automated_testing: - self.all_sks: List[PrivateKey] = [sk for sk, _ in await keychain_proxy.get_all_private_keys()] + self.all_sks: List[PrivateKey] = [ + sk for sk, _ in await keychain_proxy.get_all_private_keys() if isinstance(sk, PrivateKey) + ] else: self.all_sks = [self.farmer_master_sk] # we only want to include plots under the same fingerprint self.pool_pubkeys: List[G1Element] = [master_sk_to_pool_sk(sk).get_g1() for sk in self.all_sks] diff --git a/chia/simulator/simulator_test_tools.py b/chia/simulator/simulator_test_tools.py index 453fb45cb590..152af64f6cdb 100644 --- a/chia/simulator/simulator_test_tools.py +++ b/chia/simulator/simulator_test_tools.py @@ -45,7 +45,7 @@ def mnemonic_fingerprint(keychain: Keychain) -> Tuple[str, int]: sk, _ = keychain.add_key(mnemonic) except KeychainFingerprintExists: pass - fingerprint = sk.get_g1().get_fingerprint() + fingerprint = sk.public_key().get_fingerprint() return mnemonic, fingerprint @@ -54,6 +54,7 @@ def get_puzzle_hash_from_key(keychain: Keychain, fingerprint: int, key_id: int = if priv_key_and_entropy is None: raise Exception("Fingerprint not found") private_key = priv_key_and_entropy[0] + assert isinstance(private_key, PrivateKey) sk_for_wallet_id: PrivateKey = master_sk_to_wallet_sk(private_key, uint32(key_id)) puzzle_hash: bytes32 = create_puzzlehash_for_pk(sk_for_wallet_id.get_g1()) return puzzle_hash diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 3e04fb028c6c..5402ebbc083f 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -217,6 +217,16 @@ def parse_secret_info(cls: Type[KeyTypes], sk_bytes: bytes, key_type: KeyTypes) # mypy should prevent this from ever running raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info") + @classmethod + def parse_secret_info_from_seed(cls: Type[KeyTypes], seed: bytes, key_type: KeyTypes) -> SecretInfo[Any]: + if key_type == cls.G1_ELEMENT: + return PrivateKey.from_seed(seed) + elif key_type == cls.SECP_256_R1: + return Secp256r1PrivateKey.from_seed(seed) + else: # pragma: no cover + # mypy should prevent this from ever running + raise RuntimeError("Not all key types have been handled in KeyTypes.parse_secret_info_from_seed") + @final @streamable @@ -228,7 +238,7 @@ class KeyDataSecrets(Streamable): key_type: str = KeyTypes.G1_ELEMENT.value @property - def private_key(self) -> SecretInfo[ObservationRoot]: + def private_key(self) -> SecretInfo[Any]: return PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[KeyTypes(self.key_type)]].from_bytes( self.secret_info_bytes ) @@ -304,7 +314,7 @@ def observation_root(self) -> ObservationRoot: def __post_init__(self) -> None: # This is redundant if `from_*` methods are used but its to make sure there can't be an `KeyData` instance with # an attribute mismatch for calculated cached values. Should be ok since we don't handle a lot of keys here. - if self.secrets is not None and self.observation_root != self.private_key.get_g1(): + if self.secrets is not None and self.observation_root != self.private_key.public_key(): raise KeychainKeyDataMismatch("public_key") if uint32(self.observation_root.get_fingerprint()) != self.fingerprint: raise KeychainKeyDataMismatch("fingerprint") @@ -346,7 +356,7 @@ def entropy(self) -> bytes: return self.secrets.entropy @property - def private_key(self) -> PrivateKey: + def private_key(self) -> SecretInfo[Any]: if self.secrets is None: raise KeychainSecretsMissing() return self.secrets.private_key @@ -354,9 +364,9 @@ def private_key(self) -> PrivateKey: class Keychain: """ - The keychain stores two types of keys: private keys, which are PrivateKeys from blspy, + The keychain stores two types of keys: private keys, which are SecretInfos, and private key seeds, which are bytes objects that are used as a seed to construct - PrivateKeys. Private key seeds are converted to mnemonics when shown to users. + SecretInfos. Private key seeds are converted to mnemonics when shown to users. Both types of keys are stored as hex strings in the python keyring, and the implementation of the keyring depends on OS. Both types of keys can be added, and get_private_keys returns a @@ -428,15 +438,21 @@ def _get_free_private_key_index(self) -> int: # pylint requires these NotImplementedErrors for some reason @overload - def add_key(self, mnemonic_or_pk: str) -> Tuple[PrivateKey, KeyTypes]: + def add_key(self, mnemonic_or_pk: str) -> Tuple[SecretInfo[Any], KeyTypes]: raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[PrivateKey, KeyTypes]: + def add_key(self, mnemonic_or_pk: str, label: Optional[str]) -> Tuple[SecretInfo[Any], KeyTypes]: raise NotImplementedError() # pragma: no cover @overload - def add_key(self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True]) -> Tuple[PrivateKey, KeyTypes]: + def add_key(self, mnemonic_or_pk: str, *, key_type: KeyTypes) -> Tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True] + ) -> Tuple[SecretInfo[Any], KeyTypes]: raise NotImplementedError() # pragma: no cover @overload @@ -448,26 +464,45 @@ def add_key( @overload def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: bool - ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: + ) -> Tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True], key_type: KeyTypes + ) -> Tuple[SecretInfo[Any], KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False], key_type: KeyTypes + ) -> Tuple[ObservationRoot, KeyTypes]: + raise NotImplementedError() # pragma: no cover + + @overload + def add_key( + self, mnemonic_or_pk: str, label: Optional[str], private: bool, key_type: KeyTypes + ) -> Tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: raise NotImplementedError() # pragma: no cover def add_key( - self, mnemonic_or_pk: str, label: Optional[str] = None, private: bool = True - ) -> Tuple[Union[PrivateKey, ObservationRoot], KeyTypes]: + self, + mnemonic_or_pk: str, + label: Optional[str] = None, + private: bool = True, + key_type: KeyTypes = KeyTypes.G1_ELEMENT, + ) -> Tuple[Union[SecretInfo[Any], ObservationRoot], KeyTypes]: """ Adds a key to the keychain. The keychain itself will store the public key, and the entropy bytes (if given), but not the passphrase. """ - key: Union[PrivateKey, ObservationRoot] - key_type: KeyTypes + key: Union[SecretInfo[Any], ObservationRoot] if private: seed = mnemonic_to_seed(mnemonic_or_pk) entropy = bytes_from_mnemonic(mnemonic_or_pk) index = self._get_free_private_key_index() - key = AugSchemeMPL.key_gen(seed) - key_type = KeyTypes.G1_ELEMENT - assert isinstance(key, PrivateKey) - pk = key.get_g1() + key = KeyTypes.parse_secret_info_from_seed(seed, key_type) + pk = key.public_key() key_data = Key(bytes(pk) + entropy) fingerprint = pk.get_fingerprint() else: @@ -478,14 +513,7 @@ def add_key( pk_bytes = bytes(convertbits(data, 5, 8, False)) else: pk_bytes = hexstr_to_bytes(mnemonic_or_pk) - if len(pk_bytes) == 48: - key = G1Element.from_bytes(pk_bytes) - key_type = KeyTypes.G1_ELEMENT - elif len(pk_bytes) == 32: - key = VaultRoot(pk_bytes) - key_type = KeyTypes.VAULT_LAUNCHER - else: - raise ValueError(f"Cannot identify type of pubkey {mnemonic_or_pk}") # pragma: no cover + key = KeyTypes.parse_observation_root(pk_bytes, key_type) key_data = Key(pk_bytes) fingerprint = key.get_fingerprint() @@ -537,15 +565,17 @@ def _iterate_through_key_datas( pass return None - def get_first_private_key(self) -> Optional[Tuple[PrivateKey, bytes]]: + def get_first_private_key(self, key_type: Optional[KeyTypes] = None) -> Optional[Tuple[SecretInfo[Any], bytes]]: """ Returns the first key in the keychain that has one of the passed in passphrases. """ for key_data in self._iterate_through_key_datas(skip_public_only=True): + if key_type is not None and key_data.key_type != key_type.value: + continue return key_data.private_key, key_data.entropy return None - def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[Tuple[PrivateKey, bytes]]: + def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[Tuple[SecretInfo[Any], bytes]]: """ Return first private key which have the given public key fingerprint. """ @@ -554,12 +584,12 @@ def get_private_key_by_fingerprint(self, fingerprint: int) -> Optional[Tuple[Pri return key_data.private_key, key_data.entropy return None - def get_all_private_keys(self) -> List[Tuple[PrivateKey, bytes]]: + def get_all_private_keys(self) -> List[Tuple[SecretInfo[Any], bytes]]: """ Returns all private keys which can be retrieved, with the given passphrases. A tuple of key, and entropy bytes (i.e. mnemonic) is returned for each key. """ - all_keys: List[Tuple[PrivateKey, bytes]] = [] + all_keys: List[Tuple[SecretInfo[Any], bytes]] = [] for key_data in self._iterate_through_key_datas(skip_public_only=True): all_keys.append((key_data.private_key, key_data.entropy)) return all_keys @@ -608,7 +638,7 @@ def get_first_public_key(self) -> Optional[G1Element]: Returns the first public key. """ key_data = self.get_first_private_key() - return None if key_data is None else key_data[0].get_g1() + return None if key_data is None else key_data[0].public_key() def delete_key_by_fingerprint(self, fingerprint: int) -> int: """ @@ -634,11 +664,11 @@ def delete_key_by_fingerprint(self, fingerprint: int) -> int: pass return removed - def delete_keys(self, keys_to_delete: List[Tuple[PrivateKey, bytes]]) -> None: + def delete_keys(self, keys_to_delete: List[Tuple[SecretInfo[Any], bytes]]) -> None: """ Deletes all keys in the list. """ - remaining_fingerprints = {x[0].get_g1().get_fingerprint() for x in keys_to_delete} + remaining_fingerprints = {x[0].public_key().get_fingerprint() for x in keys_to_delete} remaining_removals = len(remaining_fingerprints) while len(remaining_fingerprints): key_to_delete = remaining_fingerprints.pop() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 05077f6427b0..a775fb53bcb3 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -69,6 +69,7 @@ from chia.util.observation_root import ObservationRoot from chia.util.path import path_from_root from chia.util.profiler import mem_profile_task, profile_task +from chia.util.secret_info import SecretInfo from chia.util.streamable import Streamable, streamable from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings from chia.wallet.transaction_record import TransactionRecord @@ -235,7 +236,7 @@ async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[ @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: Literal[True] - ) -> Optional[PrivateKey]: ... + ) -> Optional[SecretInfo[Any]]: ... @overload async def get_key_for_fingerprint( @@ -245,15 +246,15 @@ async def get_key_for_fingerprint( @overload async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool - ) -> Optional[Union[PrivateKey, ObservationRoot]]: ... + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... async def get_key_for_fingerprint( self, fingerprint: Optional[int], private: bool = False - ) -> Optional[Union[PrivateKey, ObservationRoot]]: + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: try: keychain_proxy = await self.ensure_keychain_proxy() # Returns first key if fingerprint is None - key: Optional[Union[PrivateKey, ObservationRoot]] = await keychain_proxy.get_key_for_fingerprint( + key: Optional[Union[SecretInfo[Any], ObservationRoot]] = await keychain_proxy.get_key_for_fingerprint( fingerprint, private=private ) except KeychainIsEmpty: @@ -272,25 +273,41 @@ async def get_key_for_fingerprint( return key + @overload + async def get_key(self, fingerprint: Optional[int]) -> Optional[ObservationRoot]: ... + + @overload + async def get_key(self, fingerprint: Optional[int], private: Literal[True]) -> Optional[SecretInfo[Any]]: ... + + @overload + async def get_key(self, fingerprint: Optional[int], private: Literal[False]) -> Optional[ObservationRoot]: ... + + @overload + async def get_key( + self, fingerprint: Optional[int], private: bool + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... + async def get_key( self, fingerprint: Optional[int], private: bool = True, find_a_default: bool = True - ) -> Optional[Union[PrivateKey, ObservationRoot]]: + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: """ Attempt to get the private key for the given fingerprint. If the fingerprint is None, get_key_for_fingerprint() will return the first private key. Similarly, if a key isn't returned for the provided fingerprint, the first key will be returned. """ - key: Optional[Union[PrivateKey, ObservationRoot]] = await self.get_key_for_fingerprint( + key: Optional[Union[SecretInfo[Any], ObservationRoot]] = await self.get_key_for_fingerprint( fingerprint, private=private ) if key is None and fingerprint is not None and find_a_default: key = await self.get_key_for_fingerprint(None, private=private) if key is not None: - if isinstance(key, PrivateKey): - fp = key.get_g1().get_fingerprint() + if private: + # Mypy can't understand that private being True both ensures SecretInfo[Any] and this branch + fp = key.public_key().get_fingerprint() # type: ignore[union-attr] else: - fp = key.get_fingerprint() + # Mypy can't understand that private being False both ensures ObservationRoot and this branch + fp = key.get_fingerprint() # type: ignore[union-attr] self.log.info(f"Using first key found (fingerprint: {fp})") return key @@ -424,9 +441,12 @@ async def _start_with_fingerprint( if private_key is None: observation_root = await self.get_key(fingerprint, private=False, find_a_default=False) else: - assert isinstance(private_key, PrivateKey) - observation_root = private_key.get_g1() - + if not isinstance(private_key, PrivateKey): + raise ValueError( + "Cannot start wallet with non-BLS public key." + "Try starting in observer mode first and adding private key by some other means." + ) + observation_root = private_key.public_key() if observation_root is None: private_key = await self.get_key(None, private=True, find_a_default=True) if private_key is not None: From 81ab94f35f645ca3187cc5f3ce13d924f0dbd681 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 13 Aug 2024 14:49:07 -0700 Subject: [PATCH 255/274] pre-commit --- chia/_tests/core/util/test_keychain.py | 8 ++++---- chia/_tests/wallet/test_wallet_node.py | 4 ++-- chia/wallet/wallet_node.py | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index 922d983174ea..d61ba37693f5 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -321,9 +321,9 @@ def test_key_data_without_secrets(key_info: KeyInfo) -> None: @pytest.mark.parametrize( "input_data, data_type", [ - ((mnemonic.split()[:-1], _24keyinfo.entropy, _24keyinfo.private_key), "mnemonic"), - ((mnemonic.split(), KeyDataSecrets.generate().entropy, _24keyinfo.private_key), "entropy"), - ((mnemonic.split(), _24keyinfo.entropy, KeyDataSecrets.generate().secret_info_bytes), "private_key"), + ((_24keyinfo.mnemonic.split()[:-1], _24keyinfo.entropy, _24keyinfo.private_key), "mnemonic"), + ((_24keyinfo.mnemonic.split(), KeyDataSecrets.generate().entropy, _24keyinfo.private_key), "entropy"), + ((_24keyinfo.mnemonic.split(), _24keyinfo.entropy, KeyDataSecrets.generate().secret_info_bytes), "private_key"), ], ) def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, bytes], data_type: str) -> None: @@ -339,7 +339,7 @@ def test_key_data_secrets_post_init(input_data: Tuple[List[str], bytes, bytes], _24keyinfo.fingerprint, bytes(G1Element()), None, - KeyDataSecrets(_24keyinfo.mnemonic.split(), _24keyinfo.entropy, _24keyinfo.private_key), + KeyDataSecrets(_24keyinfo.mnemonic.split(), _24keyinfo.entropy, bytes(_24keyinfo.private_key)), KeyTypes.G1_ELEMENT.value, ), "public_key", diff --git a/chia/_tests/wallet/test_wallet_node.py b/chia/_tests/wallet/test_wallet_node.py index 8c93457bab25..d27e7a363b57 100644 --- a/chia/_tests/wallet/test_wallet_node.py +++ b/chia/_tests/wallet/test_wallet_node.py @@ -750,7 +750,7 @@ async def restart_with_fingerprint(fingerprint: Optional[int]) -> None: initial_sk = wallet_node.wallet_state_manager.private_key - sk_2: PrivateKey = ( + sk_2 = ( await wallet_node.keychain_proxy.add_key( ( "cup smoke miss park baby say island tomorrow segment lava bitter easily settle gift " @@ -760,7 +760,7 @@ async def restart_with_fingerprint(fingerprint: Optional[int]) -> None: private=True, ) )[0] - fingerprint_2: int = sk_2.get_g1().get_fingerprint() + fingerprint_2: int = sk_2.public_key().get_fingerprint() await restart_with_fingerprint(fingerprint_2) assert wallet_node.wallet_state_manager.private_key == sk_2 diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index a775fb53bcb3..5174a12eb829 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -287,6 +287,11 @@ async def get_key( self, fingerprint: Optional[int], private: bool ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... + @overload + async def get_key( + self, fingerprint: Optional[int], private: bool, find_a_default: bool + ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: ... + async def get_key( self, fingerprint: Optional[int], private: bool = True, find_a_default: bool = True ) -> Optional[Union[SecretInfo[Any], ObservationRoot]]: From 6314fd9a09e59b4d3e2fc0b764f832784760b493 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 14 Aug 2024 07:19:30 -0700 Subject: [PATCH 256/274] use PKCS8 format --- chia/util/key_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/util/key_types.py b/chia/util/key_types.py index 4b6b84bd827e..d5c3815b974c 100644 --- a/chia/util/key_types.py +++ b/chia/util/key_types.py @@ -57,7 +57,7 @@ def __eq__(self, other: object) -> bool: def __bytes__(self) -> bytes: return self._private_key.private_bytes( encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.TraditionalOpenSSL, + format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) From 302e929169be8ad9ae2c5c07722e86f1ad4a23d7 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 14 Aug 2024 09:56:29 -0700 Subject: [PATCH 257/274] Test coverage --- chia/_tests/util/test_secp256r1.py | 9 ++++++++- chia/util/key_types.py | 12 +++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/chia/_tests/util/test_secp256r1.py b/chia/_tests/util/test_secp256r1.py index e1c36a43746a..0183c1e56fc6 100644 --- a/chia/_tests/util/test_secp256r1.py +++ b/chia/_tests/util/test_secp256r1.py @@ -3,7 +3,7 @@ import pytest from chia.util.ints import uint32 -from chia.util.key_types import Secp256r1PrivateKey, Secp256r1PublicKey +from chia.util.key_types import Secp256r1PrivateKey, Secp256r1PublicKey, Secp256r1Signature from chia.util.keychain import generate_mnemonic, mnemonic_to_seed @@ -23,3 +23,10 @@ def test_key_drivers() -> None: pk = sk.public_key() assert Secp256r1PublicKey.from_bytes(bytes(pk)) == pk assert pk.get_fingerprint() < uint32.MAXIMUM + with pytest.raises(NotImplementedError): + sk.derive_unhardened(1) + + sig = sk.sign(b"foo") + assert Secp256r1Signature.from_bytes(bytes(sig)) == sig + with pytest.raises(NotImplementedError): + sk.sign(b"foo", final_pk=pk) diff --git a/chia/util/key_types.py b/chia/util/key_types.py index d5c3815b974c..ea4f6dabafae 100644 --- a/chia/util/key_types.py +++ b/chia/util/key_types.py @@ -31,7 +31,8 @@ def from_bytes(cls, blob: bytes) -> Secp256r1PublicKey: pk = serialization.load_der_public_key(blob) if isinstance(pk, ec.EllipticCurvePublicKey): return Secp256r1PublicKey(pk) - else: + else: # pragma: no cover + # Not sure how to test this, it's really just for mypy sake raise ValueError("Could not load EllipticCurvePublicKey provided blob") def derive_unhardened(self, index: int) -> Secp256r1PublicKey: @@ -45,6 +46,10 @@ class Secp256r1Signature: def __bytes__(self) -> bytes: return self._buf + @classmethod + def from_bytes(cls, blob: bytes) -> Secp256r1Signature: + return cls(blob) + # A wrapper for SigningKey that conforms to the SecretInfo protocol @dataclass(frozen=True) @@ -66,7 +71,8 @@ def from_bytes(cls, blob: bytes) -> Secp256r1PrivateKey: sk = serialization.load_der_private_key(blob, password=None) if isinstance(sk, ec.EllipticCurvePrivateKey): return Secp256r1PrivateKey(sk) - else: + else: # pragma: no cover + # Not sure how to test this, it's really just for mypy sake raise ValueError("Could not load EllipticCurvePrivateKey provided blob") def public_key(self) -> Secp256r1PublicKey: @@ -80,7 +86,7 @@ def from_seed(cls, seed: bytes) -> Secp256r1PrivateKey: def sign(self, msg: bytes, final_pk: Optional[Secp256r1PublicKey] = None) -> Secp256r1Signature: if final_pk is not None: - raise ValueError("SECP256r1 does not support signature aggregation") + raise NotImplementedError("SECP256r1 does not support signature aggregation") der_sig = self._private_key.sign(msg, ec.ECDSA(hashes.SHA256(), deterministic_signing=True)) r, s = decode_dss_signature(der_sig) sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") From be9d488d579b3a2cc88fc485e29694495f1f2001 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 14 Aug 2024 13:54:57 -0700 Subject: [PATCH 258/274] Pipe through values everywhere --- chia/_tests/wallet/vault/test_vault_wallet.py | 5 ++++- chia/daemon/keychain_proxy.py | 12 +++++------- chia/rpc/wallet_rpc_api.py | 11 +++++++---- chia/util/keychain.py | 6 +++--- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index ff183eba20f2..c705ca0bf2a8 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -13,6 +13,7 @@ from chia.rpc.wallet_request_types import VaultCreate, VaultRecovery from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 +from chia.util.keychain import KeyTypes from chia.wallet.payment import Payment from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG from chia.wallet.vault.vault_info import VaultInfo @@ -86,7 +87,9 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b ), ] ) - await env.node.keychain_proxy.add_key(launcher_id.hex(), label="vault", private=False) + await env.node.keychain_proxy.add_key( + launcher_id.hex(), label="vault", private=False, key_type=KeyTypes.VAULT_LAUNCHER + ) await env.restart(vault_root.get_fingerprint()) await wallet_environments.full_node.wait_for_wallet_synced(env.node, 20) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 180d6023d3b2..089f16d4adc8 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -187,7 +187,7 @@ async def add_key( ) -> Tuple[SecretInfo[Any], KeyTypes]: ... @overload - def add_key( + async def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: Literal[True], key_type: KeyTypes ) -> Tuple[SecretInfo[Any], KeyTypes]: ... @@ -197,13 +197,11 @@ async def add_key( ) -> Tuple[ObservationRoot, KeyTypes]: ... @overload - def add_key( + async def add_key( self, mnemonic_or_pk: str, label: Optional[str], private: Literal[False], key_type: KeyTypes ) -> Tuple[ObservationRoot, KeyTypes]: ... - # Mypy doesn't seem to think the implementation handles a coupld of the cases above. As far as I can see, it does - # and I can't figure out why mypy thinks it doesn't -Quex - async def add_key( # type: ignore[misc] + async def add_key( self, mnemonic_or_pk: str, label: Optional[str] = None, @@ -214,10 +212,10 @@ async def add_key( # type: ignore[misc] Forwards to Keychain.add_key() """ if self.use_local_keychain(): - key, key_type = self.keychain.add_key(mnemonic_or_pk, label, private) + key, key_type = self.keychain.add_key(mnemonic_or_pk, label, private, key_type) else: response, success = await self.get_response_for_request( - "add_key", {"mnemonic_or_pk": mnemonic_or_pk, "label": label, "private": private} + "add_key", {"mnemonic_or_pk": mnemonic_or_pk, "label": label, "private": private, "key_type": key_type} ) if success: key_type = KeyTypes(response["data"]["key_type"]) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 757e857d2132..8cfabda80d5f 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -444,10 +444,13 @@ async def get_private_key(self, request: Dict[str, Any]) -> EndpointResult: }, } if isinstance(sk, PrivateKey): - response["private_key"] = { - "farmer_pk": bytes(master_sk_to_farmer_sk(sk).get_g1()).hex(), - "pool_pk": bytes(master_sk_to_pool_sk(sk).get_g1()).hex(), - } + response["private_key"].update( + { + "farmer_pk": bytes(master_sk_to_farmer_sk(sk).get_g1()).hex(), + "pool_pk": bytes(master_sk_to_pool_sk(sk).get_g1()).hex(), + } + ) + return response return {"success": False, "private_key": {"fingerprint": fingerprint}} async def generate_mnemonic(self, request: Dict[str, Any]) -> EndpointResult: diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 5402ebbc083f..573ad5a8fca9 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -412,7 +412,7 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> Optional[Ke key_type=KeyTypes.G1_ELEMENT.value, ) elif key.metadata.get("type", KeyTypes.G1_ELEMENT.value) == KeyTypes.VAULT_LAUNCHER.value: - observation_root = G1Element.from_bytes(str_bytes) + observation_root = VaultRoot.from_bytes(str_bytes) fingerprint = observation_root.get_fingerprint() return KeyData( fingerprint=uint32(fingerprint), @@ -503,7 +503,7 @@ def add_key( index = self._get_free_private_key_index() key = KeyTypes.parse_secret_info_from_seed(seed, key_type) pk = key.public_key() - key_data = Key(bytes(pk) + entropy) + key_data = Key(bytes(pk) + entropy, metadata={"type": key_type.value}) fingerprint = pk.get_fingerprint() else: index = self._get_free_private_key_index() @@ -514,7 +514,7 @@ def add_key( else: pk_bytes = hexstr_to_bytes(mnemonic_or_pk) key = KeyTypes.parse_observation_root(pk_bytes, key_type) - key_data = Key(pk_bytes) + key_data = Key(pk_bytes, metadata={"type": key_type.value}) fingerprint = key.get_fingerprint() if fingerprint in [pk.get_fingerprint() for pk in self.get_all_public_keys()]: From 230e53e5e1de143350699efffad3698970545877 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 14 Aug 2024 14:04:27 -0700 Subject: [PATCH 259/274] pylint --- chia/cmds/keys_funcs.py | 6 +++++- chia/daemon/keychain_proxy.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 7617131619ae..5b117de6c76f 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -315,7 +315,11 @@ def sign(message: str, private_key: SecretInfo[Any], hd_path: str, as_bytes: boo if not isinstance(private_key, PrivateKey): print("Cannot derive non-BLS keys") return - sk = derive_pk_and_sk_from_hd_path(private_key.public_key(), hd_path, master_sk=private_key)[1] + sk: Optional[SecretInfo[Any]] = derive_pk_and_sk_from_hd_path( + private_key.public_key(), hd_path, master_sk=private_key + )[1] + else: + sk = private_key assert sk is not None data = bytes.fromhex(message) if as_bytes else bytes(message, "utf-8") signing_mode: SigningMode = ( diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 089f16d4adc8..43f3dfa5c3b5 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -232,6 +232,7 @@ async def add_key( raise KeyError(word) else: self.handle_error(response) + raise RuntimeError("This should be impossible to reach") # pragma: no cover return key, key_type From 8af8eccb4e4049aea67d885f282adbc4ef98b7ff Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 14 Aug 2024 14:43:24 -0700 Subject: [PATCH 260/274] fix tests --- chia/cmds/keys.py | 6 +++--- chia/cmds/keys_funcs.py | 10 +++++++--- chia/util/keychain.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index e69debda9e73..ca8ded9deb84 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -329,7 +329,7 @@ def search_cmd( if fingerprint is None and filename is not None: sk = resolve_derivation_master_key(filename) - if not isinstance(sk, PrivateKey): + if sk is not None and not isinstance(sk, PrivateKey): print("Cannot derive from non-BLS keys") return @@ -384,7 +384,7 @@ def wallet_address_cmd( if fingerprint is None and filename is not None: sk = resolve_derivation_master_key(filename) - if not isinstance(sk, PrivateKey): + if sk is not None and not isinstance(sk, PrivateKey): print("Cannot derive from non-BLS keys") return @@ -459,7 +459,7 @@ def child_key_cmd( if fingerprint is None and filename is not None: sk = resolve_derivation_master_key(filename) - if not isinstance(sk, PrivateKey): + if sk is not None and not isinstance(sk, PrivateKey): print("Cannot derive from non-BLS keys") return diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 5b117de6c76f..5915c4df1ddd 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -186,11 +186,11 @@ def process_key_data(key_data: KeyData) -> Dict[str, Any]: key["pool_pk"] = None if isinstance(key_data.observation_root, G1Element): - assert isinstance(key_data, PrivateKey) if non_observer_derivation: if sk is None: first_wallet_pk: Optional[G1Element] = None else: + assert isinstance(sk, PrivateKey) first_wallet_pk = master_sk_to_wallet_sk(sk, uint32(0)).public_key() else: first_wallet_pk = master_pk_to_wallet_pk_unhardened(key_data.observation_root, uint32(0)) @@ -750,9 +750,13 @@ def derive_child_key( # TODO: Add coverage when vault wallet exists print("Cannot currently derive from non-BLS keys") return - assert isinstance(key_data.private_key, PrivateKey) + if key_data.secrets is not None: + assert isinstance(key_data.private_key, PrivateKey) current_pk: G1Element = key_data.observation_root - current_sk: Optional[PrivateKey] = key_data.private_key if key_data.secrets is not None else None + # mypy can't figure out the semantics here + current_sk: Optional[PrivateKey] = ( + key_data.private_key if key_data.secrets is not None else None # type: ignore[assignment] + ) else: assert private_key is not None current_pk = private_key.get_g1() diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 573ad5a8fca9..af41d5b829b9 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -270,7 +270,7 @@ def from_mnemonic(cls, mnemonic: str, key_type: KeyTypes = KeyTypes.G1_ELEMENT) secret_info_bytes=bytes( PUBLIC_TYPES_TO_PRIVATE_TYPES[KEY_TYPES_TO_TYPES[key_type]].from_seed(mnemonic_to_seed(mnemonic)) ), - key_type=key_type, + key_type=key_type.value, ) @classmethod @@ -399,7 +399,7 @@ def _get_key_data(self, index: int, include_secrets: bool = True) -> Optional[Ke pk_bytes: bytes = str_bytes[: G1Element.SIZE] observation_root: ObservationRoot = G1Element.from_bytes(pk_bytes) fingerprint = observation_root.get_fingerprint() - if len(str_bytes) == G1Element.SIZE + 32: + if len(str_bytes) > G1Element.SIZE: entropy = str_bytes[G1Element.SIZE : G1Element.SIZE + 32] else: entropy = None From 0dad751a07d1ddfee34a53f95b38fe82390e9736 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 15 Aug 2024 07:43:05 -0700 Subject: [PATCH 261/274] typo --- chia/_tests/util/test_secp256r1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/util/test_secp256r1.py b/chia/_tests/util/test_secp256r1.py index 0183c1e56fc6..1beae0961056 100644 --- a/chia/_tests/util/test_secp256r1.py +++ b/chia/_tests/util/test_secp256r1.py @@ -24,7 +24,7 @@ def test_key_drivers() -> None: assert Secp256r1PublicKey.from_bytes(bytes(pk)) == pk assert pk.get_fingerprint() < uint32.MAXIMUM with pytest.raises(NotImplementedError): - sk.derive_unhardened(1) + pk.derive_unhardened(1) sig = sk.sign(b"foo") assert Secp256r1Signature.from_bytes(bytes(sig)) == sig From 05fcad531ac6622cc96bc8f4c96eaf28042fefd5 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 15 Aug 2024 08:50:04 -0700 Subject: [PATCH 262/274] Fix one last test --- chia/cmds/keys_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 5915c4df1ddd..d0a1ce832d8f 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -537,7 +537,7 @@ def search_derive( private_keys = [ ( master_key_data.private_key - if master_key_data.secrets is not None and isinstance(master_key_data, PrivateKey) + if master_key_data.secrets is not None and isinstance(master_key_data.private_key, PrivateKey) else None ) ] From 4b628870306df532332f8ef0eaf087fc9bd9762a Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 15 Aug 2024 13:35:36 -0700 Subject: [PATCH 263/274] Tweak `gather_signing_info` to work with multiple vaults and finish txs thrown in --- chia/wallet/vault/vault_drivers.py | 7 ++++++ chia/wallet/vault/vault_wallet.py | 35 ++++++++++++++++-------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/chia/wallet/vault/vault_drivers.py b/chia/wallet/vault/vault_drivers.py index bc3d3f68b28c..8042f4f6d221 100644 --- a/chia/wallet/vault/vault_drivers.py +++ b/chia/wallet/vault/vault_drivers.py @@ -128,6 +128,13 @@ def match_vault_puzzle(mod: Program, curried_args: Program) -> bool: return False +def match_p2_delegated_secp(mod: Program, curried_args: Program) -> bool: + if mod == P2_DELEGATED_SECP_MOD: + return True + else: + return False + + def match_recovery_puzzle(mod: Program, curried_args: Program, solution: Program) -> bool: if match_vault_puzzle(mod, curried_args): try: diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 506ad685b477..1944cd3400af 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -67,6 +67,7 @@ get_vault_inner_solution, get_vault_proof, match_finish_spend, + match_p2_delegated_secp, match_recovery_puzzle, match_vault_puzzle, ) @@ -232,7 +233,7 @@ async def _generate_unsigned_transaction( ) # create the p2_singleton spends delegated_puzzle = puzzle_for_conditions(conditions) - delegated_solution = solution_for_conditions(conditions) + delegated_solution = Program.to(None) p2_singleton_spends: List[CoinSpend] = [] for coin in coins: @@ -336,26 +337,28 @@ async def get_puzzle_hash(self, new: bool) -> bytes32: async def gather_signing_info(self, coin_spends: List[Spend]) -> SigningInstructions: pk = self.vault_info.pubkey - # match the vault puzzle + + targets = [] for spend in coin_spends: mod, curried_args = spend.puzzle.uncurry() - if match_vault_puzzle(mod, curried_args): + # match the vault puzzle + if match_vault_puzzle(mod, curried_args) and match_p2_delegated_secp(*spend.solution.at("rrfrf").uncurry()): vault_spend = spend - break - inner_sol = vault_spend.solution.at("rrf") - secp_puz = inner_sol.at("rf") - secp_sol = inner_sol.at("rrf") - _, secp_args = secp_puz.uncurry() - genesis_challenge = secp_args.at("f").as_atom() - hidden_puzzle_hash = secp_args.at("rrf").as_atom() - delegated_puzzle_hash = secp_sol.at("f").get_tree_hash() - coin_id = secp_sol.at("rrrf").as_atom() - message = delegated_puzzle_hash + coin_id + genesis_challenge + hidden_puzzle_hash - fingerprint = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") - target = SigningTarget(fingerprint, message, std_hash(pk + message)) + inner_sol = vault_spend.solution.at("rrf") + secp_puz = inner_sol.at("rf") + secp_sol = inner_sol.at("rrf") + _, secp_args = secp_puz.uncurry() + genesis_challenge = secp_args.at("f").as_atom() + hidden_puzzle_hash = secp_args.at("rrf").as_atom() + delegated_puzzle_hash = secp_sol.at("f").get_tree_hash() + coin_id = secp_sol.at("rrrf").as_atom() + message = delegated_puzzle_hash + coin_id + genesis_challenge + hidden_puzzle_hash + fingerprint = self.wallet_state_manager.observation_root.get_fingerprint().to_bytes(4, "big") + targets.append(SigningTarget(fingerprint, message, std_hash(pk + message))) + sig_info = SigningInstructions( await self.wallet_state_manager.key_hints_for_pubkeys([pk]), - [target], + targets, ) return sig_info From 13b9a4177e077b3c1a5b5e95f692ab4dabbb6aeb Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 15 Aug 2024 13:36:21 -0700 Subject: [PATCH 264/274] Refer to separate recovery transactions by IDs --- chia/_tests/cmds/wallet/test_vault.py | 2 +- chia/cmds/vault_funcs.py | 5 ++--- chia/rpc/util.py | 3 ++- chia/rpc/wallet_request_types.py | 11 ++++++++++- chia/rpc/wallet_rpc_api.py | 23 +++++++++++++++++------ chia/wallet/vault/vault_wallet.py | 14 ++++++++------ 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py index a8c49f5105e1..a7bea3ab14a6 100644 --- a/chia/_tests/cmds/wallet/test_vault.py +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -59,7 +59,7 @@ async def vault_recovery( args: VaultRecovery, tx_config: TXConfig, ) -> VaultRecoveryResponse: - return VaultRecoveryResponse([STD_UTX, STD_UTX], [STD_TX, STD_TX]) + return VaultRecoveryResponse([STD_UTX, STD_UTX], [STD_TX, STD_TX], STD_TX.name, STD_TX.name) inst_rpc_client = CreateVaultRpcClient() # pylint: disable=no-value-for-parameter test_rpc_clients.wallet_rpc_client = inst_rpc_client diff --git a/chia/cmds/vault_funcs.py b/chia/cmds/vault_funcs.py index bb0a868e5d51..4163eb8a78e1 100644 --- a/chia/cmds/vault_funcs.py +++ b/chia/cmds/vault_funcs.py @@ -93,8 +93,7 @@ async def recover_vault( ), tx_config=tx_config, ) - # TODO: do not rely on ordering of transactions here - write_transactions_to_file(response.transactions[0:1], initiate_file) - write_transactions_to_file(response.transactions[1:2], finish_file) + write_transactions_to_file([response.recovery_tx], initiate_file) + write_transactions_to_file([response.finish_tx], finish_file) except Exception as e: print(f"Error creating recovery transactions: {e}") diff --git a/chia/rpc/util.py b/chia/rpc/util.py index 525e29472f98..f343ddc6e755 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -102,6 +102,7 @@ async def inner(request) -> aiohttp.web.Response: def tx_endpoint( push: bool = False, merge_spends: bool = True, + sign: Optional[bool] = None, ) -> Callable[[RpcEndpoint], RpcEndpoint]: def _inner(func: RpcEndpoint) -> RpcEndpoint: async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[str, Any]: @@ -154,7 +155,7 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s tx_config, push=request.get("push", push), merge_spends=request.get("merge_spends", merge_spends), - sign=request.get("sign", self.service.config.get("auto_sign_txs", True)), + sign=request.get("sign", self.service.config.get("auto_sign_txs", True) if sign is None else sign), ) as action_scope: response: Dict[str, Any] = await func( self, diff --git a/chia/rpc/wallet_request_types.py b/chia/rpc/wallet_request_types.py index 0454b3033339..efe1f8849d20 100644 --- a/chia/rpc/wallet_request_types.py +++ b/chia/rpc/wallet_request_types.py @@ -152,7 +152,16 @@ class VaultRecovery(TransactionEndpointRequest): @streamable @dataclass(frozen=True) class VaultRecoveryResponse(TransactionEndpointResponse): - pass + recovery_tx_id: bytes32 + finish_tx_id: bytes32 + + @property + def recovery_tx(self) -> TransactionRecord: + return next(tx for tx in self.transactions if tx.name == self.recovery_tx_id) + + @property + def finish_tx(self) -> TransactionRecord: + return next(tx for tx in self.transactions if tx.name == self.finish_tx_id) # TODO: The section below needs corresponding request types diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 789b1df6830b..b0e524b09915 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -119,7 +119,7 @@ from chia.wallet.util.curry_and_treehash import NIL_TREEHASH from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType -from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig +from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state from chia.wallet.util.wallet_types import CoinType, WalletType from chia.wallet.vault.vault_drivers import get_vault_hidden_puzzle_with_index @@ -4630,25 +4630,36 @@ async def vault_create( ) return VaultCreateResponse([], []) # tx_endpoint will take care of filling this out + @tx_endpoint(push=False, merge_spends=False, sign=False) @marshal async def vault_recovery( - self, request: VaultRecovery, tx_config: TXConfig = DEFAULT_TX_CONFIG + self, + request: VaultRecovery, + action_scope: WalletActionScope, + extra_conditions: Tuple[Condition, ...] = tuple(), ) -> VaultRecoveryResponse: """ Initiate Vault Recovery """ if request.fee != 0: raise ValueError("Recovery endpoint cannot add fees because it assumes your vault is currently inacessible") + if action_scope.config.push or action_scope.config.sign: + raise ValueError( + "Cannot push or sign from this endpoint because the vault is assumed to be inaccessible by this wallet." + " Please push the individual transactions to the /push_transactions endpoint on a wallet " + " with the correct keyset." + ) wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Vault) hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(request.hp_index).get_tree_hash() genesis_challenge = self.service.wallet_state_manager.constants.GENESIS_CHALLENGE - recovery_txs = await wallet.create_recovery_spends( + recovery_tx_id, finish_tx_id = await wallet.create_recovery_spends( request.secp_pk, hidden_puzzle_hash, genesis_challenge, - tx_config, + action_scope, bls_pk=request.bls_pk, timelock=request.timelock, ) - # TODO: port this endpoint to tx_endpoint and action scopes - return VaultRecoveryResponse([], recovery_txs) + + # tx_endpoint will take care of filling the empty lists out + return VaultRecoveryResponse([], [], recovery_tx_id, finish_tx_id) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 1944cd3400af..5c14af194607 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -45,7 +45,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_hints import compute_spend_hints_and_additions from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig +from chia.wallet.util.tx_config import CoinSelectionConfig from chia.wallet.util.wallet_sync_utils import fetch_coin_spend from chia.wallet.util.wallet_types import WalletIdentifier from chia.wallet.vault.vault_drivers import ( @@ -501,12 +501,12 @@ async def create_recovery_spends( secp_pk: bytes, hidden_puzzle_hash: bytes32, genesis_challenge: bytes32, - tx_config: TXConfig, + action_scope: WalletActionScope, bls_pk: Optional[G1Element] = None, timelock: Optional[uint64] = None, - ) -> List[TransactionRecord]: + ) -> Tuple[bytes32, bytes32]: """ - Returns two tx records + Returns two tx IDs 1. Recover the vault which can be taken to the appropriate BLS wallet for signing 2. Complete the recovery after the timelock has elapsed """ @@ -584,7 +584,6 @@ async def create_recovery_spends( [recovery_coin.name(), new_vault_coin_id], [self.id(), self.id()] ) - # make the tx records recovery_tx = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -625,7 +624,10 @@ async def create_recovery_spends( valid_times=parse_timelock_info(tuple()), ) - return [recovery_tx, finish_tx] + async with action_scope.use() as interface: + interface.side_effects.transactions.extend([recovery_tx, finish_tx]) + + return (recovery_tx.name, finish_tx.name) async def sync_vault_launcher(self) -> None: wallet_node: Any = self.wallet_state_manager.wallet_node From 7e235da64ceb868018ea0acbfbf46930299c3b60 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 21 Aug 2024 13:45:37 -0700 Subject: [PATCH 265/274] Test coverage --- chia/_tests/core/daemon/test_keychain_proxy.py | 12 ++++++++++++ chia/_tests/core/util/test_keychain.py | 3 +++ chia/_tests/wallet/test_wallet_node.py | 14 ++++++++++++++ chia/util/keychain.py | 13 ------------- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/chia/_tests/core/daemon/test_keychain_proxy.py b/chia/_tests/core/daemon/test_keychain_proxy.py index bdc0d2ea2daa..9b6acc986bf8 100644 --- a/chia/_tests/core/daemon/test_keychain_proxy.py +++ b/chia/_tests/core/daemon/test_keychain_proxy.py @@ -100,3 +100,15 @@ async def test_get_keys(keychain_proxy_with_keys: KeychainProxy, include_secrets else: expected_keys = [replace(TEST_KEY_1, secrets=None), replace(TEST_KEY_2, secrets=None)] assert keys == expected_keys + + +@pytest.mark.anyio +async def test_get_first_private_key(keychain_proxy_with_keys: KeychainProxy) -> None: + assert TEST_KEY_1.private_key == await keychain_proxy_with_keys.get_first_private_key() + + +@pytest.mark.anyio +async def test_get_all_private_keys(keychain_proxy_with_keys: KeychainProxy) -> None: + assert [TEST_KEY_1.private_key, TEST_KEY_2.private_key] == [ + k for k, e in await keychain_proxy_with_keys.get_all_private_keys() + ] diff --git a/chia/_tests/core/util/test_keychain.py b/chia/_tests/core/util/test_keychain.py index d61ba37693f5..fb4efffd6d2f 100644 --- a/chia/_tests/core/util/test_keychain.py +++ b/chia/_tests/core/util/test_keychain.py @@ -548,3 +548,6 @@ def test_key_type_support(key_type: str, key_info: KeyInfo) -> None: assert KeyTypes.parse_observation_root(bytes(obr), KeyTypes(key_type)) == obr if secret_info is not None: assert KeyTypes.parse_secret_info(bytes(secret_info), KeyTypes(key_type)) == secret_info + assert ( + KeyTypes.parse_secret_info_from_seed(mnemonic_to_seed(key_info.mnemonic), KeyTypes(key_type)) == secret_info + ) diff --git a/chia/_tests/wallet/test_wallet_node.py b/chia/_tests/wallet/test_wallet_node.py index d27e7a363b57..69876ea45372 100644 --- a/chia/_tests/wallet/test_wallet_node.py +++ b/chia/_tests/wallet/test_wallet_node.py @@ -71,6 +71,20 @@ async def test_get_private_key_default_key(root_path_populated_with_config: Path assert isinstance(key, PrivateKey) assert key.get_g1().get_fingerprint() == fingerprint + # We should get the same result with a bogus fingerprint + key = await node.get_key(123456789) + + assert key is not None + assert isinstance(key, PrivateKey) + assert key.get_g1().get_fingerprint() == fingerprint + + # Test coverage + key = await node.get_key(123456789, private=False) + + assert key is not None + assert isinstance(key, G1Element) + assert key.get_fingerprint() == fingerprint + @pytest.mark.anyio @pytest.mark.parametrize("fingerprint", [None, 1234567890]) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index b6fc29b873d9..fb5e939b4c9b 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -663,19 +663,6 @@ def delete_key_by_fingerprint(self, fingerprint: int) -> int: pass return removed - def delete_keys(self, keys_to_delete: List[Tuple[SecretInfo[Any], bytes]]) -> None: - """ - Deletes all keys in the list. - """ - remaining_fingerprints = {x[0].public_key().get_fingerprint() for x in keys_to_delete} - remaining_removals = len(remaining_fingerprints) - while len(remaining_fingerprints): - key_to_delete = remaining_fingerprints.pop() - if self.delete_key_by_fingerprint(key_to_delete) > 0: - remaining_removals -= 1 - if remaining_removals > 0: - raise ValueError(f"{remaining_removals} keys could not be found for deletion") - def delete_all_keys(self) -> None: """ Deletes all keys from the keychain. From a996208cda251870c6f8f1dfd4eca2cba5369854 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 9 Sep 2024 08:03:10 -0700 Subject: [PATCH 266/274] Fix cmd tests --- chia/_tests/cmds/wallet/test_vault.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/chia/_tests/cmds/wallet/test_vault.py b/chia/_tests/cmds/wallet/test_vault.py index a7bea3ab14a6..7d8331003ae7 100644 --- a/chia/_tests/cmds/wallet/test_vault.py +++ b/chia/_tests/cmds/wallet/test_vault.py @@ -8,8 +8,12 @@ from chia._tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert from chia._tests.cmds.wallet.test_consts import FINGERPRINT_ARG, STD_TX, STD_UTX, WALLET_ID_ARG from chia.rpc.wallet_request_types import VaultCreate, VaultCreateResponse, VaultRecovery, VaultRecoveryResponse +from chia.util.ints import uint64 +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.util.tx_config import TXConfig +test_condition_valid_times: ConditionValidTimes = ConditionValidTimes(min_time=uint64(100), max_time=uint64(150)) + def test_vault_create(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None: test_rpc_clients, root_dir = get_test_cli_clients @@ -20,6 +24,7 @@ async def vault_create( self, args: VaultCreate, tx_config: TXConfig, + timelock_info: ConditionValidTimes, ) -> VaultCreateResponse: return VaultCreateResponse([STD_UTX], [STD_TX]) @@ -44,6 +49,10 @@ async def vault_create( hidden_puzzle_index, "-m", fee, + "--valid-at", + "100", + "--expires-at", + "150", ] assert_list = ["Successfully created a Vault wallet"] run_cli_command_and_assert(capsys, root_dir, command_args, assert_list) @@ -58,6 +67,7 @@ async def vault_recovery( self, args: VaultRecovery, tx_config: TXConfig, + timelock_info: ConditionValidTimes, ) -> VaultRecoveryResponse: return VaultRecoveryResponse([STD_UTX, STD_UTX], [STD_TX, STD_TX], STD_TX.name, STD_TX.name) @@ -82,6 +92,10 @@ async def vault_recovery( str(tmp_path / "recovery_init.json"), "-rf", str(tmp_path / "recovery_finish.json"), + "--valid-at", + "100", + "--expires-at", + "150", ] assert_list = [ "Writing transactions to file ", From 2cd3e648365783f9d5c8c97b4163284cd14deb10 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 16 Sep 2024 14:52:20 -0700 Subject: [PATCH 267/274] Remove pointless (?) check --- chia/wallet/wallet_state_manager.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index bcca600bcd18..920129e9ba77 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -2404,16 +2404,6 @@ async def add_pending_transactions( additional_signing_responses != [] and additional_signing_responses is not None, ) - # Check that tx_records have additions/removals since vault txs don't have them until they're signed - for i, tx in enumerate(tx_records): - if tx.spend_bundle is not None: - if tx.additions == []: - tx = dataclasses.replace(tx, additions=tx.spend_bundle.additions()) - if tx.removals == []: - assert isinstance(tx.spend_bundle, WalletSpendBundle) - tx = dataclasses.replace(tx, removals=tx.spend_bundle.removals()) - tx_records[i] = tx - if push: all_coins_names = [] async with self.db_wrapper.writer_maybe_transaction(): From 328948e89f05a47d2ca0e88e9fac480612c33e9f Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Sep 2024 07:40:44 -0700 Subject: [PATCH 268/274] Set vault transaction removals --- chia/wallet/vault/vault_wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 2752179c0c88..09fbcfe89798 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -170,7 +170,7 @@ async def generate_signed_transaction( sent=uint32(0), spend_bundle=spend_bundle, additions=[], - removals=[], + removals=spend_bundle.removals(), wallet_id=self.id(), sent_to=[], memos=[], From 9087259cd79b138a909a2914ad3436861ad2e6b5 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 30 Sep 2024 14:09:44 -0700 Subject: [PATCH 269/274] whoops --- .gitmodules | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitmodules b/.gitmodules index e69de29bb2d1..596e3d8820f0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "chia-blockchain-gui"] + path = chia-blockchain-gui + url = https://github.com/Chia-Network/chia-blockchain-gui.git + branch = main +[submodule "mozilla-ca"] + path = mozilla-ca + url = https://github.com/Chia-Network/mozilla-ca.git + branch = main From c893846df0eddd7097b50cbd604d73f45fa34a7b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 30 Sep 2024 14:29:35 -0700 Subject: [PATCH 270/274] Fix vault client endpoints --- chia/_tests/wallet/vault/test_vault_wallet.py | 3 ++- chia/rpc/wallet_rpc_client.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 520fb7864848..e7791d5538fe 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -36,7 +36,8 @@ async def vault_setup(wallet_environments: WalletTestFramework, with_recovery: b if with_recovery: bls_pk = (await client.get_private_key(GetPrivateKey(fingerprint))).private_key.observation_root() timelock = uint64(10) - assert isinstance(bls_pk, G1Element) + if bls_pk is not None: + assert isinstance(bls_pk, G1Element) hidden_puzzle_index = uint32(0) res = await client.vault_create( VaultCreate( diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 317947aea98f..f737e627a950 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1851,11 +1851,12 @@ async def vault_create( self, args: VaultCreate, tx_config: TXConfig, + extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), ) -> VaultCreateResponse: return VaultCreateResponse.from_json_dict( await self.fetch( - "vault_create", {**args.to_json_dict(), **tx_config.to_json_dict(), **timelock_info.to_json_dict()} + "vault_create", args.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) ) ) @@ -1863,10 +1864,11 @@ async def vault_recovery( self, args: VaultRecovery, tx_config: TXConfig, + extra_conditions: Tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), ) -> VaultRecoveryResponse: return VaultRecoveryResponse.from_json_dict( await self.fetch( - "vault_recovery", {**args.to_json_dict(), **tx_config.to_json_dict(), **timelock_info.to_json_dict()} + "vault_recovery", args.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) ) ) From 7c71915cf548f33e67b669ae4800022f2ddbe803 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 15 Oct 2024 10:15:10 -0700 Subject: [PATCH 271/274] whoops --- .gitmodules | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitmodules b/.gitmodules index 8d1657136f52..596e3d8820f0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ +[submodule "chia-blockchain-gui"] + path = chia-blockchain-gui + url = https://github.com/Chia-Network/chia-blockchain-gui.git + branch = main [submodule "mozilla-ca"] path = mozilla-ca url = https://github.com/Chia-Network/mozilla-ca.git From 210fa83fa6de9dbfba4371083979e42dcd179c01 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 15 Oct 2024 10:34:41 -0700 Subject: [PATCH 272/274] pylint --- chia/_tests/wallet/vault/test_vault_wallet.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/chia/_tests/wallet/vault/test_vault_wallet.py b/chia/_tests/wallet/vault/test_vault_wallet.py index 796590ef2887..31ce1864c4b0 100644 --- a/chia/_tests/wallet/vault/test_vault_wallet.py +++ b/chia/_tests/wallet/vault/test_vault_wallet.py @@ -271,7 +271,10 @@ async def test_vault_recovery( await wallet.generate_signed_transaction(amount, recipient_ph, action_scope, memos=[recipient_ph]) await wallet_environments.environments[0].rpc_client.push_transactions( - PushTransactions(transactions=action_scope.side_effects.transactions, sign=True), + PushTransactions( # pylint: disable=unexpected-keyword-arg + transactions=action_scope.side_effects.transactions, + sign=True, + ), tx_config=wallet_environments.tx_config, ) @@ -301,13 +304,18 @@ async def test_vault_recovery( hp_index=uint32(0), bls_pk=bls_pk, timelock=timelock, + sign=False, ), tx_config=wallet_environments.tx_config, ) ).transactions await wallet_environments.environments[1].rpc_client.push_transactions( - PushTransactions(transactions=[initiate_tx], sign=True), tx_config=wallet_environments.tx_config + PushTransactions( # pylint: disable=unexpected-keyword-arg + transactions=[initiate_tx], + sign=True, + ), + tx_config=wallet_environments.tx_config, ) vault_coin = wallet.vault_info.coin @@ -373,7 +381,10 @@ async def test_vault_recovery( # Test we can push the transaction separately await wallet_environments.environments[0].rpc_client.push_transactions( - PushTransactions(transactions=action_scope.side_effects.transactions, sign=True), + PushTransactions( # pylint: disable=unexpected-keyword-arg + transactions=action_scope.side_effects.transactions, + sign=True, + ), tx_config=wallet_environments.tx_config, ) await wallet_environments.process_pending_states( From 140ce7d632511edead6a9a8391193ff71a4e3d9c Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 28 Oct 2024 08:10:58 -0700 Subject: [PATCH 273/274] Fix imports --- chia/_tests/clvm/test_p2_singletons.py | 2 +- chia/_tests/wallet/vault/test_vault_lifecycle.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/_tests/clvm/test_p2_singletons.py b/chia/_tests/clvm/test_p2_singletons.py index 3bf981d92fb1..40fb1b341098 100644 --- a/chia/_tests/clvm/test_p2_singletons.py +++ b/chia/_tests/clvm/test_p2_singletons.py @@ -3,7 +3,7 @@ import pytest from chia_rs import G2Element -from chia.clvm.spend_sim import CostLogger, sim_and_client +from chia._tests.util.spend_sim import CostLogger, sim_and_client from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import make_spend diff --git a/chia/_tests/wallet/vault/test_vault_lifecycle.py b/chia/_tests/wallet/vault/test_vault_lifecycle.py index 38d3e14eb960..e68a1cdbf74e 100644 --- a/chia/_tests/wallet/vault/test_vault_lifecycle.py +++ b/chia/_tests/wallet/vault/test_vault_lifecycle.py @@ -11,7 +11,7 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from chia._tests.clvm.test_puzzles import secret_exponent_for_index -from chia.clvm.spend_sim import CostLogger, sim_and_client +from chia._tests.util.spend_sim import CostLogger, sim_and_client from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program From e235ed526ff1ce639104f9c22271207bb2b453d8 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 12 Nov 2024 07:46:06 -0800 Subject: [PATCH 274/274] Ruff format --- chia/wallet/vault/vault_wallet.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/chia/wallet/vault/vault_wallet.py b/chia/wallet/vault/vault_wallet.py index 3971930f1767..1031b3cae48e 100644 --- a/chia/wallet/vault/vault_wallet.py +++ b/chia/wallet/vault/vault_wallet.py @@ -328,9 +328,9 @@ async def get_puzzle_hash(self, new: bool) -> bytes32: if new: return self.get_p2_singleton_puzzle_hash() else: - record: Optional[DerivationRecord] = ( - await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id()) - ) + record: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.get_current_derivation_record_for_wallet(self.id()) if record is None: return self.get_p2_singleton_puzzle_hash() return record.puzzle_hash @@ -430,9 +430,9 @@ 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()) - ) + 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() assert isinstance(record._pubkey, bytes) @@ -450,9 +450,9 @@ def require_derivation_paths(self) -> bool: return False async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: - wallet_identifier: Optional[WalletIdentifier] = ( - await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(hint) - ) + wallet_identifier: Optional[ + WalletIdentifier + ] = await self.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash(hint) if wallet_identifier: return True return False