async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: sigs: List[G2Element] = [] for spend in spend_bundle.coin_spends: matched, puzzle_args = match_cat_puzzle( spend.puzzle_reveal.to_program()) if matched: _, _, inner_puzzle = puzzle_args puzzle_hash = inner_puzzle.get_tree_hash() pubkey, private = await self.wallet_state_manager.get_keys( puzzle_hash) synthetic_secret_key = calculate_synthetic_secret_key( private, DEFAULT_HIDDEN_PUZZLE_HASH) error, conditions, cost = conditions_dict_for_solution( spend.puzzle_reveal.to_program(), spend.solution.to_program(), self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, ) if conditions is not None: synthetic_pk = synthetic_secret_key.get_g1() for pk, msg in pkm_pairs_for_conditions_dict( conditions, spend.coin.name(), 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 load_attest_files_for_recovery_spend(self, filenames): spend_bundle_list = [] info_dict = {} try: for i in filenames: f = open(i) info = f.read().split(":") info_dict[info[0]] = [ bytes.fromhex(info[2]), bytes.fromhex(info[3]), uint64(info[4]), ] new_sb = SpendBundle.from_bytes(bytes.fromhex(info[1])) spend_bundle_list.append(new_sb) f.close() # info_dict {0xidentity: "(0xparent_info 0xinnerpuz amount)"} my_recovery_list: List[bytes] = self.did_info.backup_ids # convert info dict into recovery list - same order as wallet info_list = [] for entry in my_recovery_list: if entry.hex() in info_dict: info_list.append([ info_dict[entry.hex()][0], info_dict[entry.hex()][1], info_dict[entry.hex()][2], ]) else: info_list.append([]) message_spend_bundle = SpendBundle.aggregate(spend_bundle_list) return info_list, message_spend_bundle except Exception: raise
def aggregate(cls, offers: List["Offer"]) -> "Offer": total_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {} total_bundle = SpendBundle([], G2Element()) for offer in offers: # First check for any overlap in inputs total_inputs: Set[Coin] = { cs.coin for cs in total_bundle.coin_spends } offer_inputs: Set[Coin] = { cs.coin for cs in offer.bundle.coin_spends } if total_inputs & offer_inputs: raise ValueError("The aggregated offers overlap inputs") # Next, do the aggregation for tail, payments in offer.requested_payments.items(): if tail in total_requested_payments: total_requested_payments[tail].extend(payments) else: total_requested_payments[tail] = payments total_bundle = SpendBundle.aggregate([total_bundle, offer.bundle]) return cls(total_requested_payments, total_bundle)
def to_spend_bundle(self) -> SpendBundle: # Before we serialze this as a SpendBundle, we need to serialze the `requested_payments` as dummy CoinSpends additional_coin_spends: List[CoinSpend] = [] for tail_hash, payments in self.requested_payments.items(): puzzle_reveal: Program = construct_cat_puzzle( CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD inner_solutions = [] nonces: List[bytes32] = [p.nonce for p in payments] for nonce in list( dict.fromkeys(nonces)): # dedup without messing with order nonce_payments: List[NotarizedPayment] = list( filter(lambda p: p.nonce == nonce, payments)) inner_solutions.append( (nonce, [np.as_condition_args() for np in nonce_payments])) additional_coin_spends.append( CoinSpend( Coin( ZERO_32, puzzle_reveal.get_tree_hash(), uint64(0), ), puzzle_reveal, Program.to(inner_solutions), )) return SpendBundle.aggregate([ SpendBundle(additional_coin_spends, G2Element()), self.bundle, ])
async def test_stealing_fee(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( 5, block_list_input=blocks, guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) full_node_1, full_node_2, server_1, server_2 = two_nodes peer = await connect_and_get_peer(server_1, server_2) for block in blocks: await full_node_1.full_node.respond_block( full_node_protocol.RespondBlock(block)) await time_out_assert(60, node_height_at_least, True, full_node_1, start_height + 5) receiver_puzzlehash = BURN_PUZZLE_HASH cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} fee = 9 coin_1 = list(blocks[-2].get_included_reward_coins())[0] coin_2 = None for coin in list(blocks[-1].get_included_reward_coins()): if coin.amount == coin_1.amount: coin_2 = coin spend_bundle1 = generate_test_spend_bundle(coin_1, dic, uint64(fee)) steal_fee_spendbundle = WALLET_A.generate_signed_transaction( coin_1.amount + fee - 4, receiver_puzzlehash, coin_2) assert spend_bundle1 is not None assert steal_fee_spendbundle is not None combined = SpendBundle.aggregate( [spend_bundle1, steal_fee_spendbundle]) assert combined.fees() == 4 tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction( spend_bundle1) await full_node_1.respond_transaction(tx1, peer) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle( spend_bundle1.name()) assert mempool_bundle is None
async def do_spend( self, sim: SpendSim, sim_client: SimClient, tail: Program, coins: List[Coin], lineage_proofs: List[Program], inner_solutions: List[Program], expected_result: Tuple[MempoolInclusionStatus, Err], reveal_limitations_program: bool = True, signatures: List[G2Element] = [], extra_deltas: Optional[List[int]] = None, additional_spends: List[SpendBundle] = [], limitations_solutions: Optional[List[Program]] = None, cost_str: str = "", ): if limitations_solutions is None: limitations_solutions = [Program.to([])] * len(coins) if extra_deltas is None: extra_deltas = [0] * len(coins) spendable_cat_list: List[SpendableCAT] = [] for coin, innersol, proof, limitations_solution, extra_delta in zip( coins, inner_solutions, lineage_proofs, limitations_solutions, extra_deltas ): spendable_cat_list.append( SpendableCAT( coin, tail.get_tree_hash(), acs, innersol, limitations_solution=limitations_solution, lineage_proof=proof, extra_delta=extra_delta, limitations_program_reveal=tail if reveal_limitations_program else Program.to([]), ) ) spend_bundle: SpendBundle = unsigned_spend_bundle_for_spendable_cats( CAT_MOD, spendable_cat_list, ) agg_sig = AugSchemeMPL.aggregate(signatures) result = await sim_client.push_tx( SpendBundle.aggregate( [ *additional_spends, spend_bundle, SpendBundle([], agg_sig), # "Signing" the spend bundle ] ) ) assert result == expected_result self.cost[cost_str] = cost_of_spend_bundle(spend_bundle) await sim.farm_block()
async def generate_issuance_bundle( cls, wallet, _: Dict, amount: uint64) -> Tuple[TransactionRecord, SpendBundle]: coins = await wallet.standard_wallet.select_coins(amount) origin = coins.copy().pop() origin_id = origin.name() cc_inner: Program = await wallet.get_new_inner_puzzle() await wallet.add_lineage(origin_id, LineageProof()) genesis_coin_checker: Program = cls.construct([Program.to(origin_id)]) minted_cc_puzzle_hash: bytes32 = construct_cat_puzzle( CAT_MOD, genesis_coin_checker.get_tree_hash(), cc_inner).get_tree_hash() tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction( amount, minted_cc_puzzle_hash, uint64(0), origin_id, coins) assert tx_record.spend_bundle is not None inner_solution = wallet.standard_wallet.add_condition_to_solution( Program.to([51, 0, -113, genesis_coin_checker, []]), wallet.standard_wallet.make_solution(primaries=[{ "puzzlehash": cc_inner.get_tree_hash(), "amount": amount }], ), ) eve_spend = unsigned_spend_bundle_for_spendable_cats( CAT_MOD, [ SpendableCAT( list( filter(lambda a: a.amount == amount, tx_record.additions))[0], genesis_coin_checker.get_tree_hash(), cc_inner, inner_solution, limitations_program_reveal=genesis_coin_checker, ) ], ) signed_eve_spend = await wallet.sign(eve_spend) if wallet.cat_info.my_tail is None: await wallet.save_info( CATInfo(genesis_coin_checker.get_tree_hash(), genesis_coin_checker, wallet.cat_info.lineage_proofs), False, ) return tx_record, SpendBundle.aggregate( [tx_record.spend_bundle, signed_eve_spend])
def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) dic = {cvp.opcode: [cvp]} cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2]} spend_bundle1 = generate_test_spend_bundle(coin_1, dic) spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle
async def generate_new_decentralised_id( self, amount: uint64) -> Optional[SpendBundle]: """ This must be called under the wallet state manager lock """ coins = await self.standard_wallet.select_coins(amount) if coins is None: return None origin = coins.copy().pop() did_inner: Program = await self.get_new_innerpuz() did_inner_hash = did_inner.get_tree_hash() did_puz = did_wallet_puzzles.create_fullpuz(did_inner, origin.puzzle_hash) did_puzzle_hash = did_puz.get_tree_hash() tx_record: Optional[ TransactionRecord] = await self.standard_wallet.generate_signed_transaction( amount, did_puzzle_hash, uint64(0), origin.name(), coins) eve_coin = Coin(origin.name(), did_puzzle_hash, amount) future_parent = CCParent( eve_coin.parent_coin_info, did_inner_hash, eve_coin.amount, ) eve_parent = CCParent( origin.parent_coin_info, origin.puzzle_hash, origin.amount, ) await self.add_parent(eve_coin.parent_coin_info, eve_parent, False) await self.add_parent(eve_coin.name(), future_parent, False) if tx_record is None or tx_record.spend_bundle is None: return None # Only want to save this information if the transaction is valid did_info: DIDInfo = DIDInfo( origin, self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed, self.did_info.parent_info, did_inner, None, None, None, ) await self.save_info(did_info, False) eve_spend = await self.generate_eve_spend(eve_coin, did_puz, did_inner) full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend]) return full_spend
def test_only_odd_coins_0(): blocks = initial_blocks() farmed_coin = list(blocks[-1].get_included_reward_coins())[0] metadata = [("foo", "bar")] ANYONE_CAN_SPEND_PUZZLE = Program.to(1) launcher_amount = uint64(1) launcher_puzzle = LAUNCHER_PUZZLE launcher_puzzle_hash = launcher_puzzle.get_tree_hash() initial_singleton_puzzle = adaptor_for_singleton_inner_puzzle( ANYONE_CAN_SPEND_PUZZLE) lineage_proof, launcher_id, condition_list, launcher_spend_bundle = launcher_conditions_and_spend_bundle( farmed_coin.name(), launcher_amount, initial_singleton_puzzle, metadata, launcher_puzzle) conditions = Program.to(condition_list) coin_solution = CoinSolution(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, conditions) spend_bundle = SpendBundle.aggregate( [launcher_spend_bundle, SpendBundle([coin_solution], G2Element())]) run = asyncio.get_event_loop().run_until_complete coins_added, coins_removed = run( check_spend_bundle_validity(bt.constants, blocks, spend_bundle)) coin_set_added = set([_.coin for _ in coins_added]) coin_set_removed = set([_.coin for _ in coins_removed]) launcher_coin = launcher_spend_bundle.coin_solutions[0].coin assert launcher_coin in coin_set_added assert launcher_coin in coin_set_removed assert farmed_coin in coin_set_removed # breakpoint() singleton_expected_puzzle_hash = singleton_puzzle_hash( launcher_id, launcher_puzzle_hash, initial_singleton_puzzle) expected_singleton_coin = Coin(launcher_coin.name(), singleton_expected_puzzle_hash, launcher_amount) assert expected_singleton_coin in coin_set_added # next up: spend the expected_singleton_coin # it's an adapted `ANYONE_CAN_SPEND_PUZZLE` # then try a bad lineage proof # then try writing two odd coins # then try writing zero odd coins # then, destroy the singleton with the -113 hack return 0
def spend_coin_to_singleton( puzzle_db: PuzzleDB, launcher_puzzle: Program, coin_store: CoinStore, now: CoinTimestamp) -> Tuple[List[Coin], List[CoinSpend]]: farmed_coin_amount = 100000 metadata = [("foo", "bar")] now = CoinTimestamp(10012300, 1) farmed_coin = coin_store.farm_coin(ANYONE_CAN_SPEND_PUZZLE.get_tree_hash(), now, amount=farmed_coin_amount) now.seconds += 500 now.height += 1 launcher_amount: uint64 = uint64(1) launcher_puzzle = LAUNCHER_PUZZLE launcher_puzzle_hash = launcher_puzzle.get_tree_hash() initial_singleton_puzzle = adaptor_for_singleton_inner_puzzle( ANYONE_CAN_SPEND_PUZZLE) launcher_id, condition_list, launcher_spend_bundle = launcher_conditions_and_spend_bundle( puzzle_db, farmed_coin.name(), launcher_amount, initial_singleton_puzzle, metadata, launcher_puzzle) conditions = Program.to(condition_list) coin_spend = CoinSpend(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, conditions) spend_bundle = SpendBundle.aggregate( [launcher_spend_bundle, SpendBundle([coin_spend], G2Element())]) additions, removals = coin_store.update_coin_store_for_spend_bundle( spend_bundle, now, MAX_BLOCK_COST_CLVM, COST_PER_BYTE) launcher_coin = launcher_spend_bundle.coin_spends[0].coin assert_coin_spent(coin_store, launcher_coin) assert_coin_spent(coin_store, farmed_coin) # TODO: address hint error and remove ignore # error: Argument 1 to "singleton_puzzle" has incompatible type "bytes32"; expected "Program" [arg-type] singleton_expected_puzzle = singleton_puzzle( launcher_id, # type: ignore[arg-type] launcher_puzzle_hash, initial_singleton_puzzle, ) singleton_expected_puzzle_hash = singleton_expected_puzzle.get_tree_hash() expected_singleton_coin = Coin(launcher_coin.name(), singleton_expected_puzzle_hash, launcher_amount) assert_coin_spent(coin_store, expected_singleton_coin, is_spent=False) return additions, removals
async def test_invalid_announcement_consumed_two(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height if len(blocks) > 0 else -1 blocks = bt.get_consecutive_blocks( 3, block_list_input=blocks, guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) peer = await connect_and_get_peer(server_1, server_2) for block in blocks: await full_node_1.full_node.respond_block( full_node_protocol.RespondBlock(block)) await time_out_assert(60, node_height_at_least, True, full_node_1, start_height + 3) coin_1 = list(blocks[-2].get_included_reward_coins())[0] coin_2 = list(blocks[-1].get_included_reward_coins())[0] announce = Announcement(coin_1.name(), bytes("test", "utf-8")) cvp = ConditionVarPair(ConditionOpcode.ASSERT_ANNOUNCEMENT, [announce.name()]) dic = {cvp.opcode: [cvp]} cvp2 = ConditionVarPair( ConditionOpcode.CREATE_ANNOUNCEMENT, [bytes("test", "utf-8")], ) dic2 = {cvp.opcode: [cvp2]} spend_bundle1 = generate_test_spend_bundle(coin_1, dic) spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction( spend_bundle1) await full_node_1.respond_transaction(tx1, peer) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle( bundle.name()) assert mempool_bundle is None
async def create_bundle_from_mempool( self, last_tb_header_hash: bytes32 ) -> Optional[Tuple[SpendBundle, List[Coin], List[Coin]]]: """ Returns aggregated spendbundle that can be used for creating new block, additions and removals in that spend_bundle """ if (self.peak is None or self.peak.header_hash != last_tb_header_hash or int(time.time()) <= self.constants.INITIAL_FREEZE_END_TIMESTAMP): return None cost_sum = 0 # Checks that total cost does not exceed block maximum fee_sum = 0 # Checks that total fees don't exceed 64 bits spend_bundles: List[SpendBundle] = [] removals = [] additions = [] broke_from_inner_loop = False log.info( f"Starting to make block, max cost: {self.constants.MAX_BLOCK_COST_CLVM}" ) for dic in reversed(self.mempool.sorted_spends.values()): if broke_from_inner_loop: break for item in dic.values(): log.info( f"Cumulative cost: {cost_sum}, fee per cost: {item.fee / item.cost}" ) if (item.cost + cost_sum <= self.limit_factor * self.constants.MAX_BLOCK_COST_CLVM and item.fee + fee_sum <= self.constants.MAX_COIN_AMOUNT): spend_bundles.append(item.spend_bundle) cost_sum += item.cost fee_sum += item.fee removals.extend(item.removals) additions.extend(item.additions) else: broke_from_inner_loop = True break if len(spend_bundles) > 0: log.info( f"Cumulative cost of block (real cost should be less) {cost_sum}. Proportion " f"full: {cost_sum / self.constants.MAX_BLOCK_COST_CLVM}") agg = SpendBundle.aggregate(spend_bundles) assert set(agg.additions()) == set(additions) assert set(agg.removals()) == set(removals) return agg, additions, removals else: return None
def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT, [announce.name()]) dic = {cvp.opcode: [cvp]} cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, [bytes(0x80)]) dic2 = {cvp.opcode: [cvp2]} spend_bundle1 = generate_test_spend_bundle(coin_1, dic) spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2])
def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_1.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) dic = {cvp.opcode: [cvp]} cvp2 = ConditionWithArgs( ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"], ) dic2 = {cvp.opcode: [cvp2]} spend_bundle1 = generate_test_spend_bundle(coin_1, dic) # coin 2 is making the announcement, right message wrong coin spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2])
async def test_validate_blockchain_duplicate_output(self, two_nodes): num_blocks = 3 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] receiver_puzzlehash = BURN_PUZZLE_HASH blocks = bt.get_consecutive_blocks( num_blocks, farmer_reward_puzzle_hash=coinbase_puzzlehash, guarantee_transaction_block=True) full_node_api_1, full_node_api_2, server_1, server_2 = two_nodes full_node_1 = full_node_api_1.full_node for block in blocks: await full_node_api_1.full_node.respond_block( full_node_protocol.RespondBlock(block)) spend_block = blocks[2] spend_coin = None for coin in list(spend_block.get_included_reward_coins()): if coin.puzzle_hash == coinbase_puzzlehash: spend_coin = coin spend_bundle = wallet_a.generate_signed_transaction( 1000, receiver_puzzlehash, spend_coin) spend_bundle_double = wallet_a.generate_signed_transaction( 1000, receiver_puzzlehash, spend_coin) block_spendbundle = SpendBundle.aggregate( [spend_bundle, spend_bundle_double]) new_blocks = bt.get_consecutive_blocks( 1, block_list_input=blocks, farmer_reward_puzzle_hash=coinbase_puzzlehash, transaction_data=block_spendbundle, guarantee_transaction_block=True, ) next_block = new_blocks[-1] res, err, _ = await full_node_1.blockchain.receive_block(next_block) assert res == ReceiveBlockResult.INVALID_BLOCK assert err == Err.DUPLICATE_OUTPUT
async def test_double_spend_same_bundle(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( 3, block_list_input=blocks, guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) peer = await connect_and_get_peer(server_1, server_2) for block in blocks: await full_node_1.full_node.respond_block( full_node_protocol.RespondBlock(block)) await time_out_assert(60, node_height_at_least, True, full_node_1, start_height + 3) coin = list(blocks[-1].get_included_reward_coins())[0] spend_bundle1 = generate_test_spend_bundle(coin) assert spend_bundle1 is not None spend_bundle2 = generate_test_spend_bundle( coin, new_puzzle_hash=BURN_PUZZLE_HASH_2, ) assert spend_bundle2 is not None spend_bundle_combined = SpendBundle.aggregate( [spend_bundle1, spend_bundle2]) tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction( spend_bundle_combined) await full_node_1.respond_transaction(tx, peer) sb = full_node_1.full_node.mempool_manager.get_spendbundle( spend_bundle_combined.name()) assert sb is None
async def create_bundle_from_mempool( self, peak_header_hash: bytes32 ) -> Optional[Tuple[SpendBundle, List[Coin], List[Coin]]]: """ Returns aggregated spendbundle that can be used for creating new block, additions and removals in that spend_bundle """ if ( self.peak is None or self.peak.header_hash != peak_header_hash or self.peak.height <= self.constants.INITIAL_FREEZE_PERIOD ): return None cost_sum = 0 # Checks that total cost does not exceed block maximum fee_sum = 0 # Checks that total fees don't exceed 64 bits spend_bundles: List[SpendBundle] = [] removals = [] additions = [] broke_from_inner_loop = False log.info(f"Starting to make block, max cost: {self.constants.MAX_BLOCK_COST_CLVM}") for dic in self.mempool.sorted_spends.values(): if broke_from_inner_loop: break for item in dic.values(): log.info(f"Cumulative cost: {cost_sum}") if ( item.cost_result.cost + cost_sum <= self.constants.MAX_BLOCK_COST_CLVM and item.fee + fee_sum <= self.constants.MAX_COIN_AMOUNT ): spend_bundles.append(item.spend_bundle) cost_sum += item.cost_result.cost fee_sum += item.fee removals.extend(item.removals) additions.extend(item.additions) else: broke_from_inner_loop = True break if len(spend_bundles) > 0: return SpendBundle.aggregate(spend_bundles), additions, removals else: return None
async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUNT): # Get our alice wallet some money await network.farm_block(farmer=alice) # This will use one mojo to create our piggybank on the blockchain. piggybank_coin = await alice.launch_smart_coin( create_piggybank_puzzle(1000000000000, bob.puzzle_hash)) # This retrieves us a coin that is at least 500 mojos. contribution_coin = await alice.choose_coin(CONTRIBUTION_AMOUNT) #This is the spend of the piggy bank coin. We use the driver code to create the solution. piggybank_spend = await alice.spend_coin( piggybank_coin, pushtx=False, args=solution_for_piggybank(piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT), ) # This is the spend of a standard coin. We simply spend to ourselves but minus the CONTRIBUTION_AMOUNT. contribution_spend = await alice.spend_coin( contribution_coin, pushtx=False, amt=(contribution_coin.amount - CONTRIBUTION_AMOUNT), custom_conditions=[[ ConditionOpcode.CREATE_COIN, contribution_coin.puzzle_hash, (contribution_coin.amount - CONTRIBUTION_AMOUNT) ], piggybank_announcement_assertion( piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT)]) # Aggregate them to make sure they are spent together combined_spend = SpendBundle.aggregate( [contribution_spend, piggybank_spend]) result = await network.push_tx(combined_spend) return result
async def create_absorb_transaction( self, farmer_record: FarmerRecord, singleton_coin: Coin, reward_coin_records: List[CoinRecord]) -> SpendBundle: # We assume that the farmer record singleton state is updated to the latest escape_inner_puzzle: Program = POOL_ESCAPING_MOD.curry( farmer_record.pool_puzzle_hash, self.relative_lock_height, bytes(farmer_record.owner_public_key), farmer_record.p2_singleton_puzzle_hash, ) committed_inner_puzzle: Program = POOL_COMMITED_MOD.curry( farmer_record.pool_puzzle_hash, escape_inner_puzzle.get_tree_hash(), farmer_record.p2_singleton_puzzle_hash, bytes(farmer_record.owner_public_key), ) aggregate_spend_bundle: SpendBundle = SpendBundle([], G2Element()) for reward_coin_record in reward_coin_records: found_block_index: Optional[uint32] = None for block_index in range( reward_coin_record.confirmed_block_index, reward_coin_record.confirmed_block_index - 100, -1): if block_index < 0: break pool_parent = pool_parent_id(uint32(block_index), self.constants.GENESIS_CHALLENGE) if pool_parent == reward_coin_record.coin.parent_coin_info: found_block_index = uint32(block_index) if not found_block_index: self.log.info( f"Received reward {reward_coin_record.coin} that is not a pool reward." ) singleton_full = SINGLETON_MOD.curry( singleton_mod_hash, farmer_record.singleton_genesis, committed_inner_puzzle) inner_sol = Program.to([ 0, singleton_full.get_tree_hash(), singleton_coin.amount, reward_coin_record.amount, found_block_index ]) full_sol = Program.to([ farmer_record.singleton_genesis, singleton_coin.amount, inner_sol ]) new_spend = SpendBundle( [ CoinSolution( singleton_coin, SerializedProgram.from_bytes(bytes(singleton_full)), full_sol) ], G2Element(), ) # TODO(pool): handle the case where the cost exceeds the size of the block aggregate_spend_bundle = SpendBundle.aggregate( [aggregate_spend_bundle, new_spend]) singleton_coin = await self.get_next_singleton_coin(new_spend) cost, result = singleton_full.run_with_cost( INFINITE_COST, full_sol) self.log.info(f"Cost: {cost}, result {result}") return aggregate_spend_bundle
async def test_double_spend_with_higher_fee(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( 3, block_list_input=blocks, guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) peer = await connect_and_get_peer(server_1, server_2) for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) await time_out_assert(60, node_height_at_least, True, full_node_1, start_height + 3) coins = iter(blocks[-1].get_included_reward_coins()) coin1, coin2 = next(coins), next(coins) coins = iter(blocks[-2].get_included_reward_coins()) coin3, coin4 = next(coins), next(coins) sb1_1 = await self.gen_and_send_sb(full_node_1, peer, coin1) sb1_2 = await self.gen_and_send_sb(full_node_1, peer, coin1, fee=uint64(1)) # Fee increase is insufficient, the old spendbundle must stay self.assert_sb_in_pool(full_node_1, sb1_1) self.assert_sb_not_in_pool(full_node_1, sb1_2) min_fee_increase = full_node_1.full_node.mempool_manager.get_min_fee_increase() sb1_3 = await self.gen_and_send_sb(full_node_1, peer, coin1, fee=uint64(min_fee_increase)) # Fee increase is sufficiently high, sb1_1 gets replaced with sb1_3 self.assert_sb_not_in_pool(full_node_1, sb1_1) self.assert_sb_in_pool(full_node_1, sb1_3) sb2 = generate_test_spend_bundle(coin2, fee=uint64(min_fee_increase)) sb12 = SpendBundle.aggregate((sb2, sb1_3)) await self.send_sb(full_node_1, peer, sb12) # Aggregated spendbundle sb12 replaces sb1_3 since it spends a superset # of coins spent in sb1_3 self.assert_sb_in_pool(full_node_1, sb12) self.assert_sb_not_in_pool(full_node_1, sb1_3) sb3 = generate_test_spend_bundle(coin3, fee=uint64(min_fee_increase * 2)) sb23 = SpendBundle.aggregate((sb2, sb3)) await self.send_sb(full_node_1, peer, sb23) # sb23 must not replace existing sb12 as the former does not spend all # coins that are spent in the latter (specifically, coin1) self.assert_sb_in_pool(full_node_1, sb12) self.assert_sb_not_in_pool(full_node_1, sb23) await self.send_sb(full_node_1, peer, sb3) # Adding non-conflicting sb3 should succeed self.assert_sb_in_pool(full_node_1, sb3) sb4_1 = generate_test_spend_bundle(coin4, fee=uint64(min_fee_increase)) sb1234_1 = SpendBundle.aggregate((sb12, sb3, sb4_1)) await self.send_sb(full_node_1, peer, sb1234_1) # sb1234_1 should not be in pool as it decreases total fees per cost self.assert_sb_not_in_pool(full_node_1, sb1234_1) sb4_2 = generate_test_spend_bundle(coin4, fee=uint64(min_fee_increase * 2)) sb1234_2 = SpendBundle.aggregate((sb12, sb3, sb4_2)) await self.send_sb(full_node_1, peer, sb1234_2) # sb1234_2 has a higher fee per cost than its conflicts and should get # into mempool self.assert_sb_in_pool(full_node_1, sb1234_2) self.assert_sb_not_in_pool(full_node_1, sb12) self.assert_sb_not_in_pool(full_node_1, sb3)
async def generate_launcher_spend( standard_wallet: Wallet, amount: uint64, initial_target_state: PoolState, genesis_challenge: bytes32, delay_time: uint64, delay_ph: bytes32, ) -> Tuple[SpendBundle, bytes32, bytes32]: """ Creates the initial singleton, which includes spending an origin coin, the launcher, and creating a singleton with the "pooling" inner state, which can be either self pooling or using a pool """ coins: Set[Coin] = await standard_wallet.select_coins(amount) if coins is None: raise ValueError("Not enough coins to create pool wallet") assert len(coins) == 1 launcher_parent: Coin = coins.copy().pop() genesis_launcher_puz: Program = SINGLETON_LAUNCHER launcher_coin: Coin = Coin(launcher_parent.name(), genesis_launcher_puz.get_tree_hash(), amount) escaping_inner_puzzle: Program = create_waiting_room_inner_puzzle( initial_target_state.target_puzzle_hash, initial_target_state.relative_lock_height, initial_target_state.owner_pubkey, launcher_coin.name(), genesis_challenge, delay_time, delay_ph, ) escaping_inner_puzzle_hash = escaping_inner_puzzle.get_tree_hash() self_pooling_inner_puzzle: Program = create_pooling_inner_puzzle( initial_target_state.target_puzzle_hash, escaping_inner_puzzle_hash, initial_target_state.owner_pubkey, launcher_coin.name(), genesis_challenge, delay_time, delay_ph, ) if initial_target_state.state == SELF_POOLING: puzzle = escaping_inner_puzzle elif initial_target_state.state == FARMING_TO_POOL: puzzle = self_pooling_inner_puzzle else: raise ValueError("Invalid initial state") full_pooling_puzzle: Program = create_full_puzzle(puzzle, launcher_id=launcher_coin.name()) puzzle_hash: bytes32 = full_pooling_puzzle.get_tree_hash() pool_state_bytes = Program.to([("p", bytes(initial_target_state)), ("t", delay_time), ("h", delay_ph)]) announcement_set: Set[Announcement] = set() announcement_message = Program.to([puzzle_hash, amount, pool_state_bytes]).get_tree_hash() announcement_set.add(Announcement(launcher_coin.name(), announcement_message)) create_launcher_tx_record: Optional[TransactionRecord] = await standard_wallet.generate_signed_transaction( amount, genesis_launcher_puz.get_tree_hash(), uint64(0), None, coins, None, False, announcement_set, ) assert create_launcher_tx_record is not None and create_launcher_tx_record.spend_bundle is not None genesis_launcher_solution: Program = Program.to([puzzle_hash, amount, pool_state_bytes]) launcher_cs: CoinSpend = CoinSpend( launcher_coin, SerializedProgram.from_program(genesis_launcher_puz), SerializedProgram.from_program(genesis_launcher_solution), ) launcher_sb: SpendBundle = SpendBundle([launcher_cs], G2Element()) # Current inner will be updated when state is verified on the blockchain full_spend: SpendBundle = SpendBundle.aggregate([create_launcher_tx_record.spend_bundle, launcher_sb]) return full_spend, puzzle_hash, launcher_coin.name()
async def recovery_spend( self, coin: Coin, puzhash: bytes, parent_innerpuzhash_amounts_for_recovery_ids: List[Tuple[bytes, bytes, int]], pubkey: G1Element, spend_bundle: SpendBundle, ) -> SpendBundle: assert self.did_info.origin_coin is not None # innerpuz solution is (mode amount new_puz identity my_puz parent_innerpuzhash_amounts_for_recovery_ids) innersol = Program.to([ 2, coin.amount, puzhash, coin.name(), coin.puzzle_hash, parent_innerpuzhash_amounts_for_recovery_ids, bytes(pubkey), self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed, ]) # full solution is (parent_info my_amount solution) innerpuz = self.did_info.current_inner full_puzzle: Program = did_wallet_puzzles.create_fullpuz( innerpuz, self.did_info.origin_coin.puzzle_hash, ) parent_info = await self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to([ [ self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount ], [ parent_info.parent_name, parent_info.inner_puzzle_hash, parent_info.amount, ], coin.amount, innersol, ]) list_of_solutions = [CoinSolution(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(self.wallet_state_manager.private_key, index) message = bytes(puzhash) sigs = [AugSchemeMPL.sign(private, message)] for _ in spend_bundle.coin_solutions: 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_solutions, aggsig) else: spend_bundle = spend_bundle.aggregate( [spend_bundle, SpendBundle(list_of_solutions, aggsig)]) did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), to_puzzle_hash=puzhash, amount=uint64(coin.amount), fee_amount=uint64(0), confirmed=False, sent=uint32(0), spend_bundle=spend_bundle, additions=spend_bundle.additions(), removals=spend_bundle.removals(), wallet_id=self.wallet_info.id, sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=token_bytes(), ) await self.standard_wallet.push_transaction(did_record) return spend_bundle
async def generate_unsigned_spendbundle( self, payments: List[Payment], fee: uint64 = uint64(0), cat_discrepancy: Optional[Tuple[ int, Program]] = None, # (extra_delta, limitations_solution) coins: Set[Coin] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, ) -> Tuple[SpendBundle, Optional[TransactionRecord]]: if coin_announcements_to_consume is not None: coin_announcements_bytes: Optional[Set[bytes32]] = { a.name() for a in coin_announcements_to_consume } else: coin_announcements_bytes = None if puzzle_announcements_to_consume is not None: puzzle_announcements_bytes: Optional[Set[bytes32]] = { a.name() for a in puzzle_announcements_to_consume } else: puzzle_announcements_bytes = None if cat_discrepancy is not None: extra_delta, limitations_solution = cat_discrepancy else: extra_delta, limitations_solution = 0, Program.to([]) payment_amount: int = sum([p.amount for p in payments]) starting_amount: int = payment_amount - extra_delta if coins is None: cat_coins = await self.select_coins(uint64(starting_amount)) else: cat_coins = coins selected_cat_amount = sum([c.amount for c in cat_coins]) assert selected_cat_amount >= starting_amount # Figure out if we need to absorb/melt some XCH as part of this regular_chia_to_claim: int = 0 if payment_amount > starting_amount: fee = uint64(fee + payment_amount - starting_amount) elif payment_amount < starting_amount: regular_chia_to_claim = payment_amount need_chia_transaction = (fee > 0 or regular_chia_to_claim > 0) and ( fee - regular_chia_to_claim != 0) # Calculate standard puzzle solutions change = selected_cat_amount - starting_amount primaries: List[AmountWithPuzzlehash] = [] for payment in payments: primaries.append({ "puzzlehash": payment.puzzle_hash, "amount": payment.amount, "memos": payment.memos }) if change > 0: changepuzzlehash = await self.get_new_inner_hash() primaries.append({ "puzzlehash": changepuzzlehash, "amount": uint64(change), "memos": [] }) limitations_program_reveal = Program.to([]) if self.cat_info.my_tail is None: assert cat_discrepancy is None elif cat_discrepancy is not None: limitations_program_reveal = self.cat_info.my_tail # Loop through the coins we've selected and gather the information we need to spend them spendable_cc_list = [] chia_tx = None first = True for coin in cat_coins: if first: first = False if need_chia_transaction: if fee > regular_chia_to_claim: announcement = Announcement(coin.name(), b"$", b"\xca") chia_tx, _ = await self.create_tandem_xch_tx( fee, uint64(regular_chia_to_claim), announcement_to_assert=announcement) innersol = self.standard_wallet.make_solution( primaries=primaries, coin_announcements={announcement.message}, coin_announcements_to_assert= coin_announcements_bytes, puzzle_announcements_to_assert= puzzle_announcements_bytes, ) elif regular_chia_to_claim > fee: chia_tx, _ = await self.create_tandem_xch_tx( fee, uint64(regular_chia_to_claim)) innersol = self.standard_wallet.make_solution( primaries=primaries, coin_announcements_to_assert={announcement.name()}) else: innersol = self.standard_wallet.make_solution( primaries=primaries, coin_announcements_to_assert=coin_announcements_bytes, puzzle_announcements_to_assert= puzzle_announcements_bytes, ) else: innersol = self.standard_wallet.make_solution(primaries=[]) inner_puzzle = await self.inner_puzzle_for_cc_puzhash( coin.puzzle_hash) lineage_proof = await self.get_lineage_proof_for_coin(coin) assert lineage_proof is not None new_spendable_cc = SpendableCAT( coin, self.cat_info.limitations_program_hash, inner_puzzle, innersol, limitations_solution=limitations_solution, extra_delta=extra_delta, lineage_proof=lineage_proof, limitations_program_reveal=limitations_program_reveal, ) spendable_cc_list.append(new_spendable_cc) cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats( CAT_MOD, spendable_cc_list) chia_spend_bundle = SpendBundle([], G2Element()) if chia_tx is not None and chia_tx.spend_bundle is not None: chia_spend_bundle = chia_tx.spend_bundle return ( SpendBundle.aggregate([ cat_spend_bundle, chia_spend_bundle, ]), chia_tx, )
async def test_assert_announcement_consumed(self, two_nodes): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] receiver_puzzlehash = BURN_PUZZLE_HASH # Farm blocks blocks = bt.get_consecutive_blocks( num_blocks, farmer_reward_puzzle_hash=coinbase_puzzlehash, guarantee_transaction_block=True) full_node_api_1, full_node_api_2, server_1, server_2 = two_nodes full_node_1 = full_node_api_1.full_node for block in blocks: await full_node_api_1.full_node.respond_block( full_node_protocol.RespondBlock(block)) # Coinbase that gets spent block1 = blocks[2] block2 = blocks[3] spend_coin_block_1 = None spend_coin_block_2 = None for coin in list(block1.get_included_reward_coins()): if coin.puzzle_hash == coinbase_puzzlehash: spend_coin_block_1 = coin for coin in list(block2.get_included_reward_coins()): if coin.puzzle_hash == coinbase_puzzlehash: spend_coin_block_2 = coin # This condition requires block2 coinbase to be spent block1_cvp = ConditionWithArgs( ConditionOpcode.ASSERT_ANNOUNCEMENT, [ Announcement(spend_coin_block_2.name(), bytes("test", "utf-8")).name() ], ) block1_dic = {block1_cvp.opcode: [block1_cvp]} block1_spend_bundle = wallet_a.generate_signed_transaction( 1000, receiver_puzzlehash, spend_coin_block_1, block1_dic) # This condition requires block1 coinbase to be spent block2_cvp = ConditionWithArgs( ConditionOpcode.CREATE_ANNOUNCEMENT, [bytes("test", "utf-8")], ) block2_dic = {block2_cvp.opcode: [block2_cvp]} block2_spend_bundle = wallet_a.generate_signed_transaction( 1000, receiver_puzzlehash, spend_coin_block_2, block2_dic) # Invalid block bundle assert block1_spend_bundle is not None # Create another block that includes our transaction invalid_new_blocks = bt.get_consecutive_blocks( 1, blocks, farmer_reward_puzzle_hash=coinbase_puzzlehash, transaction_data=block1_spend_bundle, guarantee_transaction_block=True, ) # Try to validate that block res, err, _ = await full_node_1.blockchain.receive_block( invalid_new_blocks[-1]) assert res == ReceiveBlockResult.INVALID_BLOCK assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED # bundle_together contains both transactions bundle_together = SpendBundle.aggregate( [block1_spend_bundle, block2_spend_bundle]) # Create another block that includes our transaction new_blocks = bt.get_consecutive_blocks( 1, blocks, farmer_reward_puzzle_hash=coinbase_puzzlehash, transaction_data=bundle_together, guarantee_transaction_block=True, ) # Try to validate newly created block res, err, _ = await full_node_1.blockchain.receive_block(new_blocks[-1] ) assert res == ReceiveBlockResult.NEW_PEAK assert err is None
async def generate_new_decentralised_id( self, amount: uint64) -> Optional[SpendBundle]: """ This must be called under the wallet state manager lock """ coins = await self.standard_wallet.select_coins(amount) if coins is None: return None origin = coins.copy().pop() genesis_launcher_puz = did_wallet_puzzles.SINGLETON_LAUNCHER launcher_coin = Coin(origin.name(), genesis_launcher_puz.get_tree_hash(), amount) did_inner: Program = await self.get_new_innerpuz() did_inner_hash = did_inner.get_tree_hash() did_full_puz = did_wallet_puzzles.create_fullpuz( did_inner, launcher_coin.name()) did_puzzle_hash = did_full_puz.get_tree_hash() announcement_set: Set[Announcement] = set() announcement_message = Program.to( [did_puzzle_hash, amount, bytes(0x80)]).get_tree_hash() announcement_set.add( Announcement(launcher_coin.name(), announcement_message).name()) tx_record: Optional[ TransactionRecord] = await self.standard_wallet.generate_signed_transaction( amount, genesis_launcher_puz.get_tree_hash(), uint64(0), origin.name(), coins, None, False, announcement_set) genesis_launcher_solution = Program.to( [did_puzzle_hash, amount, bytes(0x80)]) launcher_cs = CoinSolution(launcher_coin, genesis_launcher_puz, genesis_launcher_solution) launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) eve_coin = Coin(launcher_coin.name(), did_puzzle_hash, amount) future_parent = LineageProof( eve_coin.parent_coin_info, did_inner_hash, eve_coin.amount, ) eve_parent = LineageProof( launcher_coin.parent_coin_info, launcher_coin.puzzle_hash, launcher_coin.amount, ) await self.add_parent(eve_coin.parent_coin_info, eve_parent, False) await self.add_parent(eve_coin.name(), future_parent, False) if tx_record is None or 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, self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed, self.did_info.parent_info, did_inner, None, None, None, ) await self.save_info(did_info, False) eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner) full_spend = SpendBundle.aggregate( [tx_record.spend_bundle, eve_spend, launcher_sb]) return full_spend
async def respond_to_offer( self, file_path: Path ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: has_wallets = await self.maybe_create_wallets_for_offer(file_path) if not has_wallets: return False, None, "Unknown Error" trade_offer = None try: trade_offer_hex = file_path.read_text() trade_offer = TradeRecord.from_bytes( hexstr_to_bytes(trade_offer_hex)) except Exception as e: return False, None, f"Error: {e}" if trade_offer is not None: offer_spend_bundle: SpendBundle = trade_offer.spend_bundle coinsols: List[CoinSolution] = [] # [] of CoinSolutions cc_coinsol_outamounts: Dict[bytes32, List[Tuple[CoinSolution, int]]] = dict() aggsig = offer_spend_bundle.aggregated_signature cc_discrepancies: Dict[bytes32, int] = dict() chia_discrepancy = None wallets: Dict[bytes32, Any] = dict() # colour to wallet dict for coinsol in offer_spend_bundle.coin_solutions: puzzle: Program = Program.from_bytes(bytes(coinsol.puzzle_reveal)) solution: Program = Program.from_bytes(bytes(coinsol.solution)) # work out the deficits between coin amount and expected output for each r = cc_utils.uncurry_cc(puzzle) if r: # Calculate output amounts mod_hash, genesis_checker, inner_puzzle = r colour = bytes(genesis_checker).hex() if colour not in wallets: wallets[ colour] = await self.wallet_state_manager.get_wallet_for_colour( colour) unspent = await self.wallet_state_manager.get_spendable_coins_for_wallet( wallets[colour].id()) if coinsol.coin in [record.coin for record in unspent]: return False, None, "can't respond to own offer" innersol = solution.first() total = get_output_amount_for_puzzle_and_solution( inner_puzzle, innersol) if colour in cc_discrepancies: cc_discrepancies[colour] += coinsol.coin.amount - total else: cc_discrepancies[colour] = coinsol.coin.amount - total # Store coinsol and output amount for later if colour in cc_coinsol_outamounts: cc_coinsol_outamounts[colour].append((coinsol, total)) else: cc_coinsol_outamounts[colour] = [(coinsol, total)] else: # standard chia coin unspent = await self.wallet_state_manager.get_spendable_coins_for_wallet( 1) if coinsol.coin in [record.coin for record in unspent]: return False, None, "can't respond to own offer" if chia_discrepancy is None: chia_discrepancy = get_output_discrepancy_for_puzzle_and_solution( coinsol.coin, puzzle, solution) else: chia_discrepancy += get_output_discrepancy_for_puzzle_and_solution( coinsol.coin, puzzle, solution) coinsols.append(coinsol) chia_spend_bundle: Optional[SpendBundle] = None if chia_discrepancy is not None: chia_spend_bundle = await self.wallet_state_manager.main_wallet.create_spend_bundle_relative_chia( chia_discrepancy, []) if chia_spend_bundle is not None: for coinsol in coinsols: chia_spend_bundle.coin_solutions.append(coinsol) zero_spend_list: List[SpendBundle] = [] spend_bundle = None # create coloured coin self.log.info(cc_discrepancies) for colour in cc_discrepancies.keys(): if cc_discrepancies[colour] < 0: my_cc_spends = await wallets[colour].select_coins( abs(cc_discrepancies[colour])) else: if chia_spend_bundle is None: to_exclude: List = [] else: to_exclude = chia_spend_bundle.removals() my_cc_spends = await wallets[colour].select_coins(0) if my_cc_spends is None or my_cc_spends == set(): zero_spend_bundle: SpendBundle = await wallets[ colour].generate_zero_val_coin(False, to_exclude) if zero_spend_bundle is None: return ( False, None, "Unable to generate zero value coin. Confirm that you have chia available", ) zero_spend_list.append(zero_spend_bundle) additions = zero_spend_bundle.additions() removals = zero_spend_bundle.removals() my_cc_spends = set() for add in additions: if add not in removals and add.amount == 0: my_cc_spends.add(add) if my_cc_spends == set() or my_cc_spends is None: return False, None, "insufficient funds" # Create SpendableCC list and innersol_list with both my coins and the offered coins # Firstly get the output coin my_output_coin = my_cc_spends.pop() spendable_cc_list = [] innersol_list = [] genesis_id = genesis_coin_id_for_genesis_coin_checker( Program.from_bytes(bytes.fromhex(colour))) # Make the rest of the coins assert the output coin is consumed for coloured_coin in my_cc_spends: inner_solution = self.wallet_state_manager.main_wallet.make_solution( consumed=[my_output_coin.name()]) inner_puzzle = await self.get_inner_puzzle_for_puzzle_hash( coloured_coin.puzzle_hash) assert inner_puzzle is not None sigs = await wallets[colour].get_sigs(inner_puzzle, inner_solution, coloured_coin.name()) sigs.append(aggsig) aggsig = AugSchemeMPL.aggregate(sigs) lineage_proof = await wallets[ colour].get_lineage_proof_for_coin(coloured_coin) spendable_cc_list.append( SpendableCC(coloured_coin, genesis_id, inner_puzzle, lineage_proof)) innersol_list.append(inner_solution) # Create SpendableCC for each of the coloured coins received for cc_coinsol_out in cc_coinsol_outamounts[colour]: cc_coinsol = cc_coinsol_out[0] puzzle = Program.from_bytes(bytes(cc_coinsol.puzzle_reveal)) solution = Program.from_bytes(bytes(cc_coinsol.solution)) r = uncurry_cc(puzzle) if r: mod_hash, genesis_coin_checker, inner_puzzle = r inner_solution = solution.first() lineage_proof = solution.rest().rest().first() spendable_cc_list.append( SpendableCC(cc_coinsol.coin, genesis_id, inner_puzzle, lineage_proof)) innersol_list.append(inner_solution) # Finish the output coin SpendableCC with new information newinnerpuzhash = await wallets[colour].get_new_inner_hash() outputamount = sum([ c.amount for c in my_cc_spends ]) + cc_discrepancies[colour] + my_output_coin.amount inner_solution = self.wallet_state_manager.main_wallet.make_solution( primaries=[{ "puzzlehash": newinnerpuzhash, "amount": outputamount }]) inner_puzzle = await self.get_inner_puzzle_for_puzzle_hash( my_output_coin.puzzle_hash) assert inner_puzzle is not None lineage_proof = await wallets[colour].get_lineage_proof_for_coin( my_output_coin) spendable_cc_list.append( SpendableCC(my_output_coin, genesis_id, inner_puzzle, lineage_proof)) innersol_list.append(inner_solution) sigs = await wallets[colour].get_sigs(inner_puzzle, inner_solution, my_output_coin.name()) sigs.append(aggsig) aggsig = AugSchemeMPL.aggregate(sigs) if spend_bundle is None: spend_bundle = spend_bundle_for_spendable_ccs( CC_MOD, Program.from_bytes(bytes.fromhex(colour)), spendable_cc_list, innersol_list, [aggsig], ) else: new_spend_bundle = spend_bundle_for_spendable_ccs( CC_MOD, Program.from_bytes(bytes.fromhex(colour)), spendable_cc_list, innersol_list, [aggsig], ) spend_bundle = SpendBundle.aggregate( [spend_bundle, new_spend_bundle]) # reset sigs and aggsig so that they aren't included next time around sigs = [] aggsig = AugSchemeMPL.aggregate(sigs) my_tx_records = [] if zero_spend_list is not None and spend_bundle is not None: zero_spend_list.append(spend_bundle) spend_bundle = SpendBundle.aggregate(zero_spend_list) if spend_bundle is None: return False, None, "spend_bundle missing" # Add transaction history for this trade now = uint64(int(time.time())) if chia_spend_bundle is not None: spend_bundle = SpendBundle.aggregate( [spend_bundle, chia_spend_bundle]) # debug_spend_bundle(spend_bundle) if chia_discrepancy < 0: tx_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=now, to_puzzle_hash=token_bytes(), amount=uint64(abs(chia_discrepancy)), fee_amount=uint64(0), confirmed=False, sent=uint32(10), spend_bundle=chia_spend_bundle, additions=chia_spend_bundle.additions(), removals=chia_spend_bundle.removals(), wallet_id=uint32(1), sent_to=[], trade_id=std_hash(spend_bundle.name() + bytes(now)), type=uint32(TransactionType.OUTGOING_TRADE.value), name=chia_spend_bundle.name(), ) else: tx_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), to_puzzle_hash=token_bytes(), amount=uint64(abs(chia_discrepancy)), fee_amount=uint64(0), confirmed=False, sent=uint32(10), spend_bundle=chia_spend_bundle, additions=chia_spend_bundle.additions(), removals=chia_spend_bundle.removals(), wallet_id=uint32(1), sent_to=[], trade_id=std_hash(spend_bundle.name() + bytes(now)), type=uint32(TransactionType.INCOMING_TRADE.value), name=chia_spend_bundle.name(), ) my_tx_records.append(tx_record) for colour, amount in cc_discrepancies.items(): wallet = wallets[colour] if chia_discrepancy > 0: tx_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), to_puzzle_hash=token_bytes(), amount=uint64(abs(amount)), fee_amount=uint64(0), confirmed=False, sent=uint32(10), spend_bundle=spend_bundle, additions=spend_bundle.additions(), removals=spend_bundle.removals(), wallet_id=wallet.id(), sent_to=[], trade_id=std_hash(spend_bundle.name() + bytes(now)), type=uint32(TransactionType.OUTGOING_TRADE.value), name=spend_bundle.name(), ) else: tx_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), to_puzzle_hash=token_bytes(), amount=uint64(abs(amount)), fee_amount=uint64(0), confirmed=False, sent=uint32(10), spend_bundle=spend_bundle, additions=spend_bundle.additions(), removals=spend_bundle.removals(), wallet_id=wallet.id(), sent_to=[], trade_id=std_hash(spend_bundle.name() + bytes(now)), type=uint32(TransactionType.INCOMING_TRADE.value), name=token_bytes(), ) my_tx_records.append(tx_record) tx_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), to_puzzle_hash=token_bytes(), amount=uint64(0), fee_amount=uint64(0), confirmed=False, sent=uint32(0), spend_bundle=spend_bundle, additions=spend_bundle.additions(), removals=spend_bundle.removals(), wallet_id=uint32(0), sent_to=[], trade_id=std_hash(spend_bundle.name() + bytes(now)), type=uint32(TransactionType.OUTGOING_TRADE.value), name=spend_bundle.name(), ) now = uint64(int(time.time())) trade_record: TradeRecord = TradeRecord( confirmed_at_index=uint32(0), accepted_at_time=now, created_at_time=now, my_offer=False, sent=uint32(0), spend_bundle=offer_spend_bundle, tx_spend_bundle=spend_bundle, additions=spend_bundle.additions(), removals=spend_bundle.removals(), trade_id=std_hash(spend_bundle.name() + bytes(now)), status=uint32(TradeStatus.PENDING_CONFIRM.value), sent_to=[], ) await self.save_trade(trade_record) await self.wallet_state_manager.add_pending_transaction(tx_record) for tx in my_tx_records: await self.wallet_state_manager.add_transaction(tx) return True, trade_record, None
async def _create_offer_for_ids( self, offer: Dict[int, int] ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: """ Offer is dictionary of wallet ids and amount """ spend_bundle = None try: for id in offer.keys(): amount = offer[id] wallet_id = uint32(int(id)) wallet = self.wallet_state_manager.wallets[wallet_id] if isinstance(wallet, CCWallet): balance = await wallet.get_confirmed_balance() if balance < abs(amount) and amount < 0: raise Exception( f"insufficient funds in wallet {wallet_id}") if amount > 0: if spend_bundle is None: to_exclude: List[Coin] = [] else: to_exclude = spend_bundle.removals() zero_spend_bundle: SpendBundle = await wallet.generate_zero_val_coin( False, to_exclude) if spend_bundle is None: spend_bundle = zero_spend_bundle else: spend_bundle = SpendBundle.aggregate( [spend_bundle, zero_spend_bundle]) additions = zero_spend_bundle.additions() removals = zero_spend_bundle.removals() zero_val_coin: Optional[Coin] = None for add in additions: if add not in removals and add.amount == 0: zero_val_coin = add new_spend_bundle = await wallet.create_spend_bundle_relative_amount( amount, zero_val_coin) else: new_spend_bundle = await wallet.create_spend_bundle_relative_amount( amount) elif isinstance(wallet, Wallet): if spend_bundle is None: to_exclude = [] else: to_exclude = spend_bundle.removals() new_spend_bundle = await wallet.create_spend_bundle_relative_chia( amount, to_exclude) else: return False, None, "unsupported wallet type" if new_spend_bundle is None or new_spend_bundle.removals( ) == []: raise Exception(f"Wallet {id} was unable to create offer.") if spend_bundle is None: spend_bundle = new_spend_bundle else: spend_bundle = SpendBundle.aggregate( [spend_bundle, new_spend_bundle]) if spend_bundle is None: return False, None, None now = uint64(int(time.time())) trade_offer: TradeRecord = TradeRecord( confirmed_at_index=uint32(0), accepted_at_time=None, created_at_time=now, my_offer=True, sent=uint32(0), spend_bundle=spend_bundle, tx_spend_bundle=None, additions=spend_bundle.additions(), removals=spend_bundle.removals(), trade_id=std_hash(spend_bundle.name() + bytes(now)), status=uint32(TradeStatus.PENDING_ACCEPT.value), sent_to=[], ) return True, trade_offer, None except Exception as e: tb = traceback.format_exc() self.log.error(f"Error with creating trade offer: {type(e)}{tb}") return False, None, str(e)
async def _create_offer_for_ids( self, offer_dict: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0) ) -> Tuple[bool, Optional[Offer], Optional[str]]: """ Offer is dictionary of wallet ids and amount """ try: coins_to_offer: Dict[uint32, List[Coin]] = {} requested_payments: Dict[Optional[bytes32], List[Payment]] = {} for id, amount in offer_dict.items(): if amount > 0: if isinstance(id, int): wallet_id = uint32(id) wallet = self.wallet_state_manager.wallets[wallet_id] p2_ph: bytes32 = await wallet.get_new_puzzlehash() if wallet.type() == WalletType.STANDARD_WALLET: key: Optional[bytes32] = None memos: List[bytes] = [] elif wallet.type() == WalletType.CAT: key = bytes32(bytes.fromhex(wallet.get_asset_id())) memos = [p2_ph] else: raise ValueError(f"Offers are not implemented for {wallet.type()}") else: p2_ph = await self.wallet_state_manager.main_wallet.get_new_puzzlehash() key = id memos = [p2_ph] requested_payments[key] = [Payment(p2_ph, uint64(amount), memos)] elif amount < 0: assert isinstance(id, int) wallet_id = uint32(id) wallet = self.wallet_state_manager.wallets[wallet_id] balance = await wallet.get_confirmed_balance() if balance < abs(amount): raise Exception(f"insufficient funds in wallet {wallet_id}") coins_to_offer[wallet_id] = await wallet.select_coins(uint64(abs(amount))) elif amount == 0: raise ValueError("You cannot offer nor request 0 amount of something") 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( requested_payments, all_coins ) announcements_to_assert = Offer.calculate_announcements(notarized_payments) all_transactions: List[TransactionRecord] = [] fee_left_to_pay: uint64 = fee for wallet_id, selected_coins in coins_to_offer.items(): wallet = self.wallet_state_manager.wallets[wallet_id] # This should probably not switch on whether or not we're spending a CAT but it has to for now if wallet.type() == WalletType.CAT: txs = await wallet.generate_signed_transaction( [abs(offer_dict[int(wallet_id)])], [Offer.ph()], fee=fee_left_to_pay, coins=set(selected_coins), puzzle_announcements_to_consume=announcements_to_assert, ) all_transactions.extend(txs) else: tx = await wallet.generate_signed_transaction( abs(offer_dict[int(wallet_id)]), Offer.ph(), fee=fee_left_to_pay, coins=set(selected_coins), puzzle_announcements_to_consume=announcements_to_assert, ) all_transactions.append(tx) fee_left_to_pay = uint64(0) transaction_bundles: List[Optional[SpendBundle]] = [tx.spend_bundle for tx in all_transactions] total_spend_bundle = SpendBundle.aggregate(list(filter(lambda b: b is not None, transaction_bundles))) offer = Offer(notarized_payments, total_spend_bundle) return True, offer, None except Exception as e: tb = traceback.format_exc() self.log.error(f"Error with creating trade offer: {type(e)}{tb}") return False, None, str(e)
def to_valid_spend(self, arbitrage_ph: Optional[bytes32] = None) -> SpendBundle: if not self.is_valid(): raise ValueError("Offer is currently incomplete") completion_spends: List[CoinSpend] = [] for tail_hash, payments in self.requested_payments.items(): offered_coins: List[Coin] = self.get_offered_coins()[tail_hash] # Because of CAT supply laws, we must specify a place for the leftovers to go arbitrage_amount: int = self.arbitrage()[tail_hash] all_payments: List[NotarizedPayment] = payments.copy() if arbitrage_amount > 0: assert arbitrage_amount is not None assert arbitrage_ph is not None all_payments.append( NotarizedPayment(arbitrage_ph, uint64(arbitrage_amount), [])) for coin in offered_coins: inner_solutions = [] if coin == offered_coins[0]: nonces: List[bytes32] = [p.nonce for p in all_payments] for nonce in list(dict.fromkeys( nonces)): # dedup without messing with order nonce_payments: List[NotarizedPayment] = list( filter(lambda p: p.nonce == nonce, all_payments)) inner_solutions.append( (nonce, [np.as_condition_args() for np in nonce_payments])) if tail_hash: # CATs have a special way to be solved so we have to do some calculation before getting the solution parent_spend: CoinSpend = list( filter( lambda cs: cs.coin.name() == coin.parent_coin_info, self.bundle.coin_spends))[0] parent_coin: Coin = parent_spend.coin matched, curried_args = match_cat_puzzle( parent_spend.puzzle_reveal.to_program()) assert matched _, _, inner_puzzle = curried_args spendable_cat = SpendableCAT( coin, tail_hash, OFFER_MOD, Program.to(inner_solutions), lineage_proof=LineageProof( parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount), ) solution: Program = ( unsigned_spend_bundle_for_spendable_cats( CAT_MOD, [spendable_cat ]).coin_spends[0].solution.to_program()) else: solution = Program.to(inner_solutions) completion_spends.append( CoinSpend( coin, construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD, solution, )) return SpendBundle.aggregate( [SpendBundle(completion_spends, G2Element()), self.bundle])