mirror of https://github.com/nucypher/nucypher.git
Add account selection to collect-reward. Complete tests for stake via contract.
parent
8e2bb3e217
commit
d968cdd848
|
@ -827,8 +827,8 @@ class Staker(NucypherTokenActor):
|
|||
"""Withdraw tokens rewarded for staking."""
|
||||
if self.is_contract:
|
||||
reward_amount = self.staking_agent.calculate_staking_reward(staker_address=self.checksum_address)
|
||||
self.log.debug(f"Withdrawing staking reward, {reward_amount}, to {self.checksum_address}")
|
||||
receipt = self.preallocation_escrow_agent.withdraw_as_staker(amount=reward_amount)
|
||||
self.log.debug(f"Withdrawing staking reward ({NU.from_nunits(reward_amount)}) to {self.checksum_address}")
|
||||
receipt = self.preallocation_escrow_agent.withdraw_as_staker(value=reward_amount)
|
||||
else:
|
||||
receipt = self.staking_agent.collect_staking_reward(staker_address=self.checksum_address)
|
||||
return receipt
|
||||
|
@ -838,7 +838,7 @@ class Staker(NucypherTokenActor):
|
|||
def withdraw(self, amount: NU) -> str:
|
||||
"""Withdraw tokens (assuming they're unlocked)"""
|
||||
if self.is_contract:
|
||||
receipt = self.preallocation_escrow_agent.withdraw_as_staker(amount=int(amount))
|
||||
receipt = self.preallocation_escrow_agent.withdraw_as_staker(value=int(amount))
|
||||
else:
|
||||
receipt = self.staking_agent.withdraw(staker_address=self.checksum_address,
|
||||
amount=int(amount))
|
||||
|
@ -861,7 +861,6 @@ class Worker(NucypherTokenActor):
|
|||
work_tracker: WorkTracker = None,
|
||||
worker_address: str = None,
|
||||
start_working_now: bool = True,
|
||||
confirm_now: bool = True,
|
||||
check_active_worker: bool = True,
|
||||
*args, **kwargs):
|
||||
|
||||
|
|
|
@ -374,7 +374,8 @@ class StakingEscrowAgent(EthereumContractAgent):
|
|||
def collect_staking_reward(self, staker_address: str):
|
||||
"""Withdraw tokens rewarded for staking."""
|
||||
reward_amount = self.calculate_staking_reward(staker_address=staker_address)
|
||||
self.log.debug(f"Withdrawing staking reward, {reward_amount}, to {staker_address}")
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
self.log.debug(f"Withdrawing staking reward ({NU.from_nunits(reward_amount)}) to {staker_address}")
|
||||
return self.withdraw(staker_address=staker_address, amount=reward_amount)
|
||||
|
||||
@validate_checksum_address
|
||||
|
|
|
@ -796,6 +796,7 @@ class Ursula(Teacher, Character, Worker):
|
|||
checksum_address: str = None, # Staker address
|
||||
worker_address: str = None,
|
||||
work_tracker: WorkTracker = None,
|
||||
start_working_now: bool = True,
|
||||
client_password: str = None,
|
||||
|
||||
# Character
|
||||
|
@ -856,7 +857,8 @@ class Ursula(Teacher, Character, Worker):
|
|||
registry=self.registry,
|
||||
checksum_address=checksum_address,
|
||||
worker_address=worker_address,
|
||||
work_tracker=work_tracker)
|
||||
work_tracker=work_tracker,
|
||||
start_working_now=start_working_now)
|
||||
|
||||
#
|
||||
# ProxyRESTServer and TLSHostingPower #
|
||||
|
|
|
@ -191,6 +191,7 @@ def stake(click_config,
|
|||
return # Exit
|
||||
|
||||
elif action == 'accounts':
|
||||
# TODO: Order accounts like shown by blockchain.client.accounts
|
||||
for address, balances in STAKEHOLDER.wallet.balances.items():
|
||||
emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}")
|
||||
return # Exit
|
||||
|
@ -420,16 +421,22 @@ def stake(click_config,
|
|||
elif action == 'collect-reward':
|
||||
"""Withdraw staking reward to the specified wallet address"""
|
||||
|
||||
# TODO: Missing account selection
|
||||
# Authenticate
|
||||
client_account, staking_address = handle_client_account_for_staking(emitter=emitter,
|
||||
stakeholder=STAKEHOLDER,
|
||||
staking_address=staking_address,
|
||||
is_preallocation_staker=is_preallocation_staker,
|
||||
beneficiary_address=beneficiary_address,
|
||||
force=force)
|
||||
|
||||
password = None
|
||||
if not hw_wallet and not blockchain.client.is_local:
|
||||
password = get_client_password(checksum_address=staking_address)
|
||||
password = get_client_password(checksum_address=client_account)
|
||||
|
||||
if not staking_reward and not policy_reward:
|
||||
raise click.BadArgumentUsage(f"Either --staking-reward or --policy-reward must be True to collect rewards.")
|
||||
|
||||
STAKEHOLDER.assimilate(checksum_address=staking_address, password=password)
|
||||
STAKEHOLDER.assimilate(checksum_address=client_account, password=password)
|
||||
if staking_reward:
|
||||
# Note: Sending staking / inflation rewards to another account is not allowed.
|
||||
staking_receipt = STAKEHOLDER.collect_staking_reward()
|
||||
|
|
|
@ -15,19 +15,33 @@ You should have received a copy of the GNU Affero General Public License
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
|
||||
import maya
|
||||
import pytest
|
||||
from twisted.logger import Logger
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.eth.actors import Staker
|
||||
from nucypher.blockchain.eth.agents import StakingEscrowAgent, ContractAgency, PreallocationEscrowAgent, NucypherTokenAgent
|
||||
from nucypher.blockchain.eth.token import NU, Stake
|
||||
from nucypher.blockchain.eth.token import NU, Stake, StakeList
|
||||
from nucypher.characters.lawful import Enrico, Ursula
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.utilities.sandbox.constants import (
|
||||
TEST_PROVIDER_URI,
|
||||
INSECURE_DEVELOPMENT_PASSWORD,
|
||||
MOCK_IP_ADDRESS,
|
||||
MOCK_URSULA_STARTING_PORT,
|
||||
TEMPORARY_DOMAIN,
|
||||
MOCK_KNOWN_URSULAS_CACHE,
|
||||
select_test_port,
|
||||
)
|
||||
from nucypher.utilities.sandbox.middleware import MockRestMiddleware
|
||||
|
||||
#
|
||||
# This test module is intended to mirror tests/cli/ursula/test_stakeholder_and_ursula.py,
|
||||
|
@ -169,6 +183,68 @@ def test_stake_set_worker(click_runner,
|
|||
assert staker.worker_address == manual_worker
|
||||
|
||||
|
||||
def test_stake_detach_worker(click_runner,
|
||||
testerchain,
|
||||
token_economics,
|
||||
beneficiary,
|
||||
preallocation_escrow_agent,
|
||||
mock_allocation_registry,
|
||||
manual_worker,
|
||||
test_registry,
|
||||
stakeholder_configuration_file_location):
|
||||
|
||||
staker_address = preallocation_escrow_agent.principal_contract.address
|
||||
|
||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
||||
assert manual_worker == staking_agent.get_worker_from_staker(staker_address=staker_address)
|
||||
|
||||
testerchain.time_travel(periods=token_economics.minimum_worker_periods)
|
||||
|
||||
init_args = ('stake', 'detach-worker',
|
||||
'--config-file', stakeholder_configuration_file_location,
|
||||
'--escrow',
|
||||
'--beneficiary-address', beneficiary,
|
||||
'--allocation-filepath', mock_allocation_registry.filepath,
|
||||
'--force')
|
||||
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
init_args,
|
||||
input=INSECURE_DEVELOPMENT_PASSWORD,
|
||||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
staker = Staker(is_me=True,
|
||||
checksum_address=beneficiary,
|
||||
allocation_registry=mock_allocation_registry,
|
||||
registry=test_registry)
|
||||
|
||||
assert not staker.worker_address
|
||||
|
||||
# Ok ok, let's set the worker again.
|
||||
|
||||
init_args = ('stake', 'set-worker',
|
||||
'--config-file', stakeholder_configuration_file_location,
|
||||
'--escrow',
|
||||
'--beneficiary-address', beneficiary,
|
||||
'--allocation-filepath', mock_allocation_registry.filepath,
|
||||
'--worker-address', manual_worker,
|
||||
'--force')
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}'
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
init_args,
|
||||
input=user_input,
|
||||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
staker = Staker(is_me=True,
|
||||
checksum_address=beneficiary,
|
||||
allocation_registry=mock_allocation_registry,
|
||||
registry=test_registry)
|
||||
|
||||
assert staker.worker_address == manual_worker
|
||||
|
||||
|
||||
def test_stake_restake(click_runner,
|
||||
beneficiary,
|
||||
preallocation_escrow_agent,
|
||||
|
@ -253,3 +329,248 @@ def test_stake_restake(click_runner,
|
|||
allocation_registry=mock_allocation_registry)
|
||||
assert not staker.is_restaking
|
||||
assert "Successfully disabled" in result.output
|
||||
|
||||
|
||||
def test_ursula_init(click_runner,
|
||||
custom_filepath,
|
||||
mock_registry_filepath,
|
||||
preallocation_escrow_agent,
|
||||
manual_worker,
|
||||
testerchain):
|
||||
|
||||
init_args = ('ursula', 'init',
|
||||
'--poa',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--staker-address', preallocation_escrow_agent.principal_contract.address,
|
||||
'--worker-address', manual_worker,
|
||||
'--config-root', custom_filepath,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--registry-filepath', mock_registry_filepath,
|
||||
'--rest-host', MOCK_IP_ADDRESS,
|
||||
'--rest-port', MOCK_URSULA_STARTING_PORT)
|
||||
|
||||
user_input = '{password}\n{password}'.format(password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
init_args,
|
||||
input=user_input,
|
||||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Files and Directories
|
||||
assert os.path.isdir(custom_filepath), 'Configuration file does not exist'
|
||||
assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist'
|
||||
assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist'
|
||||
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename())
|
||||
assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist'
|
||||
|
||||
with open(custom_config_filepath, 'r') as config_file:
|
||||
raw_config_data = config_file.read()
|
||||
config_data = json.loads(raw_config_data)
|
||||
assert config_data['provider_uri'] == TEST_PROVIDER_URI
|
||||
assert config_data['worker_address'] == manual_worker
|
||||
assert config_data['checksum_address'] == preallocation_escrow_agent.principal_contract.address
|
||||
assert TEMPORARY_DOMAIN in config_data['domains']
|
||||
|
||||
|
||||
def test_ursula_run(click_runner,
|
||||
manual_worker,
|
||||
custom_filepath,
|
||||
testerchain):
|
||||
|
||||
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename())
|
||||
|
||||
# Now start running your Ursula!
|
||||
init_args = ('ursula', 'run',
|
||||
'--dry-run',
|
||||
'--config-file', custom_config_filepath)
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' * 2
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
init_args,
|
||||
input=user_input,
|
||||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_collect_rewards_integration(click_runner,
|
||||
testerchain,
|
||||
test_registry,
|
||||
stakeholder_configuration_file_location,
|
||||
blockchain_alice,
|
||||
blockchain_bob,
|
||||
random_policy_label,
|
||||
beneficiary,
|
||||
preallocation_escrow_agent,
|
||||
mock_allocation_registry,
|
||||
manual_worker,
|
||||
token_economics,
|
||||
stake_value,
|
||||
policy_value,
|
||||
policy_rate):
|
||||
|
||||
half_stake_time = token_economics.minimum_locked_periods // 2 # Test setup
|
||||
logger = Logger("Test-CLI") # Enter the Teacher's Logger, and
|
||||
current_period = 0 # State the initial period for incrementing
|
||||
|
||||
staker_address = preallocation_escrow_agent.principal_contract.address
|
||||
worker_address = manual_worker
|
||||
|
||||
# The staker is staking.
|
||||
stakes = StakeList(registry=test_registry, checksum_address=staker_address)
|
||||
stakes.refresh()
|
||||
assert stakes
|
||||
|
||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
||||
assert worker_address == staking_agent.get_worker_from_staker(staker_address=staker_address)
|
||||
|
||||
ursula_port = select_test_port()
|
||||
ursula = Ursula(is_me=True,
|
||||
checksum_address=staker_address,
|
||||
worker_address=worker_address,
|
||||
registry=test_registry,
|
||||
rest_host='127.0.0.1',
|
||||
rest_port=ursula_port,
|
||||
start_working_now=False,
|
||||
network_middleware=MockRestMiddleware())
|
||||
|
||||
MOCK_KNOWN_URSULAS_CACHE[ursula_port] = ursula
|
||||
assert ursula.worker_address == worker_address
|
||||
assert ursula.checksum_address == staker_address
|
||||
|
||||
# Mock TransactingPower consumption (Worker-Ursula)
|
||||
testerchain.transacting_power = TransactingPower(account=worker_address, password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
testerchain.transacting_power.activate()
|
||||
|
||||
# Confirm for half the first stake duration
|
||||
for _ in range(half_stake_time):
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
ursula.confirm_activity()
|
||||
testerchain.time_travel(periods=1)
|
||||
current_period += 1
|
||||
|
||||
# Alice creates a policy and grants Bob access
|
||||
blockchain_alice.selection_buffer = 1
|
||||
|
||||
M, N = 1, 1
|
||||
expiration = maya.now() + datetime.timedelta(days=3)
|
||||
blockchain_policy = blockchain_alice.grant(bob=blockchain_bob,
|
||||
label=random_policy_label,
|
||||
m=M, n=N,
|
||||
value=policy_value,
|
||||
expiration=expiration,
|
||||
handpicked_ursulas={ursula})
|
||||
|
||||
# Ensure that the handpicked Ursula was selected for the policy
|
||||
arrangement = list(blockchain_policy._accepted_arrangements)[0]
|
||||
assert arrangement.ursula == ursula
|
||||
|
||||
# Bob learns about the new staker and joins the policy
|
||||
blockchain_bob.start_learning_loop()
|
||||
blockchain_bob.remember_node(node=ursula)
|
||||
blockchain_bob.join_policy(random_policy_label, bytes(blockchain_alice.stamp))
|
||||
|
||||
# Enrico Encrypts (of course)
|
||||
enrico = Enrico(policy_encrypting_key=blockchain_policy.public_key,
|
||||
network_middleware=MockRestMiddleware())
|
||||
|
||||
verifying_key = blockchain_alice.stamp.as_umbral_pubkey()
|
||||
|
||||
for index in range(half_stake_time - 5):
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
ursula.confirm_activity()
|
||||
|
||||
# Encrypt
|
||||
random_data = os.urandom(random.randrange(20, 100))
|
||||
ciphertext, signature = enrico.encrypt_message(message=random_data)
|
||||
|
||||
# Decrypt
|
||||
cleartexts = blockchain_bob.retrieve(message_kit=ciphertext,
|
||||
data_source=enrico,
|
||||
alice_verifying_key=verifying_key,
|
||||
label=random_policy_label)
|
||||
assert random_data == cleartexts[0]
|
||||
|
||||
# Ursula Staying online and the clock advancing
|
||||
testerchain.time_travel(periods=1)
|
||||
current_period += 1
|
||||
|
||||
# Finish the passage of time
|
||||
for _ in range(5 - 1): # minus 1 because the first period was already confirmed in test_ursula_run
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
ursula.confirm_activity()
|
||||
current_period += 1
|
||||
testerchain.time_travel(periods=1)
|
||||
|
||||
#
|
||||
# WHERES THE MONEY URSULA?? - Collecting Rewards
|
||||
#
|
||||
|
||||
# The address the client wants Ursula to send policy rewards to
|
||||
burner_wallet = testerchain.w3.eth.account.create(INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
# The policy rewards wallet is initially empty, because it is freshly created
|
||||
assert testerchain.client.get_balance(burner_wallet.address) == 0
|
||||
|
||||
# Rewards will be unlocked after the
|
||||
# final confirmed period has passed (+1).
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
testerchain.time_travel(periods=1)
|
||||
current_period += 1
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
|
||||
# Since we are mocking the blockchain connection, manually consume the transacting power of the Beneficiary.
|
||||
testerchain.transacting_power = TransactingPower(account=beneficiary,
|
||||
password=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
testerchain.transacting_power.activate()
|
||||
|
||||
# Collect Policy Reward
|
||||
collection_args = ('stake', 'collect-reward',
|
||||
'--mock-networking',
|
||||
'--config-file', stakeholder_configuration_file_location,
|
||||
'--policy-reward',
|
||||
'--no-staking-reward',
|
||||
'--withdraw-address', burner_wallet.address,
|
||||
'--escrow',
|
||||
'--beneficiary-address', beneficiary,
|
||||
'--allocation-filepath', mock_allocation_registry.filepath,
|
||||
'--force')
|
||||
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
collection_args,
|
||||
input=INSECURE_DEVELOPMENT_PASSWORD,
|
||||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Policy Reward
|
||||
collected_policy_reward = testerchain.client.get_balance(burner_wallet.address)
|
||||
expected_collection = policy_rate * 30
|
||||
assert collected_policy_reward == expected_collection
|
||||
|
||||
#
|
||||
# Collect Staking Reward
|
||||
#
|
||||
token_agent = ContractAgency.get_agent(agent_class=NucypherTokenAgent, registry=test_registry)
|
||||
balance_before_collecting = token_agent.get_balance(address=staker_address)
|
||||
|
||||
collection_args = ('stake', 'collect-reward',
|
||||
'--mock-networking',
|
||||
'--config-file', stakeholder_configuration_file_location,
|
||||
'--no-policy-reward',
|
||||
'--staking-reward',
|
||||
'--escrow',
|
||||
'--beneficiary-address', beneficiary,
|
||||
'--allocation-filepath', mock_allocation_registry.filepath,
|
||||
'--force')
|
||||
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
collection_args,
|
||||
input=INSECURE_DEVELOPMENT_PASSWORD,
|
||||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# The beneficiary has withdrawn her staking rewards, which are now in the staking contract
|
||||
assert token_agent.get_balance(address=staker_address) >= balance_before_collecting
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -400,9 +400,8 @@ def test_collect_rewards_integration(click_runner,
|
|||
verifying_key = blockchain_alice.stamp.as_umbral_pubkey()
|
||||
|
||||
for index in range(half_stake_time - 5):
|
||||
ursula.confirm_activity()
|
||||
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
ursula.confirm_activity()
|
||||
|
||||
# Encrypt
|
||||
random_data = os.urandom(random.randrange(20, 100))
|
||||
|
@ -421,10 +420,10 @@ def test_collect_rewards_integration(click_runner,
|
|||
|
||||
# Finish the passage of time for the first Stake
|
||||
for _ in range(5): # plus the extended periods from stake division
|
||||
ursula.confirm_activity()
|
||||
current_period += 1
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
ursula.confirm_activity()
|
||||
testerchain.time_travel(periods=1)
|
||||
current_period += 1
|
||||
|
||||
#
|
||||
# WHERES THE MONEY URSULA?? - Collecting Rewards
|
||||
|
@ -438,6 +437,7 @@ def test_collect_rewards_integration(click_runner,
|
|||
|
||||
# Rewards will be unlocked after the
|
||||
# final confirmed period has passed (+1).
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
testerchain.time_travel(periods=1)
|
||||
current_period += 1
|
||||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
|
@ -479,14 +479,18 @@ def test_collect_rewards_integration(click_runner,
|
|||
logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<")
|
||||
testerchain.time_travel(periods=1)
|
||||
|
||||
# Collect Inflation Reward
|
||||
#
|
||||
# Collect Staking Reward
|
||||
#
|
||||
|
||||
balance_before_collecting = staker.token_agent.get_balance(address=staker_address)
|
||||
|
||||
collection_args = ('stake', 'collect-reward',
|
||||
'--mock-networking',
|
||||
'--config-file', stakeholder_configuration_file_location,
|
||||
'--no-policy-reward',
|
||||
'--staking-reward',
|
||||
'--staking-address', staker_address,
|
||||
'--withdraw-address', burner_wallet.address,
|
||||
'--force')
|
||||
|
||||
result = click_runner.invoke(nucypher_cli,
|
||||
|
@ -495,8 +499,8 @@ def test_collect_rewards_integration(click_runner,
|
|||
catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# The burner wallet has the reward ethers
|
||||
assert staker.token_agent.get_balance(address=staker_address)
|
||||
# The staker has withdrawn her staking rewards
|
||||
assert staker.token_agent.get_balance(address=staker_address) >= balance_before_collecting
|
||||
|
||||
|
||||
def test_stake_detach_worker(click_runner,
|
||||
|
|
Loading…
Reference in New Issue