mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-11-10 12:29:49 +03:00
fn speedup, transaction cost limit, max send amount
This commit is contained in:
parent
6f5fbfc071
commit
f0a317761a
@ -15,7 +15,6 @@ from src.consensus.coinbase import create_pool_coin, create_farmer_coin
|
||||
from src.consensus.constants import ConsensusConstants
|
||||
from src.full_node.bundle_tools import best_solution_program
|
||||
from src.consensus.cost_calculator import calculate_cost_of_program, CostResult
|
||||
from src.full_node.mempool_check_conditions import get_name_puzzle_conditions
|
||||
from src.full_node.signage_point import SignagePoint
|
||||
from src.consensus.block_record import BlockRecord
|
||||
from src.types.blockchain_format.coin import Coin, hash_coin_list
|
||||
@ -27,10 +26,10 @@ from src.types.blockchain_format.foliage import (
|
||||
FoliageBlockData,
|
||||
)
|
||||
from src.types.full_block import additions_for_npc, FullBlock
|
||||
from src.types.blockchain_format.pool_target import PoolTarget
|
||||
from src.types.blockchain_format.program import SerializedProgram
|
||||
from src.types.blockchain_format.proof_of_space import ProofOfSpace
|
||||
from src.types.blockchain_format.reward_chain_block import (
|
||||
from src.types.pool_target import PoolTarget
|
||||
from src.types.program import SerializedProgram
|
||||
from src.types.proof_of_space import ProofOfSpace
|
||||
from src.types.reward_chain_block import (
|
||||
RewardChainBlockUnfinished,
|
||||
RewardChainBlock,
|
||||
)
|
||||
@ -49,6 +48,8 @@ def create_foliage(
|
||||
constants: ConsensusConstants,
|
||||
reward_block_unfinished: RewardChainBlockUnfinished,
|
||||
spend_bundle: Optional[SpendBundle],
|
||||
additions: List[Coin],
|
||||
removals: List[Coin],
|
||||
prev_block: Optional[BlockRecord],
|
||||
blocks: BlockchainInterface,
|
||||
total_iters_sp: uint128,
|
||||
@ -132,13 +133,22 @@ def create_foliage(
|
||||
|
||||
if spend_bundle is not None:
|
||||
solution_program = best_solution_program(spend_bundle)
|
||||
spend_bundle_fees = spend_bundle.fees()
|
||||
aggregate_sig = spend_bundle.aggregated_signature
|
||||
|
||||
# Calculate the cost of transactions
|
||||
if solution_program is not None:
|
||||
result: CostResult = calculate_cost_of_program(solution_program, constants.CLVM_COST_RATIO_CONSTANT)
|
||||
cost = result.cost
|
||||
removal_amount = 0
|
||||
addition_amount = 0
|
||||
for coin in removals:
|
||||
removal_amount += coin.amount
|
||||
for coin in additions:
|
||||
addition_amount += coin.amount
|
||||
spend_bundle_fees = removal_amount - addition_amount
|
||||
else:
|
||||
spend_bundle_fees = 0
|
||||
|
||||
# TODO: prev generators root
|
||||
reward_claims_incorporated = []
|
||||
if height > 0:
|
||||
@ -179,17 +189,13 @@ def create_foliage(
|
||||
)
|
||||
reward_claims_incorporated += [pool_coin, farmer_coin]
|
||||
curr = blocks.block_record(curr.prev_hash)
|
||||
additions: List[Coin] = reward_claims_incorporated.copy()
|
||||
npc_list = []
|
||||
if solution_program is not None:
|
||||
error, npc_list, _ = get_name_puzzle_conditions(solution_program, False)
|
||||
additions += additions_for_npc(npc_list)
|
||||
additions.extend(reward_claims_incorporated.copy())
|
||||
for coin in additions:
|
||||
tx_additions.append(coin)
|
||||
byte_array_tx.append(bytearray(coin.puzzle_hash))
|
||||
for npc in npc_list:
|
||||
tx_removals.append(npc.coin_name)
|
||||
byte_array_tx.append(bytearray(npc.coin_name))
|
||||
for coin in removals:
|
||||
tx_removals.append(coin.name())
|
||||
byte_array_tx.append(bytearray(coin.name()))
|
||||
|
||||
bip158: PyBIP158 = PyBIP158(byte_array_tx)
|
||||
encoded = bytes(bip158.GetEncoded())
|
||||
@ -287,6 +293,8 @@ def create_unfinished_block(
|
||||
blocks: BlockchainInterface,
|
||||
seed: bytes32 = b"",
|
||||
spend_bundle: Optional[SpendBundle] = None,
|
||||
additions: Optional[List[Coin]] = None,
|
||||
removals: Optional[List[Coin]] = None,
|
||||
prev_block: Optional[BlockRecord] = None,
|
||||
finished_sub_slots_input: List[EndOfSubSlotBundle] = None,
|
||||
) -> UnfinishedBlock:
|
||||
@ -311,6 +319,8 @@ def create_unfinished_block(
|
||||
timestamp: timestamp to add to the foliage block, if created
|
||||
seed: seed to randomize chain
|
||||
spend_bundle: transactions to add to the foliage block, if created
|
||||
additions: Coins added in spend_bundle
|
||||
removals: Coins removed in spend_bundle
|
||||
prev_block: previous block (already in chain) from the signage point
|
||||
blocks: dictionary from header hash to SBR of all included SBR
|
||||
finished_sub_slots_input: finished_sub_slots at the signage point
|
||||
@ -369,11 +379,16 @@ def create_unfinished_block(
|
||||
signage_point.rc_vdf,
|
||||
rc_sp_signature,
|
||||
)
|
||||
|
||||
if additions is None:
|
||||
additions = []
|
||||
if removals is None:
|
||||
removals = []
|
||||
(foliage, foliage_transaction_block, transactions_info, solution_program,) = create_foliage(
|
||||
constants,
|
||||
rc_block,
|
||||
spend_bundle,
|
||||
additions,
|
||||
removals,
|
||||
prev_block,
|
||||
blocks,
|
||||
total_iters_sp,
|
||||
|
@ -641,8 +641,18 @@ class FullNodeAPI:
|
||||
peak: Optional[BlockRecord] = self.full_node.blockchain.get_peak()
|
||||
if peak is None:
|
||||
spend_bundle: Optional[SpendBundle] = None
|
||||
additions = None
|
||||
removals = None
|
||||
else:
|
||||
spend_bundle = await self.full_node.mempool_manager.create_bundle_from_mempool(peak.header_hash)
|
||||
mempool_bundle = await self.full_node.mempool_manager.create_bundle_from_mempool(peak.header_hash)
|
||||
if mempool_bundle is None:
|
||||
spend_bundle = None
|
||||
additions = None
|
||||
removals = None
|
||||
else:
|
||||
spend_bundle = mempool_bundle[0]
|
||||
additions = mempool_bundle[1]
|
||||
removals = mempool_bundle[2]
|
||||
|
||||
def get_plot_sig(to_sign, _) -> G2Element:
|
||||
if to_sign == request.challenge_chain_sp:
|
||||
@ -764,6 +774,8 @@ class FullNodeAPI:
|
||||
self.full_node.blockchain,
|
||||
b"",
|
||||
spend_bundle,
|
||||
additions,
|
||||
removals,
|
||||
prev_b,
|
||||
finished_sub_slots,
|
||||
)
|
||||
@ -1030,6 +1042,7 @@ class FullNodeAPI:
|
||||
error: Optional[Err] = Err.NO_TRANSACTIONS_WHILE_SYNCING
|
||||
else:
|
||||
cost_result = await self.full_node.mempool_manager.pre_validate_spendbundle(request.transaction)
|
||||
|
||||
async with self.full_node.blockchain.lock:
|
||||
cost, status, error = await self.full_node.mempool_manager.add_spendbundle(
|
||||
request.transaction, cost_result, spend_name
|
||||
@ -1038,13 +1051,15 @@ class FullNodeAPI:
|
||||
self.log.debug(f"Added transaction to mempool: {spend_name}")
|
||||
# Only broadcast successful transactions, not pending ones. Otherwise it's a DOS
|
||||
# vector.
|
||||
fees = request.transaction.fees()
|
||||
mempool_item = self.full_node.mempool_manager.get_mempool_item(spend_name)
|
||||
assert mempool_item is not None
|
||||
fees = mempool_item.fee
|
||||
assert fees >= 0
|
||||
assert cost is not None
|
||||
new_tx = full_node_protocol.NewTransaction(
|
||||
spend_name,
|
||||
cost,
|
||||
uint64(request.transaction.fees()),
|
||||
uint64(fees),
|
||||
)
|
||||
msg = make_msg(ProtocolMessageTypes.new_transaction, new_tx)
|
||||
await self.full_node.server.send_to_all([msg], NodeType.FULL_NODE)
|
||||
|
@ -9,7 +9,7 @@ from src.types.blockchain_format.sized_bytes import bytes32
|
||||
|
||||
class Mempool:
|
||||
spends: Dict[bytes32, MempoolItem]
|
||||
sorted_spends: SortedDict
|
||||
sorted_spends: SortedDict # Dict[float, Dict[bytes32, MempoolItem]]
|
||||
additions: Dict[bytes32, MempoolItem]
|
||||
removals: Dict[bytes32, MempoolItem]
|
||||
size: int
|
||||
|
@ -78,7 +78,9 @@ class MempoolManager:
|
||||
def shut_down(self):
|
||||
self.pool.shutdown(wait=True)
|
||||
|
||||
async def create_bundle_from_mempool(self, peak_header_hash: bytes32) -> Optional[SpendBundle]:
|
||||
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
|
||||
"""
|
||||
@ -92,6 +94,8 @@ class MempoolManager:
|
||||
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 = []
|
||||
for dic in self.mempool.sorted_spends.values():
|
||||
for item in dic.values():
|
||||
if (
|
||||
@ -101,10 +105,12 @@ class MempoolManager:
|
||||
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:
|
||||
break
|
||||
if len(spend_bundles) > 0:
|
||||
return SpendBundle.aggregate(spend_bundles)
|
||||
return SpendBundle.aggregate(spend_bundles), additions, removals
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -150,10 +156,12 @@ class MempoolManager:
|
||||
Errors are included within the cached_result.
|
||||
This runs in another process so we don't block the main thread
|
||||
"""
|
||||
|
||||
start_time = time.time()
|
||||
cached_result_bytes = await asyncio.get_running_loop().run_in_executor(
|
||||
self.pool, validate_transaction_multiprocess, self.constants_json, bytes(new_spend)
|
||||
)
|
||||
end_time = time.time()
|
||||
log.info(f"It took {end_time - start_time} to pre validate transaction")
|
||||
return CostResult.from_bytes(cached_result_bytes)
|
||||
|
||||
async def add_spendbundle(
|
||||
@ -342,7 +350,8 @@ class MempoolManager:
|
||||
for mempool_item in conflicting_pool_items.values():
|
||||
self.mempool.remove_spend(mempool_item)
|
||||
|
||||
new_item = MempoolItem(new_spend, fees_per_cost, uint64(fees), cost_result, spend_name)
|
||||
removals: List[Coin] = [coin for coin in removal_coin_dict.values()]
|
||||
new_item = MempoolItem(new_spend, fees_per_cost, uint64(fees), cost_result, spend_name, additions, removals)
|
||||
self.mempool.add_to_pool(new_item, additions, removal_coin_dict)
|
||||
log.info(f"add_spendbundle took {time.time() - start_time} seconds")
|
||||
return uint64(cost), MempoolInclusionStatus.SUCCESS, None
|
||||
@ -388,6 +397,12 @@ class MempoolManager:
|
||||
return self.mempool.spends[bundle_hash].spend_bundle
|
||||
return None
|
||||
|
||||
def get_mempool_item(self, bundle_hash: bytes32) -> Optional[MempoolItem]:
|
||||
""" Returns a MempoolItem if it's inside one the mempools"""
|
||||
if bundle_hash in self.mempool.spends:
|
||||
return self.mempool.spends[bundle_hash]
|
||||
return None
|
||||
|
||||
async def new_peak(self, new_peak: Optional[BlockRecord]):
|
||||
"""
|
||||
Called when a new peak is available, we try to recreate a mempool for the new tip.
|
||||
|
@ -372,6 +372,7 @@ class WalletRpcApi:
|
||||
pending_balance = await wallet.get_unconfirmed_balance(unspent_records)
|
||||
spendable_balance = await wallet.get_spendable_balance(unspent_records)
|
||||
pending_change = await wallet.get_pending_change_balance()
|
||||
max_send_amount = await wallet.get_max_send_amount(unspent_records)
|
||||
|
||||
wallet_balance = {
|
||||
"wallet_id": wallet_id,
|
||||
@ -379,6 +380,7 @@ class WalletRpcApi:
|
||||
"unconfirmed_wallet_balance": pending_balance,
|
||||
"spendable_balance": spendable_balance,
|
||||
"pending_change": pending_change,
|
||||
"max_send_amount": max_send_amount,
|
||||
}
|
||||
|
||||
return {"wallet_balance": wallet_balance}
|
||||
|
@ -6,7 +6,6 @@ from src.full_node.full_node_api import FullNodeAPI
|
||||
from src.protocols.full_node_protocol import RespondBlock
|
||||
from src.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol
|
||||
from src.types.full_block import FullBlock
|
||||
from src.types.spend_bundle import SpendBundle
|
||||
|
||||
from src.util.api_decorators import api_request
|
||||
from src.util.ints import uint8
|
||||
@ -52,14 +51,22 @@ class FullNodeSimulator(FullNodeAPI):
|
||||
|
||||
peak = self.full_node.blockchain.get_peak()
|
||||
assert peak is not None
|
||||
bundle: Optional[SpendBundle] = await self.full_node.mempool_manager.create_bundle_from_mempool(
|
||||
peak.header_hash
|
||||
)
|
||||
mempool_bundle = await self.full_node.mempool_manager.create_bundle_from_mempool(peak.header_hash)
|
||||
if mempool_bundle is None:
|
||||
spend_bundle = None
|
||||
additions = None
|
||||
removals = None
|
||||
else:
|
||||
spend_bundle = mempool_bundle[0]
|
||||
additions = mempool_bundle[1]
|
||||
removals = mempool_bundle[2]
|
||||
current_blocks = await self.get_all_full_blocks()
|
||||
target = request.puzzle_hash
|
||||
more = self.bt.get_consecutive_blocks(
|
||||
1,
|
||||
transaction_data=bundle,
|
||||
transaction_data=spend_bundle,
|
||||
additions=additions,
|
||||
removals=removals,
|
||||
farmer_reward_puzzle_hash=target,
|
||||
pool_reward_puzzle_hash=target,
|
||||
block_list_input=current_blocks,
|
||||
@ -83,14 +90,22 @@ class FullNodeSimulator(FullNodeAPI):
|
||||
|
||||
peak = self.full_node.blockchain.get_peak()
|
||||
assert peak is not None
|
||||
bundle: Optional[SpendBundle] = await self.full_node.mempool_manager.create_bundle_from_mempool(
|
||||
peak.header_hash
|
||||
)
|
||||
mempool_bundle = await self.full_node.mempool_manager.create_bundle_from_mempool(peak.header_hash)
|
||||
if mempool_bundle is None:
|
||||
spend_bundle = None
|
||||
additions = None
|
||||
removals = None
|
||||
else:
|
||||
spend_bundle = mempool_bundle[0]
|
||||
additions = mempool_bundle[1]
|
||||
removals = mempool_bundle[2]
|
||||
current_blocks = await self.get_all_full_blocks()
|
||||
target = request.puzzle_hash
|
||||
more = self.bt.get_consecutive_blocks(
|
||||
1,
|
||||
transaction_data=bundle,
|
||||
transaction_data=spend_bundle,
|
||||
additions=additions,
|
||||
removals=removals,
|
||||
farmer_reward_puzzle_hash=target,
|
||||
pool_reward_puzzle_hash=target,
|
||||
block_list_input=current_blocks,
|
||||
|
@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from src.consensus.cost_calculator import CostResult
|
||||
from src.types.coin import Coin
|
||||
from src.types.spend_bundle import SpendBundle
|
||||
from src.types.blockchain_format.sized_bytes import bytes32
|
||||
from src.util.ints import uint64
|
||||
@ -13,6 +15,8 @@ class MempoolItem:
|
||||
fee: uint64
|
||||
cost_result: CostResult
|
||||
spend_bundle_name: bytes32
|
||||
additions: List[Coin]
|
||||
removals: List[Coin]
|
||||
|
||||
def __lt__(self, other):
|
||||
# TODO test to see if it's < or >
|
||||
|
@ -34,6 +34,7 @@ from src.consensus.block_record import BlockRecord
|
||||
from src.consensus.vdf_info_computation import get_signage_point_vdf_info
|
||||
from src.plotting.plot_tools import load_plots, PlotInfo
|
||||
from src.types.blockchain_format.classgroup import ClassgroupElement
|
||||
from src.types.coin import Coin
|
||||
from src.types.end_of_slot_bundle import EndOfSubSlotBundle
|
||||
from src.types.full_block import FullBlock
|
||||
from src.types.blockchain_format.pool_target import PoolTarget
|
||||
@ -247,6 +248,8 @@ class BlockTools:
|
||||
farmer_reward_puzzle_hash: Optional[bytes32] = None,
|
||||
pool_reward_puzzle_hash: Optional[bytes32] = None,
|
||||
transaction_data: Optional[SpendBundle] = None,
|
||||
additions: Optional[List[Coin]] = None,
|
||||
removals: Optional[List[Coin]] = None,
|
||||
seed: bytes = b"",
|
||||
time_per_block: Optional[float] = None,
|
||||
force_overflow: bool = False,
|
||||
@ -402,6 +405,8 @@ class BlockTools:
|
||||
start_height,
|
||||
time_per_block,
|
||||
transaction_data,
|
||||
additions,
|
||||
removals,
|
||||
height_to_hash,
|
||||
difficulty,
|
||||
required_iters,
|
||||
@ -620,6 +625,8 @@ class BlockTools:
|
||||
start_height,
|
||||
time_per_block,
|
||||
transaction_data,
|
||||
additions,
|
||||
removals,
|
||||
height_to_hash,
|
||||
difficulty,
|
||||
required_iters,
|
||||
@ -1162,6 +1169,8 @@ def get_full_block_and_sub_record(
|
||||
start_height: uint32,
|
||||
time_per_block: float,
|
||||
transaction_data: Optional[SpendBundle],
|
||||
additions: Optional[List[Coin]],
|
||||
removals: Optional[List[Coin]],
|
||||
height_to_hash: Dict[uint32, bytes32],
|
||||
difficulty: uint64,
|
||||
required_iters: uint64,
|
||||
@ -1195,6 +1204,8 @@ def get_full_block_and_sub_record(
|
||||
BlockCache(blocks),
|
||||
seed,
|
||||
transaction_data,
|
||||
additions,
|
||||
removals,
|
||||
prev_block,
|
||||
finished_sub_slots,
|
||||
)
|
||||
|
@ -7,6 +7,8 @@ from secrets import token_bytes
|
||||
from typing import Dict, Optional, List, Any, Set
|
||||
from blspy import G2Element, AugSchemeMPL
|
||||
|
||||
from src.consensus.cost_calculator import calculate_cost_of_program, CostResult
|
||||
from src.full_node.bundle_tools import best_solution_program
|
||||
from src.protocols.wallet_protocol import PuzzleSolutionResponse
|
||||
from src.types.blockchain_format.coin import Coin
|
||||
from src.types.coin_solution import CoinSolution
|
||||
@ -59,6 +61,7 @@ class CCWallet:
|
||||
standard_wallet: Wallet
|
||||
base_puzzle_program: Optional[bytes]
|
||||
base_inner_puzzle_hash: Optional[bytes32]
|
||||
cost_of_single_tx: Optional[int]
|
||||
|
||||
@staticmethod
|
||||
async def create_new_cc(
|
||||
@ -67,6 +70,7 @@ class CCWallet:
|
||||
amount: uint64,
|
||||
):
|
||||
self = CCWallet()
|
||||
self.cost_of_single_tx = None
|
||||
self.base_puzzle_program = None
|
||||
self.base_inner_puzzle_hash = None
|
||||
self.standard_wallet = wallet
|
||||
@ -150,8 +154,8 @@ class CCWallet:
|
||||
wallet: Wallet,
|
||||
genesis_checker_hex: str,
|
||||
) -> CCWallet:
|
||||
|
||||
self = CCWallet()
|
||||
self.cost_of_single_tx = None
|
||||
self.base_puzzle_program = None
|
||||
self.base_inner_puzzle_hash = None
|
||||
self.standard_wallet = wallet
|
||||
@ -227,6 +231,39 @@ class CCWallet:
|
||||
self.log.info(f"Unconfirmed balance for cc wallet {self.id()} is {result}")
|
||||
return uint128(result)
|
||||
|
||||
async def get_max_send_amount(self, records=None):
|
||||
spendable: List[WalletCoinRecord] = list(
|
||||
await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records)
|
||||
)
|
||||
if len(spendable) == 0:
|
||||
return 0
|
||||
spendable.sort(reverse=True, key=lambda record: record.coin.amount)
|
||||
if self.cost_of_single_tx is None:
|
||||
coin = spendable[0].coin
|
||||
tx = await self.generate_signed_transaction(
|
||||
coin.amount, coin.puzzle_hash, coins={coin}, ignore_max_send=True
|
||||
)
|
||||
program = best_solution_program(tx.spend_bundle)
|
||||
# npc contains names of the coins removed, puzzle_hashes and their spend conditions
|
||||
cost_result: CostResult = calculate_cost_of_program(
|
||||
program, self.wallet_state_manager.constants.CLVM_COST_RATIO_CONSTANT, True
|
||||
)
|
||||
self.cost_of_single_tx = cost_result.cost
|
||||
self.log.info(f"Cost of a single tx for standard wallet: {self.cost_of_single_tx}")
|
||||
|
||||
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
|
||||
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 total_amount
|
||||
|
||||
async def get_name(self):
|
||||
return self.wallet_info.name
|
||||
|
||||
@ -523,19 +560,26 @@ class CCWallet:
|
||||
fee: uint64 = uint64(0),
|
||||
origin_id: bytes32 = None,
|
||||
coins: Set[Coin] = None,
|
||||
ignore_max_sent_amount: bool = False
|
||||
) -> TransactionRecord:
|
||||
sigs: List[G2Element] = []
|
||||
|
||||
# Get coins and calculate amount of change required
|
||||
outgoing_amount = uint64(sum(amounts))
|
||||
total_outgoing = outgoing_amount + fee
|
||||
|
||||
if not ignore_max_sent_amount:
|
||||
max_send = await self.get_max_send_amount()
|
||||
if total_outgoing > max_send:
|
||||
raise ValueError(f"Can't send more than {max_send} in a single transaction")
|
||||
|
||||
if coins is None:
|
||||
selected_coins: Set[Coin] = await self.select_coins(uint64(outgoing_amount + fee))
|
||||
selected_coins: Set[Coin] = await self.select_coins(uint64(total_outgoing))
|
||||
else:
|
||||
selected_coins = coins
|
||||
|
||||
total_amount = sum([x.amount for x in selected_coins])
|
||||
change = total_amount - outgoing_amount - fee
|
||||
change = total_amount - total_outgoing
|
||||
primaries = []
|
||||
for amount, puzzle_hash in zip(amounts, puzzle_hashes):
|
||||
primaries.append({"puzzlehash": puzzle_hash, "amount": amount})
|
||||
|
@ -95,6 +95,6 @@ def solution_with_hidden_puzzle(
|
||||
return Program.to([puzzle, [hidden_public_key, hidden_puzzle, solution_to_hidden_puzzle]])
|
||||
|
||||
|
||||
def solution_for_conditions(conditions: Program) -> Program:
|
||||
def solution_for_conditions(conditions) -> Program:
|
||||
delegated_puzzle = puzzle_for_conditions(conditions)
|
||||
return solution_for_delegated_puzzle(delegated_puzzle, Program.to(0))
|
||||
|
@ -346,6 +346,10 @@ class RLWallet:
|
||||
spendable_am = await self.wallet_state_manager.get_confirmed_spendable_balance_for_wallet(self.id())
|
||||
return spendable_am
|
||||
|
||||
async def get_max_send_amount(self, records=None):
|
||||
# Rate limited wallet is a singleton, max send is same as spendable
|
||||
return await self.get_spendable_balance()
|
||||
|
||||
async def get_pending_change_balance(self) -> uint64:
|
||||
unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id())
|
||||
addition_amount = 0
|
||||
|
@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List, Set, Any
|
||||
from typing import Dict, List, Set, Any, Optional
|
||||
|
||||
from blspy import G1Element
|
||||
|
||||
from src.types.blockchain_format.coin import Coin
|
||||
from src.consensus.cost_calculator import calculate_cost_of_program, CostResult
|
||||
from src.full_node.bundle_tools import best_solution_program
|
||||
from src.types.coin_solution import CoinSolution
|
||||
from src.types.blockchain_format.program import Program
|
||||
from src.types.blockchain_format.sized_bytes import bytes32
|
||||
@ -38,6 +40,7 @@ class Wallet:
|
||||
log: logging.Logger
|
||||
wallet_id: uint32
|
||||
secret_key_store: SecretKeyStore
|
||||
cost_of_single_tx: Optional[int]
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
@ -53,9 +56,42 @@ class Wallet:
|
||||
self.wallet_state_manager = wallet_state_manager
|
||||
self.wallet_id = info.id
|
||||
self.secret_key_store = SecretKeyStore()
|
||||
|
||||
self.cost_of_single_tx = None
|
||||
return self
|
||||
|
||||
async def get_max_send_amount(self, records=None):
|
||||
spendable: List[WalletCoinRecord] = list(
|
||||
await self.wallet_state_manager.get_spendable_coins_for_wallet(self.id(), records)
|
||||
)
|
||||
if len(spendable) == 0:
|
||||
return 0
|
||||
spendable.sort(reverse=True, key=lambda record: record.coin.amount)
|
||||
if self.cost_of_single_tx is None:
|
||||
coin = spendable[0].coin
|
||||
tx = await self.generate_signed_transaction(
|
||||
coin.amount, coin.puzzle_hash, coins={coin}, ignore_max_send=True
|
||||
)
|
||||
program = best_solution_program(tx.spend_bundle)
|
||||
# npc contains names of the coins removed, puzzle_hashes and their spend conditions
|
||||
cost_result: CostResult = calculate_cost_of_program(
|
||||
program, self.wallet_state_manager.constants.CLVM_COST_RATIO_CONSTANT, True
|
||||
)
|
||||
self.cost_of_single_tx = cost_result.cost
|
||||
self.log.info(f"Cost of a single tx for standard wallet: {self.cost_of_single_tx}")
|
||||
|
||||
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
|
||||
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 total_amount
|
||||
|
||||
@classmethod
|
||||
def type(cls) -> uint8:
|
||||
return uint8(WalletType.STANDARD_WALLET)
|
||||
@ -135,7 +171,7 @@ class Wallet:
|
||||
|
||||
def make_solution(
|
||||
self,
|
||||
primaries=None,
|
||||
primaries: Optional[List[Dict[str, bytes32]]] = None,
|
||||
min_time=0,
|
||||
me=None,
|
||||
announcements=None,
|
||||
@ -185,7 +221,7 @@ class Wallet:
|
||||
used_coins: Set = set()
|
||||
|
||||
# Use older coins first
|
||||
unspent.sort(key=lambda r: r.confirmed_block_height)
|
||||
unspent.sort(reverse=True, key=lambda r: r.coin.amount)
|
||||
|
||||
# Try to use coins from the store, if there isn't enough of "unused"
|
||||
# coins use change coins that are not confirmed yet
|
||||
@ -220,17 +256,32 @@ class Wallet:
|
||||
fee: uint64 = uint64(0),
|
||||
origin_id: bytes32 = None,
|
||||
coins: Set[Coin] = None,
|
||||
primaries: Optional[List[Dict[str, bytes32]]] = None,
|
||||
ignore_max_send: bool = False,
|
||||
) -> List[CoinSolution]:
|
||||
"""
|
||||
Generates a unsigned transaction in form of List(Puzzle, Solutions)
|
||||
"""
|
||||
if primaries is None:
|
||||
total_amount = amount + fee
|
||||
else:
|
||||
primaries_amount = 0
|
||||
for prim in primaries:
|
||||
primaries_amount += prim["amount"]
|
||||
total_amount = amount + fee + primaries_amount
|
||||
|
||||
if not ignore_max_send:
|
||||
max_send = await self.get_max_send_amount()
|
||||
if total_amount > max_send:
|
||||
raise ValueError(f"Can't send more than {max_send} in a single transaction")
|
||||
|
||||
if coins is None:
|
||||
coins = await self.select_coins(amount + fee)
|
||||
coins = await self.select_coins(total_amount)
|
||||
assert len(coins) > 0
|
||||
|
||||
self.log.info(f"coins is not None {coins}")
|
||||
spend_value = sum([coin.amount for coin in coins])
|
||||
change = spend_value - amount - fee
|
||||
change = spend_value - total_amount
|
||||
|
||||
spends: List[CoinSolution] = []
|
||||
output_created = False
|
||||
@ -241,11 +292,13 @@ class Wallet:
|
||||
|
||||
# Only one coin creates outputs
|
||||
if not output_created and origin_id in (None, coin.name()):
|
||||
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
|
||||
if primaries is None:
|
||||
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
|
||||
else:
|
||||
primaries.append({"puzzlehash": newpuzzlehash, "amount": amount})
|
||||
if change > 0:
|
||||
changepuzzlehash = await self.get_new_puzzlehash()
|
||||
primaries.append({"puzzlehash": changepuzzlehash, "amount": change})
|
||||
|
||||
solution = self.make_solution(primaries=primaries, fee=fee)
|
||||
output_created = True
|
||||
else:
|
||||
@ -267,10 +320,14 @@ class Wallet:
|
||||
fee: uint64 = uint64(0),
|
||||
origin_id: bytes32 = None,
|
||||
coins: Set[Coin] = None,
|
||||
primaries: Optional[List[Dict[str, bytes32]]] = None,
|
||||
ignore_max_send: bool = False,
|
||||
) -> TransactionRecord:
|
||||
""" Use this to generate transaction. """
|
||||
|
||||
transaction = await self.generate_unsigned_transaction(amount, puzzle_hash, fee, origin_id, coins)
|
||||
transaction = await self.generate_unsigned_transaction(
|
||||
amount, puzzle_hash, fee, origin_id, coins, primaries, ignore_max_send
|
||||
)
|
||||
assert len(transaction) > 0
|
||||
|
||||
self.log.info("About to sign a transaction")
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Dict, Optional, List, Set
|
||||
from typing import Dict, Optional, List, Set, Any
|
||||
import aiosqlite
|
||||
from src.types.blockchain_format.sized_bytes import bytes32
|
||||
from src.util.ints import uint32, uint8
|
||||
@ -16,7 +16,7 @@ class WalletTransactionStore:
|
||||
db_connection: aiosqlite.Connection
|
||||
cache_size: uint32
|
||||
tx_record_cache: Dict[bytes32, TransactionRecord]
|
||||
tx_wallet_cache: Dict[int, Set[bytes32]]
|
||||
tx_wallet_cache: Dict[int, Dict[Any, Set[bytes32]]]
|
||||
|
||||
@classmethod
|
||||
async def create(cls, connection: aiosqlite.Connection, cache_size: uint32 = uint32(600000)):
|
||||
@ -109,7 +109,10 @@ class WalletTransactionStore:
|
||||
self.tx_record_cache[record.name] = record
|
||||
|
||||
if record.wallet_id in self.tx_wallet_cache:
|
||||
self.tx_wallet_cache[record.wallet_id].add(record.name)
|
||||
if None in self.tx_wallet_cache[record.wallet_id]:
|
||||
self.tx_wallet_cache[record.wallet_id][None].add(record.name)
|
||||
if record.type in self.tx_wallet_cache[record.wallet_id]:
|
||||
self.tx_wallet_cache[record.wallet_id][record.type].add(record.name)
|
||||
|
||||
if len(self.tx_record_cache) > self.cache_size:
|
||||
while len(self.tx_record_cache) > self.cache_size:
|
||||
@ -156,7 +159,8 @@ class WalletTransactionStore:
|
||||
async def tx_with_addition_coin(self, removal_id: bytes32, wallet_id: int) -> List[TransactionRecord]:
|
||||
""" Returns a record containing removed coin with id: removal_id"""
|
||||
result = []
|
||||
all_records: List[TransactionRecord] = await self.get_all_transactions(wallet_id, TransactionType.OUTGOING_TX)
|
||||
all_records = await self.get_all_transactions(wallet_id, TransactionType.OUTGOING_TX.value)
|
||||
|
||||
for record in all_records:
|
||||
for coin in record.additions:
|
||||
if coin.name() == removal_id:
|
||||
@ -352,15 +356,15 @@ class WalletTransactionStore:
|
||||
await cursor.close()
|
||||
return count
|
||||
|
||||
async def get_all_transactions(self, wallet_id: int, type=None) -> List[TransactionRecord]:
|
||||
async def get_all_transactions(self, wallet_id: int, type: int = None) -> List[TransactionRecord]:
|
||||
"""
|
||||
Returns all stored transactions.
|
||||
"""
|
||||
if type is None and wallet_id in self.tx_wallet_cache:
|
||||
wallet_txs = self.tx_wallet_cache[wallet_id]
|
||||
if wallet_id in self.tx_wallet_cache and type in self.tx_wallet_cache[wallet_id]:
|
||||
wallet_txs = self.tx_wallet_cache[wallet_id][type]
|
||||
txs = []
|
||||
for tx_id in wallet_txs:
|
||||
txs.append(self.tx_record_cache[tx_id])
|
||||
for name in wallet_txs:
|
||||
txs.append(self.tx_record_cache[name])
|
||||
return txs
|
||||
|
||||
if type is None:
|
||||
@ -385,8 +389,9 @@ class WalletTransactionStore:
|
||||
records.append(record)
|
||||
cache_set.add(record.name)
|
||||
|
||||
if type is None:
|
||||
self.tx_wallet_cache[wallet_id] = cache_set
|
||||
if wallet_id not in self.tx_wallet_cache:
|
||||
self.tx_wallet_cache[wallet_id] = {}
|
||||
self.tx_wallet_cache[wallet_id][type] = cache_set
|
||||
|
||||
return records
|
||||
|
||||
|
@ -22,6 +22,13 @@ def event_loop():
|
||||
yield loop
|
||||
|
||||
|
||||
async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32):
|
||||
tx = mempool.get_spendbundle(tx_id)
|
||||
if tx is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class TestCCWallet:
|
||||
@pytest.fixture(scope="function")
|
||||
async def wallet_node(self):
|
||||
@ -38,12 +45,6 @@ class TestCCWallet:
|
||||
async for _ in setup_simulators_and_wallets(1, 3, {}):
|
||||
yield _
|
||||
|
||||
async def tx_in_pool(self, mempool: MempoolManager, tx_id: bytes32):
|
||||
tx = mempool.get_spendbundle(tx_id)
|
||||
if tx is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_colour_creation(self, two_wallet_nodes):
|
||||
num_blocks = 3
|
||||
@ -72,7 +73,7 @@ class TestCCWallet:
|
||||
tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue()
|
||||
tx_record = tx_queue[0]
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -112,7 +113,7 @@ class TestCCWallet:
|
||||
tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue()
|
||||
tx_record = tx_queue[0]
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -132,7 +133,7 @@ class TestCCWallet:
|
||||
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
@ -149,7 +150,7 @@ class TestCCWallet:
|
||||
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
@ -234,7 +235,7 @@ class TestCCWallet:
|
||||
assert cc_wallet.cc_info.my_genesis_checker == cc_wallet_2.cc_info.my_genesis_checker
|
||||
|
||||
spend_bundle = await cc_wallet_2.generate_zero_val_coin()
|
||||
await time_out_assert(15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, spend_bundle.name())
|
||||
await time_out_assert(15, tx_in_pool, True, full_node_api.full_node.mempool_manager, spend_bundle.name())
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
@ -282,7 +283,7 @@ class TestCCWallet:
|
||||
tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue()
|
||||
tx_record = tx_queue[0]
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -301,7 +302,7 @@ class TestCCWallet:
|
||||
tx_record = await cc_wallet.generate_signed_transaction([uint64(60)], [cc_2_hash])
|
||||
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -316,7 +317,7 @@ class TestCCWallet:
|
||||
tx_record = await wallet.wallet_state_manager.main_wallet.generate_signed_transaction(10, cc2_ph, 0)
|
||||
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -362,7 +363,7 @@ class TestCCWallet:
|
||||
tx_queue: List[TransactionRecord] = await wallet_node_0.wallet_state_manager.get_send_queue()
|
||||
tx_record = tx_queue[0]
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -390,7 +391,7 @@ class TestCCWallet:
|
||||
tx_record = await cc_wallet_0.generate_signed_transaction([uint64(60), uint64(20)], [cc_1_hash, cc_2_hash])
|
||||
await wallet_0.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
@ -412,10 +413,10 @@ class TestCCWallet:
|
||||
tx_record_2 = await cc_wallet_2.generate_signed_transaction([uint64(20)], [cc_hash])
|
||||
await wallet_2.wallet_state_manager.add_pending_transaction(tx_record_2)
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name()
|
||||
)
|
||||
await time_out_assert(
|
||||
15, self.tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record_2.spend_bundle.name()
|
||||
15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record_2.spend_bundle.name()
|
||||
)
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
|
@ -13,6 +13,7 @@ from src.wallet.util.transaction_type import TransactionType
|
||||
from src.wallet.wallet_state_manager import WalletStateManager
|
||||
from tests.setup_nodes import setup_simulators_and_wallets, self_hostname
|
||||
from tests.time_out_assert import time_out_assert, time_out_assert_not_none
|
||||
from tests.wallet.cc_wallet.test_cc_wallet import tx_in_pool
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@ -382,3 +383,74 @@ class TestWalletSimulator:
|
||||
|
||||
await time_out_assert(5, wallet.get_confirmed_balance, new_funds - tx_amount - tx_fee)
|
||||
await time_out_assert(5, wallet.get_unconfirmed_balance, new_funds - tx_amount - tx_fee)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes):
|
||||
num_blocks = 5
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_1 = full_nodes[0]
|
||||
wallet_node, server_2 = wallets[0]
|
||||
wallet_node_2, server_3 = wallets[1]
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_1.full_node.server._port)), None)
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
funds = sum(
|
||||
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
|
||||
)
|
||||
|
||||
await time_out_assert(5, wallet.get_confirmed_balance, funds)
|
||||
|
||||
primaries = []
|
||||
for i in range(0, 600):
|
||||
primaries.append({"puzzlehash": ph, "amount": 100000000 + i})
|
||||
|
||||
tx_split_coins = await wallet.generate_signed_transaction(1, ph, 0, primaries=primaries)
|
||||
|
||||
await wallet.push_transaction(tx_split_coins)
|
||||
await time_out_assert(
|
||||
15, tx_in_pool, True, full_node_1.full_node.mempool_manager, tx_split_coins.spend_bundle.name()
|
||||
)
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))
|
||||
|
||||
funds = sum(
|
||||
[
|
||||
calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i))
|
||||
for i in range(1, num_blocks + 1)
|
||||
]
|
||||
)
|
||||
|
||||
await time_out_assert(25, wallet.get_confirmed_balance, funds)
|
||||
max_sent_amount = await wallet.get_max_send_amount()
|
||||
|
||||
# 1) Generate transaction that is under the limit
|
||||
under_limit_tx = None
|
||||
try:
|
||||
under_limit_tx = await wallet.generate_signed_transaction(max_sent_amount - 1, ph, 0, )
|
||||
except ValueError:
|
||||
assert ValueError
|
||||
|
||||
assert under_limit_tx is not None
|
||||
|
||||
# 2) Generate transaction that is equal to limit
|
||||
at_limit_tx = None
|
||||
try:
|
||||
at_limit_tx = await wallet.generate_signed_transaction(max_sent_amount, ph, 0, )
|
||||
except ValueError:
|
||||
assert ValueError
|
||||
|
||||
assert at_limit_tx is not None
|
||||
|
||||
# 3) Generate transaction that is greater than limit
|
||||
above_limit_tx = None
|
||||
try:
|
||||
above_limit_tx = await wallet.generate_signed_transaction(max_sent_amount + 1, ph, 0, )
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
assert above_limit_tx is None
|
||||
|
Loading…
Reference in New Issue
Block a user