mirror of https://github.com/nucypher/nucypher.git
commit
788992381b
|
@ -20,16 +20,13 @@ import json
|
|||
import time
|
||||
from decimal import Decimal
|
||||
from typing import Callable, Union
|
||||
from typing import Dict, Iterable, List, Optional, Tuple
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
import maya
|
||||
from constant_sorrow.constants import FULL
|
||||
from eth_tester.exceptions import TransactionFailed as TestTransactionFailed
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_canonical_address
|
||||
from hexbytes import HexBytes
|
||||
from web3 import Web3
|
||||
from web3.exceptions import ValidationError
|
||||
from web3.types import TxReceipt
|
||||
|
||||
from nucypher.acumen.nicknames import Nickname
|
||||
|
@ -42,14 +39,10 @@ from nucypher.blockchain.eth.agents import (
|
|||
ContractAgency,
|
||||
NucypherTokenAgent,
|
||||
StakingEscrowAgent,
|
||||
WorkLockAgent,
|
||||
PREApplicationAgent
|
||||
)
|
||||
from nucypher.blockchain.eth.constants import (
|
||||
NULL_ADDRESS,
|
||||
POLICY_MANAGER_CONTRACT_NAME,
|
||||
DISPATCHER_CONTRACT_NAME,
|
||||
STAKING_ESCROW_CONTRACT_NAME,
|
||||
)
|
||||
from nucypher.blockchain.eth.decorators import (
|
||||
only_me,
|
||||
|
@ -61,7 +54,6 @@ from nucypher.blockchain.eth.deployers import (
|
|||
BaseContractDeployer,
|
||||
NucypherTokenDeployer,
|
||||
StakingEscrowDeployer,
|
||||
WorklockDeployer,
|
||||
PREApplicationDeployer,
|
||||
SubscriptionManagerDeployer
|
||||
)
|
||||
|
@ -80,8 +72,7 @@ from nucypher.blockchain.eth.token import (
|
|||
)
|
||||
from nucypher.blockchain.eth.utils import (
|
||||
calculate_period_duration,
|
||||
datetime_to_period,
|
||||
prettify_eth_amount
|
||||
datetime_to_period
|
||||
)
|
||||
from nucypher.characters.banners import STAKEHOLDER_BANNER
|
||||
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
|
||||
|
@ -201,7 +192,7 @@ class ContractAdministrator(BaseActor):
|
|||
)
|
||||
|
||||
aux_deployer_classes = (
|
||||
WorklockDeployer,
|
||||
# Add more deployer classes here
|
||||
)
|
||||
|
||||
# For ownership transfers.
|
||||
|
@ -1088,273 +1079,3 @@ class StakeHolder:
|
|||
stake = sum(staking_agent.owned_tokens(staker_address=account) for account in self.signer.accounts)
|
||||
nu_stake = NU.from_units(stake)
|
||||
return nu_stake
|
||||
|
||||
|
||||
class Bidder(NucypherTokenActor):
|
||||
"""WorkLock participant"""
|
||||
|
||||
class BidderError(NucypherTokenActor.ActorError):
|
||||
pass
|
||||
|
||||
class BiddingIsOpen(BidderError):
|
||||
pass
|
||||
|
||||
class BiddingIsClosed(BidderError):
|
||||
pass
|
||||
|
||||
class CancellationWindowIsOpen(BidderError):
|
||||
pass
|
||||
|
||||
class CancellationWindowIsClosed(BidderError):
|
||||
pass
|
||||
|
||||
class ClaimError(BidderError):
|
||||
pass
|
||||
|
||||
class WhaleError(BidderError):
|
||||
pass
|
||||
|
||||
@validate_checksum_address
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.log = Logger(f"WorkLockBidder")
|
||||
self.worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=self.registry) # type: WorkLockAgent
|
||||
self.staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.registry) # type: StakingEscrowAgent
|
||||
self.economics = EconomicsFactory.get_economics(registry=self.registry)
|
||||
self._all_bonus_bidders = None
|
||||
|
||||
def ensure_bidding_is_open(self, message: str = None) -> None:
|
||||
now = self.worklock_agent.blockchain.get_blocktime()
|
||||
start = self.worklock_agent.start_bidding_date
|
||||
end = self.worklock_agent.end_bidding_date
|
||||
if now < start:
|
||||
message = message or f'Bidding does not open until {maya.MayaDT(start).slang_date()}'
|
||||
raise self.BiddingIsClosed(message)
|
||||
if now >= end:
|
||||
message = message or f'Bidding closed at {maya.MayaDT(end).slang_date()}'
|
||||
raise self.BiddingIsClosed(message)
|
||||
|
||||
def _ensure_bidding_is_closed(self, message: str = None) -> None:
|
||||
now = self.worklock_agent.blockchain.get_blocktime()
|
||||
end = self.worklock_agent.end_bidding_date
|
||||
if now < end:
|
||||
message = message or f"Bidding does not close until {maya.MayaDT(end).slang_date()}"
|
||||
raise self.BiddingIsOpen(message)
|
||||
|
||||
def _ensure_cancellation_window(self, ensure_closed: bool = True, message: str = None) -> None:
|
||||
now = self.worklock_agent.blockchain.get_blocktime()
|
||||
end = self.worklock_agent.end_cancellation_date
|
||||
if ensure_closed and now < end:
|
||||
message = message or f"Operation cannot be performed while the cancellation window is still open " \
|
||||
f"(closes at {maya.MayaDT(end).slang_date()})."
|
||||
raise self.CancellationWindowIsOpen(message)
|
||||
elif not ensure_closed and now >= end:
|
||||
message = message or f"Operation is allowed only while the cancellation window is open " \
|
||||
f"(closed at {maya.MayaDT(end).slang_date()})."
|
||||
raise self.CancellationWindowIsClosed(message)
|
||||
|
||||
#
|
||||
# Transactions
|
||||
#
|
||||
|
||||
def place_bid(self, value: int) -> TxReceipt:
|
||||
self.ensure_bidding_is_open()
|
||||
minimum = self.worklock_agent.minimum_allowed_bid
|
||||
if not self.get_deposited_eth and value < minimum:
|
||||
raise self.BidderError(f"{prettify_eth_amount(value)} is too small a value for bidding; "
|
||||
f"bid must be at least {prettify_eth_amount(minimum)}")
|
||||
receipt = self.worklock_agent.bid(transacting_power=self.transacting_power, value=value)
|
||||
return receipt
|
||||
|
||||
def claim(self) -> TxReceipt:
|
||||
|
||||
# Require the cancellation window is closed
|
||||
self._ensure_cancellation_window(ensure_closed=True)
|
||||
|
||||
if not self.worklock_agent.is_claiming_available():
|
||||
raise self.ClaimError(f"Claiming is not available yet")
|
||||
|
||||
# Ensure the claim was not already placed
|
||||
if self.has_claimed:
|
||||
raise self.ClaimError(f"Bidder {self.checksum_address} already placed a claim.")
|
||||
|
||||
# Require an active bid
|
||||
if not self.get_deposited_eth:
|
||||
raise self.ClaimError(f"No bids available for {self.checksum_address}")
|
||||
|
||||
receipt = self.worklock_agent.claim(transacting_power=self.transacting_power)
|
||||
return receipt
|
||||
|
||||
def cancel_bid(self) -> TxReceipt:
|
||||
self._ensure_cancellation_window(ensure_closed=False)
|
||||
|
||||
# Require an active bid
|
||||
if not self.get_deposited_eth:
|
||||
self.BidderError(f"No bids available for {self.checksum_address}")
|
||||
|
||||
receipt = self.worklock_agent.cancel_bid(transacting_power=self.transacting_power)
|
||||
return receipt
|
||||
|
||||
def _get_max_bonus_bid_from_max_stake(self) -> int:
|
||||
"""Returns maximum allowed bid calculated from maximum allowed locked tokens"""
|
||||
max_bonus_tokens = self.economics.maximum_allowed_locked - self.economics.min_authorization
|
||||
bonus_eth_supply = sum(
|
||||
self._all_bonus_bidders.values()) if self._all_bonus_bidders else self.worklock_agent.get_bonus_eth_supply()
|
||||
bonus_worklock_supply = self.worklock_agent.get_bonus_lot_value()
|
||||
max_bonus_bid = max_bonus_tokens * bonus_eth_supply // bonus_worklock_supply
|
||||
return max_bonus_bid
|
||||
|
||||
def get_whales(self, force_read: bool = False) -> Dict[str, int]:
|
||||
"""Returns all worklock bidders over the whale threshold as a dictionary of addresses and bonus bid values."""
|
||||
max_bonus_bid_from_max_stake = self._get_max_bonus_bid_from_max_stake()
|
||||
|
||||
bidders = dict()
|
||||
for bidder, bid in self._get_all_bonus_bidders(force_read).items():
|
||||
if bid > max_bonus_bid_from_max_stake:
|
||||
bidders[bidder] = bid
|
||||
return bidders
|
||||
|
||||
def _get_all_bonus_bidders(self, force_read: bool = False) -> dict:
|
||||
if not force_read and self._all_bonus_bidders:
|
||||
return self._all_bonus_bidders
|
||||
|
||||
bidders = self.worklock_agent.get_bidders()
|
||||
min_bid = self.economics.worklock_min_allowed_bid
|
||||
|
||||
self._all_bonus_bidders = dict()
|
||||
for bidder in bidders:
|
||||
bid = self.worklock_agent.get_deposited_eth(bidder)
|
||||
if bid > min_bid:
|
||||
self._all_bonus_bidders[bidder] = bid - min_bid
|
||||
return self._all_bonus_bidders
|
||||
|
||||
def _reduce_bids(self, whales: dict):
|
||||
|
||||
min_whale_bonus_bid = min(whales.values())
|
||||
max_whale_bonus_bid = max(whales.values())
|
||||
|
||||
# first step - align at a minimum bid
|
||||
if min_whale_bonus_bid != max_whale_bonus_bid:
|
||||
whales = dict.fromkeys(whales.keys(), min_whale_bonus_bid)
|
||||
self._all_bonus_bidders.update(whales)
|
||||
|
||||
bonus_eth_supply = sum(self._all_bonus_bidders.values())
|
||||
bonus_worklock_supply = self.worklock_agent.get_bonus_lot_value()
|
||||
max_bonus_tokens = self.economics.maximum_allowed_locked - self.economics.min_authorization
|
||||
if (min_whale_bonus_bid * bonus_worklock_supply) // bonus_eth_supply <= max_bonus_tokens:
|
||||
raise self.WhaleError(f"At least one of bidders {whales} has allowable bid")
|
||||
|
||||
a = min_whale_bonus_bid * bonus_worklock_supply - max_bonus_tokens * bonus_eth_supply
|
||||
b = bonus_worklock_supply - max_bonus_tokens * len(whales)
|
||||
refund = -(-a // b) # div ceil
|
||||
min_whale_bonus_bid -= refund
|
||||
whales = dict.fromkeys(whales.keys(), min_whale_bonus_bid)
|
||||
self._all_bonus_bidders.update(whales)
|
||||
|
||||
return whales
|
||||
|
||||
def force_refund(self) -> TxReceipt:
|
||||
self._ensure_cancellation_window(ensure_closed=True)
|
||||
|
||||
whales = self.get_whales()
|
||||
if not whales:
|
||||
raise self.WhaleError(f"Force refund aborted: No whales detected and all bids qualify for claims.")
|
||||
|
||||
new_whales = whales.copy()
|
||||
while new_whales:
|
||||
whales.update(new_whales)
|
||||
whales = self._reduce_bids(whales)
|
||||
new_whales = self.get_whales()
|
||||
|
||||
receipt = self.worklock_agent.force_refund(transacting_power=self.transacting_power,
|
||||
addresses=list(whales.keys()))
|
||||
|
||||
if self.get_whales(force_read=True):
|
||||
raise RuntimeError(f"Internal error: offline simulation differs from transaction results")
|
||||
return receipt
|
||||
|
||||
# TODO better control: max iterations, interactive mode
|
||||
def verify_bidding_correctness(self, gas_limit: int) -> dict:
|
||||
self._ensure_cancellation_window(ensure_closed=True)
|
||||
|
||||
if self.worklock_agent.bidders_checked():
|
||||
raise self.BidderError(f"Check was already done")
|
||||
|
||||
whales = self.get_whales()
|
||||
if whales:
|
||||
raise self.WhaleError(f"Some bidders have bids that are too high: {whales}")
|
||||
|
||||
self.log.debug(f"Starting bidding verification. Next bidder to check: {self.worklock_agent.next_bidder_to_check()}")
|
||||
|
||||
receipts = dict()
|
||||
iteration = 1
|
||||
while not self.worklock_agent.bidders_checked():
|
||||
receipt = self.worklock_agent.verify_bidding_correctness(transacting_power=self.transacting_power,
|
||||
gas_limit=gas_limit)
|
||||
self.log.debug(f"Iteration {iteration}. Next bidder to check: {self.worklock_agent.next_bidder_to_check()}")
|
||||
receipts[iteration] = receipt
|
||||
iteration += 1
|
||||
return receipts
|
||||
|
||||
def refund_deposit(self) -> dict:
|
||||
"""Refund ethers for completed work"""
|
||||
if not self.available_refund:
|
||||
raise self.BidderError(f'There is no refund available for {self.checksum_address}')
|
||||
receipt = self.worklock_agent.refund(transacting_power=self.transacting_power)
|
||||
return receipt
|
||||
|
||||
def withdraw_compensation(self) -> TxReceipt:
|
||||
"""Withdraw compensation after force refund"""
|
||||
if not self.available_compensation:
|
||||
raise self.BidderError(f'There is no compensation available for {self.checksum_address}; '
|
||||
f'Did you mean to call "refund"?')
|
||||
receipt = self.worklock_agent.withdraw_compensation(transacting_power=self.transacting_power)
|
||||
return receipt
|
||||
|
||||
#
|
||||
# Calls
|
||||
#
|
||||
|
||||
@property
|
||||
def get_deposited_eth(self) -> int:
|
||||
bid = self.worklock_agent.get_deposited_eth(checksum_address=self.checksum_address)
|
||||
return bid
|
||||
|
||||
@property
|
||||
def has_claimed(self) -> bool:
|
||||
has_claimed = self.worklock_agent.check_claim(self.checksum_address)
|
||||
return has_claimed
|
||||
|
||||
@property
|
||||
def completed_work(self) -> int:
|
||||
work = self.staking_agent.get_completed_work(bidder_address=self.checksum_address)
|
||||
completed_work = work - self.refunded_work
|
||||
return completed_work
|
||||
|
||||
@property
|
||||
def remaining_work(self) -> int:
|
||||
try:
|
||||
work = self.worklock_agent.get_remaining_work(checksum_address=self.checksum_address)
|
||||
except (TestTransactionFailed, ValidationError, ValueError): # TODO: 1950
|
||||
work = 0
|
||||
return work
|
||||
|
||||
@property
|
||||
def refunded_work(self) -> int:
|
||||
work = self.worklock_agent.get_refunded_work(checksum_address=self.checksum_address)
|
||||
return work
|
||||
|
||||
@property
|
||||
def available_refund(self) -> int:
|
||||
refund_eth = self.worklock_agent.get_available_refund(checksum_address=self.checksum_address)
|
||||
return refund_eth
|
||||
|
||||
@property
|
||||
def available_compensation(self) -> int:
|
||||
compensation_eth = self.worklock_agent.get_available_compensation(checksum_address=self.checksum_address)
|
||||
return compensation_eth
|
||||
|
||||
@property
|
||||
def available_claim(self) -> int:
|
||||
tokens = self.worklock_agent.eth_to_tokens(self.get_deposited_eth)
|
||||
return tokens
|
||||
|
|
|
@ -26,12 +26,11 @@ from constant_sorrow.constants import ( # type: ignore
|
|||
TRANSACTION,
|
||||
CONTRACT_ATTRIBUTE
|
||||
)
|
||||
from eth_typing.encoding import HexStr
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from eth_utils.address import to_checksum_address
|
||||
from hexbytes.main import HexBytes
|
||||
from web3.contract import Contract, ContractFunction
|
||||
from web3.types import Wei, Timestamp, TxReceipt, TxParams, Nonce
|
||||
from web3.types import Wei, Timestamp, TxReceipt, TxParams
|
||||
|
||||
from nucypher.blockchain.eth.constants import (
|
||||
ADJUDICATOR_CONTRACT_NAME,
|
||||
|
@ -39,10 +38,8 @@ from nucypher.blockchain.eth.constants import (
|
|||
ETH_ADDRESS_BYTE_LENGTH,
|
||||
NUCYPHER_TOKEN_CONTRACT_NAME,
|
||||
NULL_ADDRESS,
|
||||
POLICY_MANAGER_CONTRACT_NAME,
|
||||
SUBSCRIPTION_MANAGER_CONTRACT_NAME,
|
||||
STAKING_ESCROW_CONTRACT_NAME,
|
||||
WORKLOCK_CONTRACT_NAME,
|
||||
PRE_APPLICATION_CONTRACT_NAME
|
||||
)
|
||||
from nucypher.blockchain.eth.decorators import contract_api
|
||||
|
@ -62,7 +59,6 @@ from nucypher.types import (
|
|||
RawSubStakeInfo,
|
||||
Period,
|
||||
Work,
|
||||
WorklockParameters,
|
||||
StakerFlags,
|
||||
StakerInfo,
|
||||
StakingProviderInfo,
|
||||
|
@ -1140,284 +1136,6 @@ class PREApplicationAgent(EthereumContractAgent):
|
|||
return receipt
|
||||
|
||||
|
||||
class WorkLockAgent(EthereumContractAgent):
|
||||
|
||||
contract_name: str = WORKLOCK_CONTRACT_NAME
|
||||
_excluded_interfaces = ('shutdown', 'tokenDeposit')
|
||||
|
||||
#
|
||||
# Transactions
|
||||
#
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def bid(self, value: Wei, transacting_power: TransactingPower) -> TxReceipt:
|
||||
"""Bid for NU tokens with ETH."""
|
||||
contract_function: ContractFunction = self.contract.functions.bid()
|
||||
receipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power,
|
||||
payload={'value': value})
|
||||
return receipt
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def cancel_bid(self, transacting_power: TransactingPower) -> TxReceipt:
|
||||
"""Cancel bid and refund deposited ETH."""
|
||||
contract_function: ContractFunction = self.contract.functions.cancelBid()
|
||||
receipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power)
|
||||
return receipt
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def force_refund(self, transacting_power: TransactingPower, addresses: List[ChecksumAddress]) -> TxReceipt:
|
||||
"""Force refund to bidders who can get tokens more than maximum allowed."""
|
||||
addresses = sorted(addresses, key=str.casefold)
|
||||
contract_function: ContractFunction = self.contract.functions.forceRefund(addresses)
|
||||
receipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power)
|
||||
return receipt
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def verify_bidding_correctness(self,
|
||||
transacting_power: TransactingPower,
|
||||
gas_limit: Wei, # TODO - #842: Gas Management
|
||||
gas_to_save_state: Wei = Wei(30000)) -> TxReceipt:
|
||||
"""Verify all bids are less than max allowed bid"""
|
||||
contract_function: ContractFunction = self.contract.functions.verifyBiddingCorrectness(gas_to_save_state)
|
||||
receipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power,
|
||||
transaction_gas_limit=gas_limit)
|
||||
return receipt
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def claim(self, transacting_power: TransactingPower) -> TxReceipt:
|
||||
"""
|
||||
Claim tokens - will be deposited and locked as stake in the StakingEscrow contract.
|
||||
"""
|
||||
contract_function: ContractFunction = self.contract.functions.claim()
|
||||
receipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power,
|
||||
gas_estimation_multiplier=1.5) # FIXME
|
||||
return receipt
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def refund(self, transacting_power: TransactingPower) -> TxReceipt:
|
||||
"""Refund ETH for completed work."""
|
||||
contract_function: ContractFunction = self.contract.functions.refund()
|
||||
receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power)
|
||||
return receipt
|
||||
|
||||
@contract_api(TRANSACTION)
|
||||
def withdraw_compensation(self, transacting_power: TransactingPower) -> TxReceipt:
|
||||
"""Withdraw compensation after force refund."""
|
||||
contract_function: ContractFunction = self.contract.functions.withdrawCompensation()
|
||||
receipt: TxReceipt = self.blockchain.send_transaction(contract_function=contract_function,
|
||||
transacting_power=transacting_power)
|
||||
return receipt
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def check_claim(self, checksum_address: ChecksumAddress) -> bool:
|
||||
has_claimed: bool = bool(self.contract.functions.workInfo(checksum_address).call()[2])
|
||||
return has_claimed
|
||||
|
||||
#
|
||||
# Internal
|
||||
#
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_refunded_work(self, checksum_address: ChecksumAddress) -> Work:
|
||||
work = self.contract.functions.workInfo(checksum_address).call()[1]
|
||||
return Work(work)
|
||||
|
||||
#
|
||||
# Calls
|
||||
#
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_available_refund(self, checksum_address: ChecksumAddress) -> Wei:
|
||||
refund_eth: int = self.contract.functions.getAvailableRefund(checksum_address).call()
|
||||
return Wei(refund_eth)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_available_compensation(self, checksum_address: ChecksumAddress) -> Wei:
|
||||
compensation_eth: int = self.contract.functions.compensation(checksum_address).call()
|
||||
return Wei(compensation_eth)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_deposited_eth(self, checksum_address: ChecksumAddress) -> Wei:
|
||||
current_bid: int = self.contract.functions.workInfo(checksum_address).call()[0]
|
||||
return Wei(current_bid)
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def lot_value(self) -> NuNits:
|
||||
"""
|
||||
Total number of tokens than can be bid for and awarded in or the number of NU
|
||||
tokens deposited before the bidding windows begins via tokenDeposit().
|
||||
"""
|
||||
supply: int = self.contract.functions.tokenSupply().call()
|
||||
return NuNits(supply)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_bonus_lot_value(self) -> NuNits:
|
||||
"""
|
||||
Total number of tokens than can be awarded for bonus part of bid.
|
||||
"""
|
||||
num_bidders: int = self.get_bidders_population()
|
||||
supply: int = self.lot_value - num_bidders * self.contract.functions.minAllowableLockedTokens().call()
|
||||
return NuNits(supply)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_remaining_work(self, checksum_address: str) -> Work:
|
||||
"""Get remaining work periods until full refund for the target address."""
|
||||
result = self.contract.functions.getRemainingWork(checksum_address).call()
|
||||
return Work(result)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_bonus_eth_supply(self) -> Wei:
|
||||
supply = self.contract.functions.bonusETHSupply().call()
|
||||
return Wei(supply)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_eth_supply(self) -> Wei:
|
||||
num_bidders: int = self.get_bidders_population()
|
||||
min_bid: int = self.minimum_allowed_bid
|
||||
supply: int = num_bidders * min_bid + self.get_bonus_eth_supply()
|
||||
return Wei(supply)
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def boosting_refund(self) -> int:
|
||||
refund = self.contract.functions.boostingRefund().call()
|
||||
return refund
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def slowing_refund(self) -> int:
|
||||
refund: int = self.contract.functions.SLOWING_REFUND().call()
|
||||
return refund
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_bonus_refund_rate(self) -> float:
|
||||
f = self.contract.functions
|
||||
slowing_refund: int = f.SLOWING_REFUND().call()
|
||||
boosting_refund: int = f.boostingRefund().call()
|
||||
refund_rate: float = self.get_bonus_deposit_rate() * slowing_refund / boosting_refund
|
||||
return refund_rate
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_base_refund_rate(self) -> int:
|
||||
f = self.contract.functions
|
||||
slowing_refund = f.SLOWING_REFUND().call()
|
||||
boosting_refund = f.boostingRefund().call()
|
||||
refund_rate = self.get_base_deposit_rate() * slowing_refund / boosting_refund
|
||||
return refund_rate
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_base_deposit_rate(self) -> int:
|
||||
min_allowed_locked_tokens: NuNits = self.contract.functions.minAllowableLockedTokens().call()
|
||||
deposit_rate: int = min_allowed_locked_tokens // self.minimum_allowed_bid # should never divide by 0
|
||||
return deposit_rate
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_bonus_deposit_rate(self) -> int:
|
||||
try:
|
||||
deposit_rate: int = self.get_bonus_lot_value() // self.get_bonus_eth_supply()
|
||||
except ZeroDivisionError:
|
||||
return 0
|
||||
return deposit_rate
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def eth_to_tokens(self, value: Wei) -> NuNits:
|
||||
tokens: int = self.contract.functions.ethToTokens(value).call()
|
||||
return NuNits(tokens)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def eth_to_work(self, value: Wei) -> Work:
|
||||
tokens: int = self.contract.functions.ethToWork(value).call()
|
||||
return Work(tokens)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def work_to_eth(self, value: Work) -> Wei:
|
||||
wei: Wei = self.contract.functions.workToETH(value).call()
|
||||
return Wei(wei)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_bidders_population(self) -> int:
|
||||
"""Returns the number of bidders on the blockchain"""
|
||||
return self.contract.functions.getBiddersLength().call()
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def get_bidders(self) -> List[ChecksumAddress]:
|
||||
"""Returns a list of bidders"""
|
||||
num_bidders: int = self.get_bidders_population()
|
||||
bidders: List[ChecksumAddress] = [self.contract.functions.bidders(i).call() for i in range(num_bidders)]
|
||||
return bidders
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def is_claiming_available(self) -> bool:
|
||||
"""Returns True if claiming is available"""
|
||||
result: bool = self.contract.functions.isClaimingAvailable().call()
|
||||
return result
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def estimate_verifying_correctness(self, gas_limit: Wei, gas_to_save_state: Wei = Wei(30000)) -> int: # TODO - #842: Gas Management
|
||||
"""Returns how many bidders will be verified using specified gas limit"""
|
||||
return self.contract.functions.verifyBiddingCorrectness(gas_to_save_state).call({'gas': gas_limit})
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def next_bidder_to_check(self) -> int:
|
||||
"""Returns the index of the next bidder to check as part of the bids verification process"""
|
||||
return self.contract.functions.nextBidderToCheck().call()
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def bidders_checked(self) -> bool:
|
||||
"""Returns True if bidders have been checked"""
|
||||
bidders_population: int = self.get_bidders_population()
|
||||
return self.next_bidder_to_check() == bidders_population
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def minimum_allowed_bid(self) -> Wei:
|
||||
min_bid: Wei = self.contract.functions.minAllowedBid().call()
|
||||
return min_bid
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def start_bidding_date(self) -> Timestamp:
|
||||
date: int = self.contract.functions.startBidDate().call()
|
||||
return Timestamp(date)
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def end_bidding_date(self) -> Timestamp:
|
||||
date: int = self.contract.functions.endBidDate().call()
|
||||
return Timestamp(date)
|
||||
|
||||
@property # type: ignore
|
||||
@contract_api(CONTRACT_ATTRIBUTE)
|
||||
def end_cancellation_date(self) -> Timestamp:
|
||||
date: int = self.contract.functions.endCancellationDate().call()
|
||||
return Timestamp(date)
|
||||
|
||||
@contract_api(CONTRACT_CALL)
|
||||
def worklock_parameters(self) -> WorklockParameters:
|
||||
parameter_signatures = (
|
||||
'tokenSupply',
|
||||
'startBidDate',
|
||||
'endBidDate',
|
||||
'endCancellationDate',
|
||||
'boostingRefund',
|
||||
'stakingPeriods',
|
||||
'minAllowedBid',
|
||||
)
|
||||
|
||||
def _call_function_by_name(name: str) -> int:
|
||||
return getattr(self.contract.functions, name)().call()
|
||||
|
||||
parameters = WorklockParameters(map(_call_function_by_name, parameter_signatures))
|
||||
return parameters
|
||||
|
||||
|
||||
class ContractAgency:
|
||||
"""Where agents live and die."""
|
||||
|
||||
|
|
|
@ -20,26 +20,18 @@
|
|||
#
|
||||
|
||||
DISPATCHER_CONTRACT_NAME = 'Dispatcher'
|
||||
STAKING_INTERFACE_ROUTER_CONTRACT_NAME = "StakingInterfaceRouter"
|
||||
NUCYPHER_TOKEN_CONTRACT_NAME = 'NuCypherToken'
|
||||
STAKING_ESCROW_CONTRACT_NAME = 'StakingEscrow'
|
||||
STAKING_ESCROW_STUB_CONTRACT_NAME = 'StakingEscrowStub'
|
||||
POLICY_MANAGER_CONTRACT_NAME = 'PolicyManager'
|
||||
STAKING_INTERFACE_CONTRACT_NAME = 'StakingInterface'
|
||||
ADJUDICATOR_CONTRACT_NAME = 'Adjudicator'
|
||||
WORKLOCK_CONTRACT_NAME = 'WorkLock'
|
||||
PRE_APPLICATION_CONTRACT_NAME = 'SimplePREApplication' # TODO: Use the real PREApplication
|
||||
SUBSCRIPTION_MANAGER_CONTRACT_NAME = 'SubscriptionManager'
|
||||
|
||||
NUCYPHER_CONTRACT_NAMES = (
|
||||
NUCYPHER_TOKEN_CONTRACT_NAME,
|
||||
STAKING_ESCROW_CONTRACT_NAME,
|
||||
POLICY_MANAGER_CONTRACT_NAME,
|
||||
ADJUDICATOR_CONTRACT_NAME,
|
||||
DISPATCHER_CONTRACT_NAME,
|
||||
STAKING_INTERFACE_CONTRACT_NAME,
|
||||
STAKING_INTERFACE_ROUTER_CONTRACT_NAME,
|
||||
WORKLOCK_CONTRACT_NAME,
|
||||
PRE_APPLICATION_CONTRACT_NAME,
|
||||
SUBSCRIPTION_MANAGER_CONTRACT_NAME
|
||||
)
|
||||
|
|
|
@ -32,11 +32,9 @@ from web3.contract import Contract
|
|||
from nucypher.blockchain.economics import Economics
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
AdjudicatorAgent,
|
||||
ContractAgency,
|
||||
EthereumContractAgent,
|
||||
NucypherTokenAgent,
|
||||
StakingEscrowAgent,
|
||||
WorkLockAgent,
|
||||
PREApplicationAgent,
|
||||
SubscriptionManagerAgent
|
||||
)
|
||||
|
@ -545,7 +543,10 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
STUB_MIN_ALLOWED_TOKENS = NU(15_000, 'NU').to_units()
|
||||
STUB_MAX_ALLOWED_TOKENS = NU(30_000_000, 'NU').to_units()
|
||||
|
||||
def __init__(self, staking_interface: ChecksumAddress = None, *args, **kwargs):
|
||||
def __init__(self,
|
||||
staking_interface: ChecksumAddress = None,
|
||||
worklock_address: ChecksumAddress = None,
|
||||
*args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__dispatcher_contract = None
|
||||
|
||||
|
@ -553,20 +554,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
self.token_contract = self.blockchain.get_contract_by_name(registry=self.registry,
|
||||
contract_name=token_contract_name)
|
||||
self.threshold_staking_address = staking_interface
|
||||
self.worklock = self._get_contract(deployer_class=WorklockDeployer)
|
||||
|
||||
def _get_contract(self, deployer_class) -> VersionedContract:
|
||||
contract_name = deployer_class.contract_name
|
||||
try:
|
||||
proxy_name = deployer_class._proxy_deployer.contract_name
|
||||
except AttributeError:
|
||||
proxy_name = None
|
||||
try:
|
||||
return self.blockchain.get_contract_by_name(registry=self.registry,
|
||||
contract_name=contract_name,
|
||||
proxy_name=proxy_name)
|
||||
except self.registry.UnknownContract:
|
||||
return None
|
||||
self.worklock_address = worklock_address
|
||||
|
||||
def _deploy_stub(self,
|
||||
transacting_power: TransactingPower,
|
||||
|
@ -600,7 +588,7 @@ class StakingEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin, Owna
|
|||
**overrides):
|
||||
constructor_kwargs = {}
|
||||
constructor_kwargs.update({"_token": self.token_contract.address,
|
||||
"_workLock": self.worklock.address if self.worklock is not None else NULL_ADDRESS,
|
||||
"_workLock": self.worklock_address if self.worklock_address is not None else NULL_ADDRESS,
|
||||
"_tStaking": self.threshold_staking_address})
|
||||
constructor_kwargs.update(overrides)
|
||||
constructor_kwargs = {k: v for k, v in constructor_kwargs.items() if v is not None}
|
||||
|
@ -947,119 +935,3 @@ class PREApplicationDeployer(BaseContractDeployer):
|
|||
self._contract = contract
|
||||
self.deployment_receipts = dict(zip(self.deployment_steps, (receipt, )))
|
||||
return self.deployment_receipts
|
||||
|
||||
|
||||
# TODO: delete me
|
||||
class WorklockDeployer(BaseContractDeployer):
|
||||
|
||||
agency = WorkLockAgent
|
||||
contract_name = agency.contract_name
|
||||
deployment_steps = ('contract_deployment', 'approve_funding', 'fund_worklock')
|
||||
_upgradeable = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
token_contract_name = NucypherTokenDeployer.contract_name
|
||||
self.token_contract = self.blockchain.get_contract_by_name(registry=self.registry,
|
||||
contract_name=token_contract_name)
|
||||
|
||||
staking_contract_name = StakingEscrowDeployer.contract_name
|
||||
proxy_name = StakingEscrowDeployer._proxy_deployer.contract_name
|
||||
try:
|
||||
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
|
||||
contract_name=staking_contract_name,
|
||||
proxy_name=proxy_name)
|
||||
except self.registry.UnknownContract:
|
||||
staking_contract_name = StakingEscrowDeployer.contract_name_stub
|
||||
self.staking_contract = self.blockchain.get_contract_by_name(registry=self.registry,
|
||||
contract_name=staking_contract_name,
|
||||
proxy_name=proxy_name)
|
||||
|
||||
def _deploy_essential(self, transacting_power: TransactingPower, gas_limit: int = None, confirmations: int = 0):
|
||||
# Deploy
|
||||
constructor_args = (self.token_contract.address,
|
||||
self.staking_contract.address,
|
||||
*self.economics.worklock_deployment_parameters)
|
||||
|
||||
worklock_contract, receipt = self.blockchain.deploy_contract(transacting_power,
|
||||
self.registry,
|
||||
self.contract_name,
|
||||
*constructor_args,
|
||||
gas_limit=gas_limit,
|
||||
confirmations=confirmations)
|
||||
|
||||
self._contract = worklock_contract
|
||||
return worklock_contract, receipt
|
||||
|
||||
def deploy(self,
|
||||
transacting_power: TransactingPower,
|
||||
gas_limit: int = None,
|
||||
progress=None,
|
||||
confirmations: int = 0,
|
||||
deployment_mode=FULL,
|
||||
ignore_deployed: bool = False,
|
||||
emitter=None,
|
||||
) -> Dict[str, dict]:
|
||||
|
||||
if deployment_mode != FULL:
|
||||
raise self.ContractDeploymentError(f"{self.contract_name} cannot be deployed in {deployment_mode} mode")
|
||||
|
||||
self.check_deployment_readiness(deployer_address=transacting_power.account,
|
||||
ignore_deployed=ignore_deployed)
|
||||
|
||||
# Essential
|
||||
if emitter:
|
||||
emitter.message(f"\nNext Transaction: {self.contract_name} Contract Creation", color='blue', bold=True)
|
||||
worklock_contract, deployment_receipt = self._deploy_essential(transacting_power=transacting_power,
|
||||
gas_limit=gas_limit,
|
||||
confirmations=confirmations)
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
||||
# Funding
|
||||
approve_receipt, funding_receipt = self.fund(transacting_power=transacting_power,
|
||||
progress=progress,
|
||||
confirmations=confirmations,
|
||||
emitter=emitter)
|
||||
|
||||
# Gather the transaction hashes
|
||||
self.deployment_receipts = dict(zip(self.deployment_steps, (deployment_receipt,
|
||||
approve_receipt,
|
||||
funding_receipt)))
|
||||
return self.deployment_receipts
|
||||
|
||||
def fund(self,
|
||||
transacting_power: TransactingPower,
|
||||
progress=None,
|
||||
confirmations: int = 0,
|
||||
emitter=None
|
||||
) -> Tuple[dict, dict]:
|
||||
"""
|
||||
Convenience method for funding the contract and establishing the
|
||||
total worklock lot value to be auctioned.
|
||||
"""
|
||||
supply = int(self.economics.worklock_supply)
|
||||
|
||||
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=self.registry)
|
||||
if emitter:
|
||||
emitter.message(f"\nNext Transaction: Approve Token Transfer to {self.contract_name}", color='blue', bold=True)
|
||||
approve_function = token_agent.contract.functions.approve(self.contract_address, supply)
|
||||
approve_receipt = self.blockchain.send_transaction(contract_function=approve_function,
|
||||
transacting_power=transacting_power,
|
||||
confirmations=confirmations)
|
||||
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
||||
if emitter:
|
||||
emitter.message(f"\nNext Transaction: Transfer Tokens to {self.contract_name}", color='blue', bold=True)
|
||||
funding_function = self.contract.functions.tokenDeposit(supply)
|
||||
funding_receipt = self.blockchain.send_transaction(contract_function=funding_function,
|
||||
transacting_power=transacting_power,
|
||||
confirmations=confirmations)
|
||||
|
||||
if progress:
|
||||
progress.update(1)
|
||||
|
||||
return approve_receipt, funding_receipt
|
||||
|
|
|
@ -1,555 +0,0 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pragma solidity ^0.8.0; // TODO use 0.7.x version and revert changes ?
|
||||
|
||||
|
||||
import "zeppelin/math/SafeMath.sol";
|
||||
import "zeppelin/token/ERC20/SafeERC20.sol";
|
||||
import "zeppelin/utils/Address.sol";
|
||||
import "zeppelin/ownership/Ownable.sol";
|
||||
import "contracts/NuCypherToken.sol";
|
||||
import "contracts/IStakingEscrow.sol";
|
||||
import "contracts/lib/AdditionalMath.sol";
|
||||
|
||||
|
||||
/**
|
||||
* @notice The WorkLock distribution contract
|
||||
*/
|
||||
contract WorkLock is Ownable {
|
||||
using SafeERC20 for NuCypherToken;
|
||||
using SafeMath for uint256;
|
||||
using AdditionalMath for uint256;
|
||||
using Address for address payable;
|
||||
using Address for address;
|
||||
|
||||
event Deposited(address indexed sender, uint256 value);
|
||||
event Bid(address indexed sender, uint256 depositedETH);
|
||||
event Claimed(address indexed sender, uint256 claimedTokens);
|
||||
event Refund(address indexed sender, uint256 refundETH, uint256 completedWork);
|
||||
event Canceled(address indexed sender, uint256 value);
|
||||
event BiddersChecked(address indexed sender, uint256 startIndex, uint256 endIndex);
|
||||
event ForceRefund(address indexed sender, address indexed bidder, uint256 refundETH);
|
||||
event CompensationWithdrawn(address indexed sender, uint256 value);
|
||||
event Shutdown(address indexed sender);
|
||||
|
||||
struct WorkInfo {
|
||||
uint256 depositedETH;
|
||||
uint256 completedWork;
|
||||
bool claimed;
|
||||
uint128 index;
|
||||
}
|
||||
|
||||
uint16 public constant SLOWING_REFUND = 100;
|
||||
uint256 private constant MAX_ETH_SUPPLY = 2e10 ether;
|
||||
|
||||
NuCypherToken public immutable token;
|
||||
IStakingEscrow public immutable escrow;
|
||||
|
||||
/*
|
||||
* @dev WorkLock calculations:
|
||||
* bid = minBid + bonusETHPart
|
||||
* bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens
|
||||
* bonusDepositRate = bonusTokenSupply / bonusETHSupply
|
||||
* claimedTokens = minAllowableLockedTokens + bonusETHPart * bonusDepositRate
|
||||
* bonusRefundRate = bonusDepositRate * SLOWING_REFUND / boostingRefund
|
||||
* refundETH = completedWork / refundRate
|
||||
*/
|
||||
uint256 public immutable boostingRefund;
|
||||
uint256 public immutable minAllowedBid;
|
||||
uint16 public immutable stakingPeriods;
|
||||
// copy from the escrow contract
|
||||
uint256 public immutable maxAllowableLockedTokens;
|
||||
uint256 public immutable minAllowableLockedTokens;
|
||||
|
||||
uint256 public tokenSupply;
|
||||
uint256 public startBidDate;
|
||||
uint256 public endBidDate;
|
||||
uint256 public endCancellationDate;
|
||||
|
||||
uint256 public bonusETHSupply;
|
||||
mapping(address => WorkInfo) public workInfo;
|
||||
mapping(address => uint256) public compensation;
|
||||
|
||||
address[] public bidders;
|
||||
// if value == bidders.length then WorkLock is fully checked
|
||||
uint256 public nextBidderToCheck;
|
||||
|
||||
/**
|
||||
* @dev Checks timestamp regarding cancellation window
|
||||
*/
|
||||
modifier afterCancellationWindow()
|
||||
{
|
||||
require(block.timestamp >= endCancellationDate,
|
||||
"Operation is allowed when cancellation phase is over");
|
||||
_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param _token Token contract
|
||||
* @param _escrow Escrow contract
|
||||
* @param _startBidDate Timestamp when bidding starts
|
||||
* @param _endBidDate Timestamp when bidding will end
|
||||
* @param _endCancellationDate Timestamp when cancellation will ends
|
||||
* @param _boostingRefund Coefficient to boost refund ETH
|
||||
* @param _stakingPeriods Amount of periods during which tokens will be locked after claiming
|
||||
* @param _minAllowedBid Minimum allowed ETH amount for bidding
|
||||
*/
|
||||
constructor(
|
||||
NuCypherToken _token,
|
||||
IStakingEscrow _escrow,
|
||||
uint256 _startBidDate,
|
||||
uint256 _endBidDate,
|
||||
uint256 _endCancellationDate,
|
||||
uint256 _boostingRefund,
|
||||
uint16 _stakingPeriods,
|
||||
uint256 _minAllowedBid
|
||||
) {
|
||||
uint256 totalSupply = _token.totalSupply();
|
||||
require(totalSupply > 0 && // token contract is deployed and accessible
|
||||
_escrow.secondsPerPeriod() > 0 && // escrow contract is deployed and accessible
|
||||
_escrow.token() == _token && // same token address for worklock and escrow
|
||||
_endBidDate > _startBidDate && // bidding period lasts some time
|
||||
_endBidDate > block.timestamp && // there is time to make a bid
|
||||
_endCancellationDate >= _endBidDate && // cancellation window includes bidding
|
||||
_minAllowedBid > 0 && // min allowed bid was set
|
||||
_boostingRefund > 0 && // boosting coefficient was set
|
||||
_stakingPeriods >= _escrow.minLockedPeriods()); // staking duration is consistent with escrow contract
|
||||
// worst case for `ethToWork()` and `workToETH()`,
|
||||
// when ethSupply == MAX_ETH_SUPPLY and tokenSupply == totalSupply
|
||||
require(MAX_ETH_SUPPLY * totalSupply * SLOWING_REFUND / MAX_ETH_SUPPLY / totalSupply == SLOWING_REFUND &&
|
||||
MAX_ETH_SUPPLY * totalSupply * _boostingRefund / MAX_ETH_SUPPLY / totalSupply == _boostingRefund);
|
||||
|
||||
token = _token;
|
||||
escrow = _escrow;
|
||||
startBidDate = _startBidDate;
|
||||
endBidDate = _endBidDate;
|
||||
endCancellationDate = _endCancellationDate;
|
||||
boostingRefund = _boostingRefund;
|
||||
stakingPeriods = _stakingPeriods;
|
||||
minAllowedBid = _minAllowedBid;
|
||||
maxAllowableLockedTokens = _escrow.maxAllowableLockedTokens();
|
||||
minAllowableLockedTokens = _escrow.minAllowableLockedTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deposit tokens to contract
|
||||
* @param _value Amount of tokens to transfer
|
||||
*/
|
||||
function tokenDeposit(uint256 _value) external {
|
||||
require(block.timestamp < endBidDate, "Can't deposit more tokens after end of bidding");
|
||||
token.safeTransferFrom(msg.sender, address(this), _value);
|
||||
tokenSupply += _value;
|
||||
emit Deposited(msg.sender, _value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate amount of tokens that will be get for specified amount of ETH
|
||||
* @dev This value will be fixed only after end of bidding
|
||||
*/
|
||||
function ethToTokens(uint256 _ethAmount) public view returns (uint256) {
|
||||
if (_ethAmount < minAllowedBid) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// when all participants bid with the same minimum amount of eth
|
||||
if (bonusETHSupply == 0) {
|
||||
return tokenSupply / bidders.length;
|
||||
}
|
||||
|
||||
uint256 bonusETH = _ethAmount - minAllowedBid;
|
||||
uint256 bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens;
|
||||
return minAllowableLockedTokens + bonusETH.mul(bonusTokenSupply).div(bonusETHSupply);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate amount of work that need to be done to refund specified amount of ETH
|
||||
*/
|
||||
function ethToWork(uint256 _ethAmount, uint256 _tokenSupply, uint256 _ethSupply)
|
||||
internal view returns (uint256)
|
||||
{
|
||||
return _ethAmount.mul(_tokenSupply).mul(SLOWING_REFUND).divCeil(_ethSupply.mul(boostingRefund));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate amount of work that need to be done to refund specified amount of ETH
|
||||
* @dev This value will be fixed only after end of bidding
|
||||
* @param _ethToReclaim Specified sum of ETH staker wishes to reclaim following completion of work
|
||||
* @param _restOfDepositedETH Remaining ETH in staker's deposit once ethToReclaim sum has been subtracted
|
||||
* @dev _ethToReclaim + _restOfDepositedETH = depositedETH
|
||||
*/
|
||||
function ethToWork(uint256 _ethToReclaim, uint256 _restOfDepositedETH) internal view returns (uint256) {
|
||||
|
||||
uint256 baseETHSupply = bidders.length * minAllowedBid;
|
||||
// when all participants bid with the same minimum amount of eth
|
||||
if (bonusETHSupply == 0) {
|
||||
return ethToWork(_ethToReclaim, tokenSupply, baseETHSupply);
|
||||
}
|
||||
|
||||
uint256 baseETH = 0;
|
||||
uint256 bonusETH = 0;
|
||||
|
||||
// If the staker's total remaining deposit (including the specified sum of ETH to reclaim)
|
||||
// is lower than the minimum bid size,
|
||||
// then only the base part is used to calculate the work required to reclaim ETH
|
||||
if (_ethToReclaim + _restOfDepositedETH <= minAllowedBid) {
|
||||
baseETH = _ethToReclaim;
|
||||
|
||||
// If the staker's remaining deposit (not including the specified sum of ETH to reclaim)
|
||||
// is still greater than the minimum bid size,
|
||||
// then only the bonus part is used to calculate the work required to reclaim ETH
|
||||
} else if (_restOfDepositedETH >= minAllowedBid) {
|
||||
bonusETH = _ethToReclaim;
|
||||
|
||||
// If the staker's remaining deposit (not including the specified sum of ETH to reclaim)
|
||||
// is lower than the minimum bid size,
|
||||
// then both the base and bonus parts must be used to calculate the work required to reclaim ETH
|
||||
} else {
|
||||
bonusETH = _ethToReclaim + _restOfDepositedETH - minAllowedBid;
|
||||
baseETH = _ethToReclaim - bonusETH;
|
||||
}
|
||||
|
||||
uint256 baseTokenSupply = bidders.length * minAllowableLockedTokens;
|
||||
uint256 work = 0;
|
||||
if (baseETH > 0) {
|
||||
work = ethToWork(baseETH, baseTokenSupply, baseETHSupply);
|
||||
}
|
||||
|
||||
if (bonusETH > 0) {
|
||||
uint256 bonusTokenSupply = tokenSupply - baseTokenSupply;
|
||||
work += ethToWork(bonusETH, bonusTokenSupply, bonusETHSupply);
|
||||
}
|
||||
|
||||
return work;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate amount of work that need to be done to refund specified amount of ETH
|
||||
* @dev This value will be fixed only after end of bidding
|
||||
*/
|
||||
function ethToWork(uint256 _ethAmount) public view returns (uint256) {
|
||||
return ethToWork(_ethAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate amount of ETH that will be refund for completing specified amount of work
|
||||
*/
|
||||
function workToETH(uint256 _completedWork, uint256 _ethSupply, uint256 _tokenSupply)
|
||||
internal view returns (uint256)
|
||||
{
|
||||
return _completedWork.mul(_ethSupply).mul(boostingRefund).div(_tokenSupply.mul(SLOWING_REFUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculate amount of ETH that will be refund for completing specified amount of work
|
||||
* @dev This value will be fixed only after end of bidding
|
||||
*/
|
||||
function workToETH(uint256 _completedWork, uint256 _depositedETH) public view returns (uint256) {
|
||||
uint256 baseETHSupply = bidders.length * minAllowedBid;
|
||||
// when all participants bid with the same minimum amount of eth
|
||||
if (bonusETHSupply == 0) {
|
||||
return workToETH(_completedWork, baseETHSupply, tokenSupply);
|
||||
}
|
||||
|
||||
uint256 bonusWork = 0;
|
||||
uint256 bonusETH = 0;
|
||||
uint256 baseTokenSupply = bidders.length * minAllowableLockedTokens;
|
||||
|
||||
if (_depositedETH > minAllowedBid) {
|
||||
bonusETH = _depositedETH - minAllowedBid;
|
||||
uint256 bonusTokenSupply = tokenSupply - baseTokenSupply;
|
||||
bonusWork = ethToWork(bonusETH, bonusTokenSupply, bonusETHSupply);
|
||||
|
||||
if (_completedWork <= bonusWork) {
|
||||
return workToETH(_completedWork, bonusETHSupply, bonusTokenSupply);
|
||||
}
|
||||
}
|
||||
|
||||
_completedWork -= bonusWork;
|
||||
return bonusETH + workToETH(_completedWork, baseETHSupply, baseTokenSupply);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get remaining work to full refund
|
||||
*/
|
||||
function getRemainingWork(address _bidder) external view returns (uint256) {
|
||||
WorkInfo storage info = workInfo[_bidder];
|
||||
uint256 completedWork = escrow.getCompletedWork(_bidder).sub(info.completedWork);
|
||||
uint256 remainingWork = ethToWork(info.depositedETH);
|
||||
if (remainingWork <= completedWork) {
|
||||
return 0;
|
||||
}
|
||||
return remainingWork - completedWork;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get length of bidders array
|
||||
*/
|
||||
function getBiddersLength() external view returns (uint256) {
|
||||
return bidders.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Bid for tokens by transferring ETH
|
||||
*/
|
||||
function bid() external payable {
|
||||
require(block.timestamp >= startBidDate, "Bidding is not open yet");
|
||||
require(block.timestamp < endBidDate, "Bidding is already finished");
|
||||
WorkInfo storage info = workInfo[msg.sender];
|
||||
|
||||
// first bid
|
||||
if (info.depositedETH == 0) {
|
||||
require(msg.value >= minAllowedBid, "Bid must be at least minimum");
|
||||
require(bidders.length < tokenSupply / minAllowableLockedTokens, "Not enough tokens for more bidders");
|
||||
info.index = uint128(bidders.length);
|
||||
bidders.push(msg.sender);
|
||||
bonusETHSupply = bonusETHSupply.add(msg.value - minAllowedBid);
|
||||
} else {
|
||||
bonusETHSupply = bonusETHSupply.add(msg.value);
|
||||
}
|
||||
|
||||
info.depositedETH = info.depositedETH.add(msg.value);
|
||||
emit Bid(msg.sender, msg.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Cancel bid and refund deposited ETH
|
||||
*/
|
||||
function cancelBid() external {
|
||||
require(block.timestamp < endCancellationDate,
|
||||
"Cancellation allowed only during cancellation window");
|
||||
WorkInfo storage info = workInfo[msg.sender];
|
||||
require(info.depositedETH > 0, "No bid to cancel");
|
||||
require(!info.claimed, "Tokens are already claimed");
|
||||
uint256 refundETH = info.depositedETH;
|
||||
info.depositedETH = 0;
|
||||
|
||||
// remove from bidders array, move last bidder to the empty place
|
||||
uint256 lastIndex = bidders.length - 1;
|
||||
if (info.index != lastIndex) {
|
||||
address lastBidder = bidders[lastIndex];
|
||||
bidders[info.index] = lastBidder;
|
||||
workInfo[lastBidder].index = info.index;
|
||||
}
|
||||
bidders.pop();
|
||||
|
||||
if (refundETH > minAllowedBid) {
|
||||
bonusETHSupply = bonusETHSupply.sub(refundETH - minAllowedBid);
|
||||
}
|
||||
payable(msg.sender).sendValue(refundETH);
|
||||
emit Canceled(msg.sender, refundETH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Cancels distribution, makes possible to retrieve all bids and owner gets all tokens
|
||||
*/
|
||||
function shutdown() external onlyOwner {
|
||||
require(!isClaimingAvailable(), "Claiming has already been enabled");
|
||||
internalShutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Cancels distribution, makes possible to retrieve all bids and owner gets all tokens
|
||||
*/
|
||||
function internalShutdown() internal {
|
||||
startBidDate = 0;
|
||||
endBidDate = 0;
|
||||
endCancellationDate = type(uint256).max; // "infinite" cancellation window
|
||||
token.safeTransfer(owner(), tokenSupply);
|
||||
emit Shutdown(msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Make force refund to bidders who can get tokens more than maximum allowed
|
||||
* @param _biddersForRefund Sorted list of unique bidders. Only bidders who must receive a refund
|
||||
*/
|
||||
function forceRefund(address payable[] calldata _biddersForRefund) external afterCancellationWindow {
|
||||
require(nextBidderToCheck != bidders.length, "Bidders have already been checked");
|
||||
|
||||
uint256 length = _biddersForRefund.length;
|
||||
require(length > 0, "Must be at least one bidder for a refund");
|
||||
|
||||
uint256 minNumberOfBidders = tokenSupply.divCeil(maxAllowableLockedTokens);
|
||||
if (bidders.length < minNumberOfBidders) {
|
||||
internalShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
address previousBidder = _biddersForRefund[0];
|
||||
uint256 minBid = workInfo[previousBidder].depositedETH;
|
||||
uint256 maxBid = minBid;
|
||||
|
||||
// get minimum and maximum bids
|
||||
for (uint256 i = 1; i < length; i++) {
|
||||
address bidder = _biddersForRefund[i];
|
||||
uint256 depositedETH = workInfo[bidder].depositedETH;
|
||||
require(bidder > previousBidder && depositedETH > 0, "Addresses must be an array of unique bidders");
|
||||
if (minBid > depositedETH) {
|
||||
minBid = depositedETH;
|
||||
} else if (maxBid < depositedETH) {
|
||||
maxBid = depositedETH;
|
||||
}
|
||||
previousBidder = bidder;
|
||||
}
|
||||
|
||||
uint256[] memory refunds = new uint256[](length);
|
||||
// first step - align at a minimum bid
|
||||
if (minBid != maxBid) {
|
||||
for (uint256 i = 0; i < length; i++) {
|
||||
address bidder = _biddersForRefund[i];
|
||||
WorkInfo storage info = workInfo[bidder];
|
||||
if (info.depositedETH > minBid) {
|
||||
refunds[i] = info.depositedETH - minBid;
|
||||
info.depositedETH = minBid;
|
||||
bonusETHSupply -= refunds[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require(ethToTokens(minBid) > maxAllowableLockedTokens,
|
||||
"At least one of bidders has allowable bid");
|
||||
|
||||
// final bids adjustment (only for bonus part)
|
||||
// (min_whale_bid * token_supply - max_stake * eth_supply) / (token_supply - max_stake * n_whales)
|
||||
uint256 maxBonusTokens = maxAllowableLockedTokens - minAllowableLockedTokens;
|
||||
uint256 minBonusETH = minBid - minAllowedBid;
|
||||
uint256 bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens;
|
||||
uint256 refundETH = minBonusETH.mul(bonusTokenSupply)
|
||||
.sub(maxBonusTokens.mul(bonusETHSupply))
|
||||
.divCeil(bonusTokenSupply - maxBonusTokens.mul(length));
|
||||
uint256 resultBid = minBid.sub(refundETH);
|
||||
bonusETHSupply -= length * refundETH;
|
||||
for (uint256 i = 0; i < length; i++) {
|
||||
address bidder = _biddersForRefund[i];
|
||||
WorkInfo storage info = workInfo[bidder];
|
||||
refunds[i] += refundETH;
|
||||
info.depositedETH = resultBid;
|
||||
}
|
||||
|
||||
// reset verification
|
||||
nextBidderToCheck = 0;
|
||||
|
||||
// save a refund
|
||||
for (uint256 i = 0; i < length; i++) {
|
||||
address bidder = _biddersForRefund[i];
|
||||
compensation[bidder] += refunds[i];
|
||||
emit ForceRefund(msg.sender, bidder, refunds[i]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Withdraw compensation after force refund
|
||||
*/
|
||||
function withdrawCompensation() external {
|
||||
uint256 refund = compensation[msg.sender];
|
||||
require(refund > 0, "There is no compensation");
|
||||
compensation[msg.sender] = 0;
|
||||
payable(msg.sender).sendValue(refund);
|
||||
emit CompensationWithdrawn(msg.sender, refund);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check that the claimed tokens are within `maxAllowableLockedTokens` for all participants,
|
||||
* starting from the last point `nextBidderToCheck`
|
||||
* @dev Method stops working when the remaining gas is less than `_gasToSaveState`
|
||||
* and saves the state in `nextBidderToCheck`.
|
||||
* If all bidders have been checked then `nextBidderToCheck` will be equal to the length of the bidders array
|
||||
*/
|
||||
function verifyBiddingCorrectness(uint256 _gasToSaveState) external afterCancellationWindow returns (uint256) {
|
||||
require(nextBidderToCheck != bidders.length, "Bidders have already been checked");
|
||||
|
||||
// all participants bid with the same minimum amount of eth
|
||||
uint256 index = nextBidderToCheck;
|
||||
if (bonusETHSupply == 0) {
|
||||
require(tokenSupply / bidders.length <= maxAllowableLockedTokens, "Not enough bidders");
|
||||
index = bidders.length;
|
||||
}
|
||||
|
||||
uint256 maxBonusTokens = maxAllowableLockedTokens - minAllowableLockedTokens;
|
||||
uint256 bonusTokenSupply = tokenSupply - bidders.length * minAllowableLockedTokens;
|
||||
uint256 maxBidFromMaxStake = minAllowedBid + maxBonusTokens.mul(bonusETHSupply).div(bonusTokenSupply);
|
||||
|
||||
|
||||
while (index < bidders.length && gasleft() > _gasToSaveState) {
|
||||
address bidder = bidders[index];
|
||||
require(workInfo[bidder].depositedETH <= maxBidFromMaxStake, "Bid is greater than max allowable bid");
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index != nextBidderToCheck) {
|
||||
emit BiddersChecked(msg.sender, nextBidderToCheck, index);
|
||||
nextBidderToCheck = index;
|
||||
}
|
||||
return nextBidderToCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Checks if claiming available
|
||||
*/
|
||||
function isClaimingAvailable() public view returns (bool) {
|
||||
return block.timestamp >= endCancellationDate &&
|
||||
nextBidderToCheck == bidders.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Claimed tokens will be deposited and locked as stake in the StakingEscrow contract.
|
||||
*/
|
||||
function claim() external returns (uint256 claimedTokens) {
|
||||
require(isClaimingAvailable(), "Claiming has not been enabled yet");
|
||||
WorkInfo storage info = workInfo[msg.sender];
|
||||
require(!info.claimed, "Tokens are already claimed");
|
||||
claimedTokens = ethToTokens(info.depositedETH);
|
||||
require(claimedTokens > 0, "Nothing to claim");
|
||||
|
||||
info.claimed = true;
|
||||
token.approve(address(escrow), claimedTokens);
|
||||
escrow.depositFromWorkLock(msg.sender, claimedTokens, stakingPeriods);
|
||||
info.completedWork = escrow.setWorkMeasurement(msg.sender, true);
|
||||
emit Claimed(msg.sender, claimedTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get available refund for bidder
|
||||
*/
|
||||
function getAvailableRefund(address _bidder) public view returns (uint256) {
|
||||
WorkInfo storage info = workInfo[_bidder];
|
||||
// nothing to refund
|
||||
if (info.depositedETH == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 currentWork = escrow.getCompletedWork(_bidder);
|
||||
uint256 completedWork = currentWork.sub(info.completedWork);
|
||||
// no work that has been completed since last refund
|
||||
if (completedWork == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 refundETH = workToETH(completedWork, info.depositedETH);
|
||||
if (refundETH > info.depositedETH) {
|
||||
refundETH = info.depositedETH;
|
||||
}
|
||||
return refundETH;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Refund ETH for the completed work
|
||||
*/
|
||||
function refund() external returns (uint256 refundETH) {
|
||||
WorkInfo storage info = workInfo[msg.sender];
|
||||
require(info.claimed, "Tokens must be claimed before refund");
|
||||
refundETH = getAvailableRefund(msg.sender);
|
||||
require(refundETH > 0, "Nothing to refund: there is no ETH to refund or no completed work");
|
||||
|
||||
if (refundETH == info.depositedETH) {
|
||||
escrow.setWorkMeasurement(msg.sender, false);
|
||||
}
|
||||
info.depositedETH = info.depositedETH.sub(refundETH);
|
||||
// convert refund back to work to eliminate potential rounding errors
|
||||
uint256 completedWork = ethToWork(refundETH, info.depositedETH);
|
||||
|
||||
info.completedWork = info.completedWork.add(completedWork);
|
||||
emit Refund(msg.sender, refundETH, completedWork);
|
||||
payable(msg.sender).sendValue(refundETH);
|
||||
}
|
||||
}
|
|
@ -106,15 +106,3 @@ STAKEHOLDER_BANNER = r"""
|
|||
|
||||
The Holder of Stakes.
|
||||
"""
|
||||
|
||||
|
||||
WORKLOCK_BANNER = r"""
|
||||
_ _ _ _ _
|
||||
| | | | | | | | | |
|
||||
| | | | ___ _ __ | | __| | ___ ___ | | __
|
||||
| |/\| | / _ \ | '__|| |/ /| | / _ \ / __|| |/ /
|
||||
\ /\ /| (_) || | | < | |____| (_) || (__ | <
|
||||
\/ \/ \___/ |_| |_|\_\\_____/ \___/ \___||_|\_\
|
||||
|
||||
══ {} ══
|
||||
"""
|
||||
|
|
|
@ -23,7 +23,6 @@ import click
|
|||
from nucypher.blockchain.eth.actors import Staker
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent
|
||||
from nucypher.blockchain.eth.constants import (
|
||||
POLICY_MANAGER_CONTRACT_NAME,
|
||||
STAKING_ESCROW_CONTRACT_NAME
|
||||
)
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
|
@ -179,7 +178,7 @@ def events(general_config, registry_options, contract_name, from_block, to_block
|
|||
if event_name:
|
||||
raise click.BadOptionUsage(option_name='--event-name', message='--event-name requires --contract-name')
|
||||
# FIXME should we force a contract name to be specified?
|
||||
contract_names = [STAKING_ESCROW_CONTRACT_NAME, POLICY_MANAGER_CONTRACT_NAME]
|
||||
contract_names = [STAKING_ESCROW_CONTRACT_NAME,]
|
||||
else:
|
||||
contract_names = [contract_name]
|
||||
|
||||
|
|
|
@ -1,414 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import maya
|
||||
import os
|
||||
import tabulate
|
||||
from decimal import Decimal
|
||||
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.blockchain.eth.actors import Bidder
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, WorkLockAgent
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.blockchain.eth.signers import Signer, ClefSigner
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.blockchain.eth.utils import prettify_eth_amount
|
||||
from nucypher.cli.actions.auth import get_client_password
|
||||
from nucypher.cli.actions.select import select_client_account
|
||||
from nucypher.cli.utils import connect_to_blockchain, get_registry, setup_emitter
|
||||
from nucypher.cli.config import group_general_config, GroupGeneralConfig
|
||||
from nucypher.cli.literature import (
|
||||
AVAILABLE_CLAIM_NOTICE,
|
||||
BID_AMOUNT_PROMPT_WITH_MIN_BID,
|
||||
BID_INCREASE_AMOUNT_PROMPT,
|
||||
BIDDERS_ALREADY_VERIFIED,
|
||||
BIDDING_WINDOW_CLOSED,
|
||||
BIDS_VALID_NO_FORCE_REFUND_INDICATED,
|
||||
CANCELLATION_WINDOW_CLOSED,
|
||||
CLAIM_ALREADY_PLACED,
|
||||
CLAIMING_NOT_AVAILABLE,
|
||||
COMPLETED_BID_VERIFICATION,
|
||||
CONFIRM_BID_VERIFICATION,
|
||||
CONFIRM_COLLECT_WORKLOCK_REFUND,
|
||||
CONFIRM_REQUEST_WORKLOCK_COMPENSATION,
|
||||
CONFIRM_WORKLOCK_CLAIM,
|
||||
EXISTING_BID_AMOUNT_NOTICE,
|
||||
PROMPT_BID_VERIFY_GAS_LIMIT,
|
||||
REQUESTING_WORKLOCK_COMPENSATION,
|
||||
SUBMITTING_WORKLOCK_CLAIM,
|
||||
SUBMITTING_WORKLOCK_REFUND_REQUEST,
|
||||
SUCCESSFUL_BID_CANCELLATION,
|
||||
VERIFICATION_ESTIMATES,
|
||||
WHALE_WARNING,
|
||||
WORKLOCK_ADDITIONAL_COMPENSATION_AVAILABLE,
|
||||
WORKLOCK_CLAIM_ADVISORY
|
||||
)
|
||||
from nucypher.cli.options import (
|
||||
group_options,
|
||||
option_force,
|
||||
option_hw_wallet,
|
||||
option_network,
|
||||
option_provider_uri,
|
||||
option_registry_filepath,
|
||||
option_signer_uri,
|
||||
option_participant_address)
|
||||
from nucypher.cli.painting.transactions import paint_receipt_summary
|
||||
from nucypher.cli.painting.worklock import (
|
||||
paint_bidder_status,
|
||||
paint_bidding_notice,
|
||||
paint_worklock_claim,
|
||||
paint_worklock_status
|
||||
)
|
||||
from nucypher.cli.types import DecimalRange, EIP55_CHECKSUM_ADDRESS
|
||||
from nucypher.config.constants import NUCYPHER_ENVVAR_PROVIDER_URI
|
||||
|
||||
|
||||
class WorkLockOptions:
|
||||
|
||||
__option_name__ = 'worklock_options'
|
||||
|
||||
def __init__(self,
|
||||
participant_address: ChecksumAddress,
|
||||
signer_uri: str,
|
||||
provider_uri: str,
|
||||
registry_filepath: Path,
|
||||
network: str):
|
||||
|
||||
self.bidder_address = participant_address
|
||||
self.signer_uri = signer_uri
|
||||
self.provider_uri = provider_uri
|
||||
self.registry_filepath = registry_filepath
|
||||
self.network = network
|
||||
|
||||
def setup(self, general_config) -> tuple:
|
||||
emitter = setup_emitter(general_config) # TODO: Restore Banner: network=self.network.capitalize()
|
||||
registry = get_registry(network=self.network, registry_filepath=self.registry_filepath)
|
||||
blockchain = connect_to_blockchain(emitter=emitter, provider_uri=self.provider_uri)
|
||||
return emitter, registry, blockchain
|
||||
|
||||
def get_bidder_address(self, emitter, registry):
|
||||
if not self.bidder_address:
|
||||
self.bidder_address = select_client_account(emitter=emitter,
|
||||
provider_uri=self.provider_uri,
|
||||
signer_uri=self.signer_uri,
|
||||
network=self.network,
|
||||
registry=registry,
|
||||
show_eth_balance=True)
|
||||
return self.bidder_address
|
||||
|
||||
def __create_bidder(self,
|
||||
registry,
|
||||
domain: str,
|
||||
transacting: bool = True,
|
||||
hw_wallet: bool = False,
|
||||
) -> Bidder:
|
||||
|
||||
is_clef = ClefSigner.is_valid_clef_uri(self.signer_uri)
|
||||
testnet = self.network != NetworksInventory.MAINNET
|
||||
signer = Signer.from_signer_uri(self.signer_uri, testnet=testnet) if self.signer_uri else None
|
||||
password_required = (not is_clef and not hw_wallet)
|
||||
if signer and transacting and password_required:
|
||||
client_password = get_client_password(checksum_address=self.bidder_address)
|
||||
signer.unlock_account(account=self.bidder_address, password=client_password)
|
||||
|
||||
transacting_power = None
|
||||
if transacting:
|
||||
transacting_power = TransactingPower(account=self.bidder_address, signer=signer)
|
||||
transacting_power.unlock(password=client_password)
|
||||
|
||||
bidder = Bidder(registry=registry,
|
||||
transacting_power=transacting_power,
|
||||
checksum_address=self.bidder_address if not transacting_power else None,
|
||||
domain=domain)
|
||||
return bidder
|
||||
|
||||
def create_bidder(self, registry, hw_wallet: bool = False):
|
||||
return self.__create_bidder(registry=registry,
|
||||
domain=self.network,
|
||||
hw_wallet=hw_wallet,
|
||||
transacting=True)
|
||||
|
||||
def create_transactionless_bidder(self, registry):
|
||||
return self.__create_bidder(registry, transacting=False, domain=self.network)
|
||||
|
||||
|
||||
group_worklock_options = group_options(
|
||||
WorkLockOptions,
|
||||
participant_address=option_participant_address,
|
||||
signer_uri=option_signer_uri,
|
||||
provider_uri=option_provider_uri(required=True, default=os.environ.get(NUCYPHER_ENVVAR_PROVIDER_URI)),
|
||||
network=option_network(default=NetworksInventory.DEFAULT, validate=True), # TODO: See 2214
|
||||
registry_filepath=option_registry_filepath,
|
||||
)
|
||||
|
||||
|
||||
@click.group()
|
||||
def worklock():
|
||||
"""Participate in NuCypher's WorkLock to obtain a NU stake"""
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@group_worklock_options
|
||||
@group_general_config
|
||||
def status(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions):
|
||||
"""Show current WorkLock information"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
paint_worklock_status(emitter=emitter, registry=registry)
|
||||
if worklock_options.bidder_address:
|
||||
bidder = worklock_options.create_transactionless_bidder(registry=registry)
|
||||
paint_bidder_status(emitter=emitter, bidder=bidder)
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@group_general_config
|
||||
@group_worklock_options
|
||||
@option_force
|
||||
@option_hw_wallet
|
||||
@click.option('--value', help="ETH value to escrow", type=DecimalRange(min=0))
|
||||
def escrow(general_config: GroupGeneralConfig,
|
||||
worklock_options: WorkLockOptions,
|
||||
force: bool,
|
||||
hw_wallet: bool,
|
||||
value: Decimal):
|
||||
"""Create an ETH escrow, or increase an existing escrow"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
|
||||
now = maya.now().epoch
|
||||
if not worklock_agent.start_bidding_date <= now <= worklock_agent.end_bidding_date:
|
||||
emitter.echo(BIDDING_WINDOW_CLOSED, color='red')
|
||||
raise click.Abort()
|
||||
|
||||
_bidder_address = worklock_options.get_bidder_address(emitter, registry)
|
||||
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
|
||||
|
||||
existing_bid_amount = bidder.get_deposited_eth
|
||||
if not value:
|
||||
if force:
|
||||
raise click.MissingParameter("Missing --value.")
|
||||
|
||||
if not existing_bid_amount: # It's the first bid
|
||||
minimum_bid = bidder.worklock_agent.minimum_allowed_bid
|
||||
minimum_bid_in_eth = Web3.fromWei(minimum_bid, 'ether')
|
||||
prompt = BID_AMOUNT_PROMPT_WITH_MIN_BID.format(minimum_bid_in_eth=minimum_bid_in_eth)
|
||||
else: # There's an existing bid and the bidder is increasing the amount
|
||||
emitter.message(EXISTING_BID_AMOUNT_NOTICE.format(eth_amount=Web3.fromWei(existing_bid_amount, 'ether')))
|
||||
minimum_bid_in_eth = Web3.fromWei(1, 'ether')
|
||||
prompt = BID_INCREASE_AMOUNT_PROMPT
|
||||
value = click.prompt(prompt, type=DecimalRange(min=minimum_bid_in_eth))
|
||||
|
||||
value = int(Web3.toWei(Decimal(value), 'ether'))
|
||||
|
||||
if not force:
|
||||
if not existing_bid_amount:
|
||||
paint_bidding_notice(emitter=emitter, bidder=bidder)
|
||||
click.confirm(f"Place WorkLock escrow of {prettify_eth_amount(value)}?", abort=True)
|
||||
else:
|
||||
click.confirm(f"Increase current escrow ({prettify_eth_amount(existing_bid_amount)}) "
|
||||
f"by {prettify_eth_amount(value)}?", abort=True)
|
||||
|
||||
receipt = bidder.place_bid(value=value)
|
||||
emitter.message("Publishing WorkLock Escrow...")
|
||||
|
||||
maximum = NU.from_units(bidder.economics.maximum_allowed_locked)
|
||||
available_claim = NU.from_units(bidder.available_claim)
|
||||
message = f'\nCurrent escrow: {prettify_eth_amount(bidder.get_deposited_eth)} | Allocation: {available_claim}\n'
|
||||
if available_claim > maximum:
|
||||
message += f"\nThis allocation is currently above the allowed max ({maximum}), " \
|
||||
f"so the escrow may be partially refunded.\n"
|
||||
message += f'Note that the available allocation value may fluctuate until the escrow period closes and ' \
|
||||
f'allocations are finalized.\n'
|
||||
emitter.echo(message, color='yellow')
|
||||
|
||||
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@group_general_config
|
||||
@group_worklock_options
|
||||
@option_force
|
||||
@option_hw_wallet
|
||||
def cancel_escrow(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions, force: bool, hw_wallet: bool):
|
||||
"""Cancel your escrow and receive your ETH back"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
|
||||
now = maya.now().epoch
|
||||
if not worklock_agent.start_bidding_date <= now <= worklock_agent.end_cancellation_date:
|
||||
emitter.echo(CANCELLATION_WINDOW_CLOSED, color='red')
|
||||
raise click.Abort()
|
||||
|
||||
bidder_address = worklock_options.get_bidder_address(emitter, registry)
|
||||
|
||||
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
|
||||
if not force:
|
||||
value = bidder.get_deposited_eth
|
||||
click.confirm(f"Confirm escrow cancellation of {prettify_eth_amount(value)} for {bidder_address}?", abort=True)
|
||||
receipt = bidder.cancel_bid()
|
||||
emitter.echo(SUCCESSFUL_BID_CANCELLATION, color='green')
|
||||
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
|
||||
return # Exit
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@option_force
|
||||
@option_hw_wallet
|
||||
@group_worklock_options
|
||||
@group_general_config
|
||||
def claim(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions, force: bool, hw_wallet: bool):
|
||||
"""Claim tokens for your escrow, and start staking them"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
|
||||
if not worklock_agent.is_claiming_available():
|
||||
emitter.echo(CLAIMING_NOT_AVAILABLE, color='red')
|
||||
raise click.Abort()
|
||||
|
||||
bidder_address = worklock_options.get_bidder_address(emitter, registry)
|
||||
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
|
||||
|
||||
unspent_bid = bidder.available_compensation
|
||||
if unspent_bid:
|
||||
emitter.echo(WORKLOCK_ADDITIONAL_COMPENSATION_AVAILABLE.format(amount=prettify_eth_amount(unspent_bid)))
|
||||
if not force:
|
||||
message = CONFIRM_REQUEST_WORKLOCK_COMPENSATION.format(bidder_address=bidder_address)
|
||||
click.confirm(message, abort=True)
|
||||
emitter.echo(REQUESTING_WORKLOCK_COMPENSATION)
|
||||
receipt = bidder.withdraw_compensation()
|
||||
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
|
||||
|
||||
has_claimed = bidder.has_claimed
|
||||
if bool(has_claimed):
|
||||
emitter.echo(CLAIM_ALREADY_PLACED.format(bidder_address=bidder.checksum_address), color='red')
|
||||
raise click.Abort()
|
||||
|
||||
tokens = NU.from_units(bidder.available_claim)
|
||||
emitter.echo(AVAILABLE_CLAIM_NOTICE.format(tokens=tokens), color='green', bold=True)
|
||||
if not force:
|
||||
lock_duration = bidder.worklock_agent.worklock_parameters()[-2]
|
||||
emitter.echo(WORKLOCK_CLAIM_ADVISORY.format(lock_duration=lock_duration), color='blue')
|
||||
click.confirm(CONFIRM_WORKLOCK_CLAIM.format(bidder_address=bidder_address), abort=True)
|
||||
emitter.echo(SUBMITTING_WORKLOCK_CLAIM)
|
||||
|
||||
receipt = bidder.claim()
|
||||
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
|
||||
paint_worklock_claim(emitter=emitter,
|
||||
bidder_address=bidder_address,
|
||||
network=worklock_options.network,
|
||||
provider_uri=worklock_options.provider_uri)
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@group_worklock_options
|
||||
@group_general_config
|
||||
def remaining_work(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions):
|
||||
"""Check how much work is pending until you can get all your escrowed ETH back"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
bidder_address = worklock_options.get_bidder_address(emitter, registry)
|
||||
bidder = worklock_options.create_transactionless_bidder(registry=registry)
|
||||
emitter.echo(f"Work Remaining for {bidder_address}: {bidder.remaining_work}")
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@option_force
|
||||
@option_hw_wallet
|
||||
@group_worklock_options
|
||||
@group_general_config
|
||||
def refund(general_config: GroupGeneralConfig, worklock_options: WorkLockOptions, force: bool, hw_wallet: bool):
|
||||
"""Reclaim ETH unlocked by your work"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
bidder_address = worklock_options.get_bidder_address(emitter, registry)
|
||||
|
||||
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
|
||||
|
||||
if not force:
|
||||
click.confirm(CONFIRM_COLLECT_WORKLOCK_REFUND.format(bidder_address=bidder_address), abort=True)
|
||||
emitter.echo(SUBMITTING_WORKLOCK_REFUND_REQUEST)
|
||||
|
||||
receipt = bidder.refund_deposit()
|
||||
paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=bidder.staking_agent.blockchain.client.chain_name)
|
||||
|
||||
|
||||
@worklock.command()
|
||||
@group_general_config
|
||||
@group_worklock_options
|
||||
@option_force
|
||||
@option_hw_wallet
|
||||
@click.option('--gas-limit', help="Gas limit per each verification transaction", type=click.IntRange(min=60000))
|
||||
# TODO: Consider moving to administrator (nucypher-deploy) #1758
|
||||
def enable_claiming(general_config: GroupGeneralConfig,
|
||||
worklock_options: WorkLockOptions,
|
||||
force: bool,
|
||||
hw_wallet: bool,
|
||||
gas_limit: int):
|
||||
"""Ensure correctness of WorkLock participants and enable allocation"""
|
||||
emitter, registry, blockchain = worklock_options.setup(general_config=general_config)
|
||||
bidder_address = worklock_options.get_bidder_address(emitter, registry)
|
||||
bidder = worklock_options.create_bidder(registry=registry, hw_wallet=hw_wallet)
|
||||
|
||||
whales = bidder.get_whales()
|
||||
if whales:
|
||||
headers = ("Participants that require correction", "Current bonus")
|
||||
columns = (whales.keys(), map(prettify_eth_amount, whales.values()))
|
||||
emitter.echo(tabulate.tabulate(dict(zip(headers, columns)), headers=headers, floatfmt="fancy_grid"))
|
||||
|
||||
if not force:
|
||||
click.confirm(f"Confirm force refund to at least {len(whales)} participants using {bidder_address}?", abort=True)
|
||||
|
||||
force_refund_receipt = bidder.force_refund()
|
||||
emitter.echo(WHALE_WARNING.format(number=len(whales)), color='green')
|
||||
|
||||
paint_receipt_summary(receipt=force_refund_receipt,
|
||||
emitter=emitter,
|
||||
chain_name=bidder.staking_agent.blockchain.client.chain_name,
|
||||
transaction_type=f"force-refund")
|
||||
else:
|
||||
emitter.echo(BIDS_VALID_NO_FORCE_REFUND_INDICATED, color='yellow')
|
||||
|
||||
if not bidder.worklock_agent.bidders_checked():
|
||||
|
||||
confirmation = gas_limit and force
|
||||
while not confirmation:
|
||||
if not gas_limit:
|
||||
min_gas = 180000
|
||||
gas_limit = click.prompt(PROMPT_BID_VERIFY_GAS_LIMIT.format(min_gas=min_gas), type=click.IntRange(min=min_gas))
|
||||
|
||||
bidders_per_transaction = bidder.worklock_agent.estimate_verifying_correctness(gas_limit=gas_limit)
|
||||
|
||||
if not force:
|
||||
message = CONFIRM_BID_VERIFICATION.format(bidder_address=bidder_address,
|
||||
gas_limit=gas_limit,
|
||||
bidders_per_transaction=bidders_per_transaction)
|
||||
confirmation = click.confirm(message)
|
||||
gas_limit = gas_limit if confirmation else None
|
||||
else:
|
||||
emitter.echo(VERIFICATION_ESTIMATES.format(gas_limit=gas_limit,
|
||||
bidders_per_transaction=bidders_per_transaction))
|
||||
confirmation = True
|
||||
|
||||
verification_receipts = bidder.verify_bidding_correctness(gas_limit=gas_limit)
|
||||
emitter.echo(COMPLETED_BID_VERIFICATION, color='green')
|
||||
|
||||
for iteration, receipt in verification_receipts.items():
|
||||
paint_receipt_summary(receipt=receipt,
|
||||
emitter=emitter,
|
||||
chain_name=bidder.staking_agent.blockchain.client.chain_name,
|
||||
transaction_type=f"verify-correctness[{iteration}]")
|
||||
else:
|
||||
emitter.echo(BIDDERS_ALREADY_VERIFIED, color='yellow')
|
|
@ -569,116 +569,6 @@ Compiled with solc version {solc_version}
|
|||
|
||||
'''
|
||||
|
||||
#
|
||||
# Worklock
|
||||
#
|
||||
|
||||
BID_AMOUNT_PROMPT_WITH_MIN_BID = "Enter escrow amount in ETH (at least {minimum_bid_in_eth} ETH)"
|
||||
|
||||
BID_INCREASE_AMOUNT_PROMPT = "Enter the amount in ETH that you want to increase your escrow"
|
||||
|
||||
EXISTING_BID_AMOUNT_NOTICE = "⚠️ You have an existing escrow of {eth_amount} ETH"
|
||||
|
||||
WORKLOCK_AGREEMENT = """
|
||||
⚠️ WorkLock Participant Notice ⚠️
|
||||
---------------------------------
|
||||
|
||||
- By participating in NuCypher's WorkLock you are committing to operating a staking
|
||||
NuCypher node once the allocation period opens.
|
||||
|
||||
- WorkLock allocation is provided in the form of a stake and will be locked for
|
||||
the stake duration ({duration} periods). During this time,
|
||||
you are obligated to maintain a networked and available
|
||||
NuCypher node bonded to the staker address {bidder_address}.
|
||||
|
||||
- ETH in escrow will be available for refund at a rate of {refund_rate}
|
||||
per confirmed period. This rate may vary until {end_date}.
|
||||
|
||||
- You agree to allow NuCypher network users to carry out uninterrupted work orders
|
||||
at will without interference. Failure to keep your node online or fulfill re-encryption
|
||||
work orders will result in loss of staked NU as described in the NuCypher slashing protocol:
|
||||
https://docs.nucypher.com/en/latest/architecture/slashing.html
|
||||
|
||||
- Correctly servicing work orders will result in rewards paid out in ethers retro-actively
|
||||
and on-demand.
|
||||
|
||||
Accept WorkLock terms and node operator obligation?""" # TODO: Show a special message for first bidder, since there's no refund rate yet?
|
||||
|
||||
BIDDING_WINDOW_CLOSED = "❌ You can't escrow, the escrow period is closed."
|
||||
|
||||
CANCELLATION_WINDOW_CLOSED = "❌ You can't cancel your escrow, the cancellation period is closed."
|
||||
|
||||
SUCCESSFUL_BID_CANCELLATION = "✅ Escrow canceled\n"
|
||||
|
||||
WORKLOCK_ADDITIONAL_COMPENSATION_AVAILABLE = """
|
||||
⚠️ Note that WorkLock did not use your entire escrow due to a maximum allocation limit.
|
||||
Therefore, an unspent amount of {amount} is available for refund.
|
||||
"""
|
||||
|
||||
CONFIRM_REQUEST_WORKLOCK_COMPENSATION = """
|
||||
Before requesting the NU allocation for {bidder_address},
|
||||
you will need to be refunded your unspent escrow amount.
|
||||
|
||||
Proceed with request?
|
||||
"""
|
||||
|
||||
REQUESTING_WORKLOCK_COMPENSATION = "Requesting refund of unspent escrow amount..."
|
||||
|
||||
CLAIMING_NOT_AVAILABLE = "❌ You can't request a NU allocation yet, allocations are not currently available."
|
||||
|
||||
CLAIM_ALREADY_PLACED = "⚠️ An allocation was already assigned to {bidder_address}"
|
||||
|
||||
AVAILABLE_CLAIM_NOTICE = "\nYou have an available allocation of {tokens} 🎉 \n"
|
||||
|
||||
WORKLOCK_CLAIM_ADVISORY = """
|
||||
⚠️ Note: Allocating WorkLock NU will initialize a new stake to be locked for {lock_duration} periods.
|
||||
"""
|
||||
|
||||
CONFIRM_WORKLOCK_CLAIM = "Continue WorkLock allocation for participant {bidder_address}?"
|
||||
|
||||
SUBMITTING_WORKLOCK_CLAIM = "Submitting allocation request..."
|
||||
|
||||
CONFIRM_COLLECT_WORKLOCK_REFUND = "Collect ETH refund for participant {bidder_address}?"
|
||||
|
||||
SUBMITTING_WORKLOCK_REFUND_REQUEST = "Submitting WorkLock refund request..."
|
||||
|
||||
PROMPT_BID_VERIFY_GAS_LIMIT = "Enter gas limit per each verification transaction (at least {min_gas})"
|
||||
|
||||
COMPLETED_BID_VERIFICATION = "Escrow amounts have been checked\n"
|
||||
|
||||
BIDS_VALID_NO_FORCE_REFUND_INDICATED = "All escrows are correct, force refund is not needed\n"
|
||||
|
||||
CONFIRM_BID_VERIFICATION = """
|
||||
Confirm verification of escrow from {bidder_address} using {gas_limit} gas
|
||||
for {bidders_per_transaction} participants per each transaction?
|
||||
"""
|
||||
|
||||
VERIFICATION_ESTIMATES = "Using {gas_limit} gas for {bidders_per_transaction} participants per each transaction\n"
|
||||
|
||||
WHALE_WARNING = "At least {number} participants got a force refund\n"
|
||||
|
||||
BIDDERS_ALREADY_VERIFIED = "All escrow amounts have already been checked\n"
|
||||
|
||||
SUCCESSFUL_WORKLOCK_CLAIM = """
|
||||
|
||||
✅ Successfully allocated WorkLock NU for {bidder_address}.
|
||||
|
||||
You can check that the stake was created correctly by running:
|
||||
|
||||
nucypher status stakers --staking-address {bidder_address} --network {network} --provider {provider_uri}
|
||||
|
||||
Next Steps for WorkLock Participants
|
||||
====================================
|
||||
|
||||
Congratulations! 🎉 You're officially a Staker in the NuCypher network.
|
||||
|
||||
See the official NuCypher documentation for a comprehensive guide on next steps!
|
||||
|
||||
As a first step, you need to bond a worker to your stake by running:
|
||||
|
||||
nucypher stake bond-worker --worker-address <WORKER ADDRESS>
|
||||
|
||||
"""
|
||||
|
||||
#
|
||||
# Ursula
|
||||
|
|
|
@ -24,7 +24,6 @@ from nucypher.cli.commands import (
|
|||
stake,
|
||||
status,
|
||||
ursula,
|
||||
worklock,
|
||||
cloudworkers,
|
||||
contacts,
|
||||
porter
|
||||
|
@ -75,7 +74,6 @@ ENTRY_POINTS = (
|
|||
enrico.enrico, # Encryptor of Data
|
||||
ursula.ursula, # Untrusted Re-Encryption Proxy
|
||||
stake.stake, # Stake Management
|
||||
worklock.worklock, # WorkLock
|
||||
|
||||
# Utility Commands
|
||||
status.status, # Network Status
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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 maya
|
||||
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, WorkLockAgent
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.blockchain.eth.utils import prettify_eth_amount
|
||||
from nucypher.cli.literature import SUCCESSFUL_WORKLOCK_CLAIM, WORKLOCK_AGREEMENT
|
||||
|
||||
|
||||
def paint_bidding_notice(emitter, bidder):
|
||||
message = WORKLOCK_AGREEMENT.format(refund_rate=prettify_eth_amount(bidder.worklock_agent.get_bonus_refund_rate()),
|
||||
end_date=maya.MayaDT(bidder.economics.bidding_end_date).local_datetime(),
|
||||
bidder_address=bidder.checksum_address,
|
||||
duration=bidder.economics.worklock_commitment_duration)
|
||||
emitter.echo(message)
|
||||
return
|
||||
|
||||
|
||||
def paint_worklock_claim(emitter, bidder_address: str, network: str, provider_uri: str):
|
||||
message = SUCCESSFUL_WORKLOCK_CLAIM.format(bidder_address=bidder_address,
|
||||
network=network,
|
||||
provider_uri=provider_uri)
|
||||
emitter.echo(message, color='green')
|
||||
|
||||
|
||||
def paint_worklock_status(emitter, registry: BaseContractRegistry):
|
||||
from maya import MayaDT
|
||||
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=registry) # type: WorkLockAgent
|
||||
blockchain = worklock_agent.blockchain
|
||||
|
||||
# Time
|
||||
bidding_start = MayaDT(worklock_agent.start_bidding_date)
|
||||
bidding_end = MayaDT(worklock_agent.end_bidding_date)
|
||||
cancellation_end = MayaDT(worklock_agent.end_cancellation_date)
|
||||
|
||||
bidding_duration = bidding_end - bidding_start
|
||||
cancellation_duration = cancellation_end - bidding_start
|
||||
|
||||
now = maya.now()
|
||||
bidding_remaining = bidding_end - now if bidding_end > now else 'Closed'
|
||||
cancellation_remaining = cancellation_end - now if cancellation_end > now else "Closed"
|
||||
|
||||
cancellation_open = bidding_start <= now <= cancellation_end
|
||||
bidding_open = bidding_start <= now <= bidding_end
|
||||
|
||||
# Refund
|
||||
refund_multiple = worklock_agent.boosting_refund / worklock_agent.slowing_refund
|
||||
|
||||
emitter.echo(f"\nWorkLock ({worklock_agent.contract_address})", bold=True, color='green')
|
||||
|
||||
payload = f"""
|
||||
Time
|
||||
══════════════════════════════════════════════════════
|
||||
|
||||
Escrow Period ({'Open' if bidding_open else 'Closed'})
|
||||
------------------------------------------------------
|
||||
Allocations Available . {'Yes' if worklock_agent.is_claiming_available() else 'No'}
|
||||
Start Date ............ {bidding_start}
|
||||
End Date .............. {bidding_end}
|
||||
Duration .............. {bidding_duration}
|
||||
Time Remaining ........ {bidding_remaining}
|
||||
|
||||
Cancellation Period ({'Open' if cancellation_open else 'Closed'})
|
||||
------------------------------------------------------
|
||||
End Date .............. {cancellation_end}
|
||||
Duration .............. {cancellation_duration}
|
||||
Time Remaining ........ {cancellation_remaining}
|
||||
|
||||
|
||||
Economics
|
||||
══════════════════════════════════════════════════════
|
||||
|
||||
Participation
|
||||
------------------------------------------------------
|
||||
Lot Size .............. {NU.from_units(worklock_agent.lot_value)}
|
||||
Min. Allowed Escrow ... {prettify_eth_amount(worklock_agent.minimum_allowed_bid)}
|
||||
Participants .......... {worklock_agent.get_bidders_population()}
|
||||
ETH Supply ............ {prettify_eth_amount(worklock_agent.get_eth_supply())}
|
||||
ETH Pool .............. {prettify_eth_amount(blockchain.client.get_balance(worklock_agent.contract_address))}
|
||||
|
||||
Base (minimum escrow)
|
||||
------------------------------------------------------
|
||||
Base Deposit Rate ..... {worklock_agent.get_base_deposit_rate()} NU per base ETH
|
||||
|
||||
Bonus (surplus over minimum escrow)
|
||||
------------------------------------------------------
|
||||
Bonus ETH Supply ...... {prettify_eth_amount(worklock_agent.get_bonus_eth_supply())}
|
||||
Bonus Lot Size ........ {NU.from_units(worklock_agent.get_bonus_lot_value())}
|
||||
Bonus Deposit Rate .... {worklock_agent.get_bonus_deposit_rate()} NU per bonus ETH
|
||||
|
||||
Refunds
|
||||
------------------------------------------------------
|
||||
Refund Rate Multiple .. {refund_multiple:.2f}
|
||||
Bonus Refund Rate ..... {worklock_agent.get_bonus_refund_rate()} units of work to unlock 1 bonus ETH
|
||||
Base Refund Rate ...... {worklock_agent.get_base_refund_rate()} units of work to unlock 1 base ETH
|
||||
|
||||
* NOTE: bonus ETH is refunded before base ETH
|
||||
"""
|
||||
emitter.echo(payload)
|
||||
|
||||
|
||||
def paint_bidder_status(emitter, bidder):
|
||||
claim = NU.from_units(bidder.available_claim)
|
||||
if claim > bidder.economics.maximum_allowed_locked:
|
||||
claim = f"{claim} (Above the allowed max. The escrow will be partially refunded)"
|
||||
|
||||
deposited_eth = bidder.get_deposited_eth
|
||||
bonus_eth = deposited_eth - bidder.economics.worklock_min_allowed_bid
|
||||
|
||||
message = f"""
|
||||
WorkLock Participant {bidder.checksum_address}
|
||||
══════════════════════════════════════════════════════"""
|
||||
|
||||
if bidder.has_claimed:
|
||||
message += f"""
|
||||
NU Claimed? ............ Yes
|
||||
Locked ETH ............. {prettify_eth_amount(bidder.get_deposited_eth)}"""
|
||||
else:
|
||||
message += f"""
|
||||
NU Claimed? ............ No
|
||||
Total Escrow ........... {prettify_eth_amount(deposited_eth)}
|
||||
Base ETH ........... {prettify_eth_amount(bidder.economics.worklock_min_allowed_bid)}
|
||||
Bonus ETH .......... {prettify_eth_amount(bonus_eth)}
|
||||
NU Allocated ........... {claim}"""
|
||||
|
||||
compensation = bidder.available_compensation
|
||||
if compensation:
|
||||
message += f"""
|
||||
Unspent Escrow Amount .. {prettify_eth_amount(compensation)}"""
|
||||
|
||||
message += f"""\n
|
||||
Completed Work ......... {bidder.completed_work}
|
||||
Available Refund ....... {prettify_eth_amount(bidder.available_refund)}
|
||||
|
||||
Refunded Work .......... {bidder.refunded_work}
|
||||
Remaining Work ......... {bidder.remaining_work}
|
||||
"""
|
||||
|
||||
emitter.echo(message)
|
|
@ -32,16 +32,6 @@ PeriodDelta = NewType('PeriodDelta', int)
|
|||
ContractReturnValue = TypeVar('ContractReturnValue', bound=Union[TxReceipt, Wei, int, str, bool])
|
||||
|
||||
|
||||
class WorklockParameters(Tuple):
|
||||
token_supply: NuNits
|
||||
start_bid_date: Timestamp
|
||||
end_bid_date: Timestamp
|
||||
end_cancellation_date: Timestamp
|
||||
boosting_refund: int
|
||||
staking_periods: int
|
||||
min_allowed_bid: Wei
|
||||
|
||||
|
||||
class StakingEscrowParameters(Tuple):
|
||||
seconds_per_period: int
|
||||
minting_coefficient: int
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from nucypher.blockchain.eth.events import ContractEventsThrottler
|
||||
from nucypher.blockchain.eth.utils import estimate_block_number_for_period
|
||||
|
||||
try:
|
||||
from prometheus_client import Gauge, Enum, Counter, Info, Histogram, Summary
|
||||
|
@ -28,7 +27,7 @@ from eth_typing.evm import ChecksumAddress
|
|||
|
||||
import nucypher
|
||||
from nucypher.blockchain.eth.actors import NucypherTokenActor
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, PolicyManagerAgent, StakingEscrowAgent, WorkLockAgent, \
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent, \
|
||||
PREApplicationAgent
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
||||
|
@ -36,7 +35,7 @@ from nucypher.datastore.queries import get_reencryption_requests
|
|||
|
||||
from typing import Dict, Union, Type
|
||||
|
||||
ContractAgents = Union[StakingEscrowAgent, WorkLockAgent, PolicyManagerAgent]
|
||||
ContractAgents = Union[StakingEscrowAgent]
|
||||
|
||||
|
||||
class MetricsCollector(ABC):
|
||||
|
@ -241,43 +240,6 @@ class OperatorMetricsCollector(BaseMetricsCollector):
|
|||
self.metrics["worker_token_balance_gauge"].set(int(nucypher_worker_token_actor.token_balance))
|
||||
|
||||
|
||||
class WorkLockMetricsCollector(BaseMetricsCollector):
|
||||
"""Collector for WorkLock specific metrics."""
|
||||
def __init__(self, staker_address: ChecksumAddress, contract_registry: BaseContractRegistry):
|
||||
super().__init__()
|
||||
self.staker_address = staker_address
|
||||
self.contract_registry = contract_registry
|
||||
|
||||
def initialize(self, metrics_prefix: str, registry: CollectorRegistry) -> None:
|
||||
self.metrics = {
|
||||
"available_refund_gauge": Gauge(f'{metrics_prefix}_available_refund',
|
||||
'Available refund',
|
||||
registry=registry),
|
||||
"worklock_remaining_work_gauge": Gauge(f'{metrics_prefix}_worklock_refund_remaining_work',
|
||||
'Worklock remaining work',
|
||||
registry=registry),
|
||||
"worklock_refund_completed_work_gauge": Gauge(f'{metrics_prefix}_worklock_refund_completedWork',
|
||||
'Worklock completed work',
|
||||
registry=registry),
|
||||
}
|
||||
|
||||
def _collect_internal(self) -> None:
|
||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=self.contract_registry)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=self.contract_registry)
|
||||
|
||||
self.metrics["available_refund_gauge"].set(
|
||||
worklock_agent.get_available_refund(checksum_address=self.staker_address))
|
||||
|
||||
self.metrics["worklock_remaining_work_gauge"].set(
|
||||
worklock_agent.get_remaining_work(checksum_address=self.staker_address)
|
||||
)
|
||||
|
||||
self.metrics["worklock_refund_completed_work_gauge"].set(
|
||||
staking_agent.get_completed_work(bidder_address=self.staker_address) -
|
||||
worklock_agent.get_refunded_work(checksum_address=self.staker_address)
|
||||
)
|
||||
|
||||
|
||||
class EventMetricsCollector(BaseMetricsCollector):
|
||||
"""General collector for emitted events."""
|
||||
def __init__(self,
|
||||
|
@ -401,22 +363,3 @@ class OperatorBondedEventMetricsCollector(EventMetricsCollector):
|
|||
contract_agent = ContractAgency.get_agent(self.contract_agent_class, registry=self.contract_registry)
|
||||
self.metrics["current_worker_is_me_gauge"].set(
|
||||
contract_agent.get_worker_from_staker(self.staker_address) == self.operator_address)
|
||||
|
||||
|
||||
class WorkLockRefundEventMetricsCollector(EventMetricsCollector):
|
||||
"""Collector for WorkLock Refund event."""
|
||||
|
||||
def __init__(self, staker_address: ChecksumAddress, event_name: str = 'Refund', *args, **kwargs):
|
||||
super().__init__(event_name=event_name, argument_filters={'sender': staker_address}, *args, **kwargs)
|
||||
self.staker_address = staker_address
|
||||
|
||||
def initialize(self, metrics_prefix: str, registry: CollectorRegistry) -> None:
|
||||
super().initialize(metrics_prefix=metrics_prefix, registry=registry)
|
||||
self.metrics["worklock_deposited_eth_gauge"] = Gauge(f'{metrics_prefix}_worklock_current_deposited_eth',
|
||||
'Worklock deposited ETH',
|
||||
registry=registry)
|
||||
|
||||
def _event_occurred(self, event) -> None:
|
||||
super()._event_occurred(event)
|
||||
contract_agent = ContractAgency.get_agent(self.contract_agent_class, registry=self.contract_registry)
|
||||
self.metrics["worklock_deposited_eth_gauge"].set(contract_agent.get_deposited_eth(self.staker_address))
|
||||
|
|
|
@ -37,20 +37,19 @@ from nucypher.utilities.prometheus.collector import (
|
|||
BlockchainMetricsCollector,
|
||||
StakerMetricsCollector,
|
||||
OperatorMetricsCollector,
|
||||
WorkLockMetricsCollector,
|
||||
EventMetricsCollector,
|
||||
ReStakeEventMetricsCollector,
|
||||
WindDownEventMetricsCollector,
|
||||
OperatorBondedEventMetricsCollector,
|
||||
CommitmentMadeEventMetricsCollector,
|
||||
WorkLockRefundEventMetricsCollector)
|
||||
CommitmentMadeEventMetricsCollector
|
||||
)
|
||||
|
||||
from typing import List
|
||||
|
||||
from twisted.internet import reactor, task
|
||||
from twisted.web.resource import Resource
|
||||
|
||||
from nucypher.blockchain.eth.agents import StakingEscrowAgent, PolicyManagerAgent, WorkLockAgent
|
||||
from nucypher.blockchain.eth.agents import StakingEscrowAgent
|
||||
|
||||
|
||||
class PrometheusMetricsConfig:
|
||||
|
@ -205,24 +204,6 @@ def create_metrics_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[Met
|
|||
metrics_prefix=metrics_prefix)
|
||||
collectors.extend(staking_events_collectors)
|
||||
|
||||
# Policy Events
|
||||
policy_events_collectors = create_policy_events_metric_collectors(ursula=ursula,
|
||||
metrics_prefix=metrics_prefix)
|
||||
collectors.extend(policy_events_collectors)
|
||||
|
||||
#
|
||||
# WorkLock information - only collected for mainnet
|
||||
#
|
||||
if ursula.domain == NetworksInventory.MAINNET:
|
||||
# WorkLock metrics
|
||||
collectors.append(WorkLockMetricsCollector(staker_address=ursula.checksum_address,
|
||||
contract_registry=ursula.registry))
|
||||
|
||||
# WorkLock Events
|
||||
worklock_events_collectors = create_worklock_events_metric_collectors(ursula=ursula,
|
||||
metrics_prefix=metrics_prefix)
|
||||
collectors.extend(worklock_events_collectors)
|
||||
|
||||
return collectors
|
||||
|
||||
|
||||
|
@ -305,35 +286,3 @@ def create_staking_events_metric_collectors(ursula: 'Ursula', metrics_prefix: st
|
|||
))
|
||||
|
||||
return collectors
|
||||
|
||||
|
||||
def create_worklock_events_metric_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[MetricsCollector]:
|
||||
"""Create collectors for worklock-related events."""
|
||||
# Refund
|
||||
collectors: List[MetricsCollector] = [WorkLockRefundEventMetricsCollector(
|
||||
event_args_config={
|
||||
"refundETH": (Gauge, f'{metrics_prefix}_worklock_refund_refundETH',
|
||||
'Refunded ETH'),
|
||||
},
|
||||
staker_address=ursula.checksum_address,
|
||||
contract_agent_class=WorkLockAgent,
|
||||
contract_registry=ursula.registry
|
||||
)]
|
||||
|
||||
return collectors
|
||||
|
||||
|
||||
def create_policy_events_metric_collectors(ursula: 'Ursula', metrics_prefix: str) -> List[MetricsCollector]:
|
||||
"""Create collectors for policy-related events."""
|
||||
# Withdrawn
|
||||
collectors: List[MetricsCollector] = [EventMetricsCollector(
|
||||
event_name='Withdrawn',
|
||||
event_args_config={
|
||||
"value": (Gauge, f'{metrics_prefix}_policy_withdrawn_reward', 'Policy reward')
|
||||
},
|
||||
argument_filters={"recipient": ursula.checksum_address},
|
||||
contract_agent_class=PolicyManagerAgent,
|
||||
contract_registry=ursula.registry
|
||||
)]
|
||||
|
||||
return collectors
|
||||
|
|
|
@ -30,18 +30,10 @@ CONTRACTS = {
|
|||
'token': ['NuCypherToken',
|
||||
'TokenRecipient'],
|
||||
'main': ['StakingEscrow',
|
||||
'PolicyManager',
|
||||
'Adjudicator',
|
||||
'WorkLock'],
|
||||
'SimplePREApplication', # TODO change to PREApplication when ready
|
||||
'Adjudicator'],
|
||||
'proxy': ['Dispatcher',
|
||||
'Upgradeable'],
|
||||
'staking': ['StakingInterface',
|
||||
'StakingInterfaceRouter',
|
||||
'AbstractStakingContract',
|
||||
'InitializableStakingContract',
|
||||
'PoolingStakingContract',
|
||||
'PoolingStakingContractV2',
|
||||
'WorkLockPoolingContract']
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,226 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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 random
|
||||
|
||||
import pytest
|
||||
from eth_tester.exceptions import TransactionFailed
|
||||
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.actors import Bidder
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent, WorkLockAgent
|
||||
from nucypher.blockchain.eth.constants import NULL_ADDRESS
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_bidder(testerchain, test_registry, agency, application_economics):
|
||||
bidder_address = testerchain.unassigned_accounts[0]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(domain=TEMPORARY_DOMAIN,
|
||||
registry=test_registry,
|
||||
transacting_power=tpower)
|
||||
assert bidder.checksum_address == bidder_address
|
||||
assert bidder.registry == test_registry
|
||||
|
||||
assert not bidder.get_deposited_eth
|
||||
assert not bidder.completed_work
|
||||
assert not bidder.remaining_work
|
||||
assert not bidder.refunded_work
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_bidding(testerchain, agency, application_economics, test_registry):
|
||||
min_allowed_bid = application_economics.worklock_min_allowed_bid
|
||||
max_bid = 2000 * min_allowed_bid
|
||||
small_bids = [random.randrange(min_allowed_bid, 2 * min_allowed_bid) for _ in range(10)]
|
||||
total_small_bids = sum(small_bids)
|
||||
min_potential_whale_bid = (max_bid - total_small_bids) // 9
|
||||
whales_bids = [random.randrange(min_potential_whale_bid, max_bid) for _ in range(9)]
|
||||
initial_bids = small_bids + whales_bids
|
||||
|
||||
for i, bid in enumerate(initial_bids):
|
||||
bidder_address = testerchain.client.accounts[i]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
domain=TEMPORARY_DOMAIN,
|
||||
transacting_power=tpower)
|
||||
|
||||
assert bidder.get_deposited_eth == 0
|
||||
receipt = bidder.place_bid(value=bid)
|
||||
assert receipt['status'] == 1
|
||||
assert bidder.get_deposited_eth == bid
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_cancel_bid(testerchain, agency, application_economics, test_registry):
|
||||
# Wait until the bidding window closes...
|
||||
testerchain.time_travel(seconds=application_economics.bidding_duration + 1)
|
||||
|
||||
bidder_address = testerchain.client.accounts[1]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
assert bidder.get_deposited_eth # Bid
|
||||
receipt = bidder.cancel_bid() # Cancel
|
||||
assert receipt['status'] == 1
|
||||
assert not bidder.get_deposited_eth # No more bid
|
||||
|
||||
# Can't cancel a bid twice in a row
|
||||
with pytest.raises((TransactionFailed, ValueError)):
|
||||
_receipt = bidder.cancel_bid()
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_get_remaining_work(testerchain, agency, application_economics, test_registry):
|
||||
bidder_address = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
remaining = bidder.remaining_work
|
||||
assert remaining
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_verify_correctness_before_refund(testerchain, agency, application_economics, test_registry):
|
||||
bidder_address = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
with pytest.raises(Bidder.CancellationWindowIsOpen):
|
||||
_receipt = bidder.claim()
|
||||
|
||||
# Wait until the cancellation window closes...
|
||||
testerchain.time_travel(seconds=application_economics.cancellation_window_duration + 1)
|
||||
|
||||
with pytest.raises(Bidder.BidderError):
|
||||
_receipt = bidder.verify_bidding_correctness(gas_limit=100000)
|
||||
assert not worklock_agent.bidders_checked()
|
||||
assert bidder.get_whales()
|
||||
assert not worklock_agent.is_claiming_available()
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_force_refund(testerchain, agency, application_economics, test_registry):
|
||||
bidder_address = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
whales = bidder.get_whales()
|
||||
|
||||
# Simulate force refund
|
||||
new_whales = whales.copy()
|
||||
while new_whales:
|
||||
whales.update(new_whales)
|
||||
whales = bidder._reduce_bids(whales)
|
||||
new_whales = bidder.get_whales()
|
||||
|
||||
bidder_address = testerchain.client.accounts[1]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
receipt = bidder.force_refund()
|
||||
assert receipt['status'] == 1
|
||||
assert not bidder.get_whales()
|
||||
assert not worklock_agent.bidders_checked()
|
||||
|
||||
# Compare off-chain and on-chain calculations
|
||||
min_bid = application_economics.worklock_min_allowed_bid
|
||||
for whale, bonus in whales.items():
|
||||
contract_bid = worklock_agent.get_deposited_eth(whale)
|
||||
assert bonus == contract_bid - min_bid
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_verify_correctness(testerchain, agency, application_economics, test_registry):
|
||||
bidder_address = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
assert not worklock_agent.bidders_checked()
|
||||
with pytest.raises(Bidder.ClaimError):
|
||||
_receipt = bidder.claim()
|
||||
|
||||
receipts = bidder.verify_bidding_correctness(gas_limit=100000)
|
||||
assert worklock_agent.bidders_checked()
|
||||
assert worklock_agent.is_claiming_available()
|
||||
for iteration, receipt in receipts.items():
|
||||
assert receipt['status'] == 1
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_withdraw_compensation(testerchain, agency, application_economics, test_registry):
|
||||
bidder_address = testerchain.client.accounts[12]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
assert worklock_agent.get_available_compensation(checksum_address=bidder_address) > 0
|
||||
receipt = bidder.withdraw_compensation()
|
||||
assert receipt['status'] == 1
|
||||
assert worklock_agent.get_available_compensation(checksum_address=bidder_address) == 0
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_claim(testerchain, agency, application_economics, test_registry):
|
||||
bidder_address = testerchain.client.accounts[11]
|
||||
tpower = TransactingPower(account=bidder_address, signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
# Ensure that the bidder is not staking.
|
||||
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder.checksum_address, periods=10)
|
||||
assert locked_tokens == 0
|
||||
|
||||
receipt = bidder.claim()
|
||||
assert receipt['status'] == 1
|
||||
|
||||
# Cant claim more than once
|
||||
with pytest.raises(Bidder.ClaimError):
|
||||
_receipt = bidder.claim()
|
||||
|
||||
assert bidder.get_deposited_eth > application_economics.worklock_min_allowed_bid
|
||||
assert bidder.completed_work == 0
|
||||
assert bidder.remaining_work <= application_economics.maximum_allowed_locked // 2
|
||||
assert bidder.refunded_work == 0
|
||||
|
||||
# Ensure that the claimant is now the holder of an unbonded stake.
|
||||
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder.checksum_address, periods=10)
|
||||
assert locked_tokens <= application_economics.maximum_allowed_locked
|
||||
|
||||
# Confirm the stake is unbonded
|
||||
worker_address = staking_agent.get_worker_from_staker(staker_address=bidder.checksum_address)
|
||||
assert worker_address == NULL_ADDRESS
|
|
@ -1,216 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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 pytest
|
||||
from eth_tester.exceptions import TransactionFailed
|
||||
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, StakingEscrowAgent, WorkLockAgent
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_create_worklock_agent(testerchain, test_registry, agency, application_economics):
|
||||
agent = WorkLockAgent(registry=test_registry)
|
||||
assert agent.contract_address
|
||||
same_agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
assert agent == same_agent
|
||||
assert not agent.is_claiming_available()
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_bidding(testerchain, agency, application_economics, test_registry):
|
||||
small_bid = application_economics.worklock_min_allowed_bid
|
||||
big_bid = 5 * application_economics.worklock_min_allowed_bid
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
# Round 1
|
||||
for multiplier, bidder in enumerate(testerchain.client.accounts[:11], start=1):
|
||||
bid = big_bid * multiplier
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
receipt = agent.bid(transacting_power=tpower, value=bid)
|
||||
assert receipt['status'] == 1
|
||||
|
||||
# Round 2
|
||||
for multiplier, bidder in enumerate(testerchain.client.accounts[:11], start=1):
|
||||
bid = (small_bid * 2) * multiplier
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
receipt = agent.bid(transacting_power=tpower, value=bid)
|
||||
assert receipt['status'] == 1
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_get_deposited_eth(testerchain, agency, application_economics, test_registry):
|
||||
small_bid = application_economics.worklock_min_allowed_bid
|
||||
small_bidder = testerchain.client.accounts[-1]
|
||||
tpower = TransactingPower(account=small_bidder, signer=Web3Signer(testerchain.client))
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
receipt = agent.bid(transacting_power=tpower, value=small_bid)
|
||||
assert receipt['status'] == 1
|
||||
bid = agent.get_deposited_eth(small_bidder)
|
||||
assert bid == small_bid
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_get_base_deposit_rate(agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
base_deposit_rate = agent.get_base_deposit_rate()
|
||||
assert base_deposit_rate == application_economics.min_authorization / application_economics.worklock_min_allowed_bid
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_get_base_refund_rate(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
base_refund_rate = agent.get_base_refund_rate()
|
||||
|
||||
slowing_refund = agent.contract.functions.SLOWING_REFUND().call()
|
||||
assert base_refund_rate == (application_economics.min_authorization / application_economics.worklock_min_allowed_bid) * \
|
||||
(slowing_refund / application_economics.worklock_boosting_refund_rate)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_cancel_bid(testerchain, agency, application_economics, test_registry):
|
||||
bidder = testerchain.client.accounts[1]
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
|
||||
assert agent.get_deposited_eth(bidder) # Bid
|
||||
receipt = agent.cancel_bid(transacting_power=tpower) # Cancel
|
||||
assert receipt['status'] == 1
|
||||
assert not agent.get_deposited_eth(bidder) # No more bid
|
||||
|
||||
# Can't cancel a bid twice in a row
|
||||
with pytest.raises((TransactionFailed, ValueError)):
|
||||
_receipt = agent.cancel_bid(transacting_power=tpower)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_get_remaining_work(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
bidder = testerchain.client.accounts[0]
|
||||
remaining = agent.get_remaining_work(checksum_address=bidder)
|
||||
assert remaining > 0
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_early_claim(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
bidder = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
with pytest.raises(TransactionFailed):
|
||||
_receipt = agent.claim(transacting_power=tpower)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_cancel_after_bidding(testerchain, agency, application_economics, test_registry):
|
||||
|
||||
# Wait until the bidding window closes...
|
||||
testerchain.time_travel(seconds=application_economics.bidding_duration + 1)
|
||||
|
||||
bidder = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
|
||||
assert agent.get_deposited_eth(bidder) # Bid
|
||||
receipt = agent.cancel_bid(transacting_power=tpower) # Cancel
|
||||
assert receipt['status'] == 1
|
||||
assert not agent.get_deposited_eth(bidder) # No more bid
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_claim_before_checking(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
bidder = testerchain.client.accounts[2]
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
|
||||
assert not agent.is_claiming_available()
|
||||
with pytest.raises(TransactionFailed):
|
||||
_receipt = agent.claim(transacting_power=tpower)
|
||||
|
||||
# Wait until the cancellation window closes...
|
||||
testerchain.time_travel(seconds=application_economics.cancellation_end_date + 1)
|
||||
|
||||
assert not agent.is_claiming_available()
|
||||
with pytest.raises(TransactionFailed):
|
||||
_receipt = agent.claim(transacting_power=tpower)
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_force_refund(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
caller = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=caller, signer=Web3Signer(testerchain.client))
|
||||
|
||||
with pytest.raises(BlockchainInterface.InterfaceError):
|
||||
_receipt = agent.verify_bidding_correctness(transacting_power=tpower, gas_limit=100000)
|
||||
|
||||
receipt = agent.force_refund(transacting_power=tpower, addresses=testerchain.client.accounts[2:11])
|
||||
assert receipt['status'] == 1
|
||||
assert agent.get_available_compensation(testerchain.client.accounts[2]) > 0
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_verify_correctness(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry) # type: WorkLockAgent
|
||||
caller = testerchain.client.accounts[0]
|
||||
tpower = TransactingPower(account=caller, signer=Web3Signer(testerchain.client))
|
||||
assert not agent.bidders_checked()
|
||||
assert agent.estimate_verifying_correctness(gas_limit=100000) == 10
|
||||
receipt = agent.verify_bidding_correctness(transacting_power=tpower, gas_limit=100000)
|
||||
assert receipt['status'] == 1
|
||||
assert agent.bidders_checked()
|
||||
assert agent.is_claiming_available()
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_withdraw_compensation(testerchain, agency, application_economics, test_registry):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
bidder = testerchain.client.accounts[2]
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
balance = testerchain.w3.eth.getBalance(bidder)
|
||||
receipt = agent.withdraw_compensation(transacting_power=tpower)
|
||||
assert receipt['status'] == 1
|
||||
assert testerchain.w3.eth.getBalance(bidder) > balance
|
||||
assert agent.get_available_compensation(testerchain.client.accounts[2]) == 0
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_successful_claim(testerchain, agency, application_economics, test_registry):
|
||||
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=test_registry)
|
||||
staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=test_registry)
|
||||
|
||||
bidder = testerchain.client.accounts[2]
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
|
||||
# Ensure that the bidder is not staking.
|
||||
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder, periods=5)
|
||||
assert locked_tokens == 0
|
||||
|
||||
receipt = agent.claim(transacting_power=tpower)
|
||||
assert receipt['status'] == 1
|
||||
|
||||
# Cant claim more than once
|
||||
with pytest.raises(TransactionFailed):
|
||||
_receipt = agent.claim(transacting_power=tpower)
|
||||
|
||||
# Ensure that the claimant is now the holder of a stake.
|
||||
locked_tokens = staking_agent.get_locked_tokens(staker_address=bidder, periods=5)
|
||||
assert locked_tokens > 0
|
|
@ -18,7 +18,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
import pytest
|
||||
|
||||
from nucypher.blockchain.eth.agents import WorkLockAgent, PREApplicationAgent
|
||||
from nucypher.blockchain.eth.agents import PREApplicationAgent
|
||||
from nucypher.blockchain.eth.constants import PRE_APPLICATION_CONTRACT_NAME
|
||||
from nucypher.blockchain.eth.deployers import PREApplicationDeployer
|
||||
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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 pytest
|
||||
|
||||
from constant_sorrow import constants
|
||||
from nucypher.blockchain.economics import EconomicsFactory, Economics
|
||||
from nucypher.blockchain.eth.agents import WorkLockAgent
|
||||
from nucypher.blockchain.eth.constants import WORKLOCK_CONTRACT_NAME
|
||||
from nucypher.blockchain.eth.deployers import WorklockDeployer
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def baseline_deployment(staking_escrow_stub_deployer, transacting_power):
|
||||
staking_escrow_stub_deployer.deploy(deployment_mode=constants.INIT, transacting_power=transacting_power)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def worklock_deployer(baseline_deployment,
|
||||
testerchain,
|
||||
test_registry,
|
||||
application_economics):
|
||||
worklock_deployer = WorklockDeployer(registry=test_registry, economics=application_economics)
|
||||
return worklock_deployer
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_worklock_deployment(worklock_deployer,
|
||||
baseline_deployment,
|
||||
staking_escrow_stub_deployer,
|
||||
deployment_progress,
|
||||
test_registry,
|
||||
testerchain,
|
||||
transacting_power):
|
||||
|
||||
# Deploy
|
||||
assert worklock_deployer.contract_name == WORKLOCK_CONTRACT_NAME
|
||||
deployment_receipts = worklock_deployer.deploy(progress=deployment_progress,
|
||||
transacting_power=transacting_power) # < ---- DEPLOY
|
||||
|
||||
# deployment steps must match expected number of steps
|
||||
steps = worklock_deployer.deployment_steps
|
||||
assert deployment_progress.num_steps == len(steps) == len(deployment_receipts) == 3
|
||||
|
||||
# Ensure every step is successful
|
||||
for step_title in steps:
|
||||
assert deployment_receipts[step_title]['status'] == 1
|
||||
|
||||
# Ensure the correct staking escrow address is set
|
||||
staking_escrow_address = worklock_deployer.contract.functions.escrow().call()
|
||||
assert staking_escrow_stub_deployer.contract_address == staking_escrow_address
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_make_agent(worklock_deployer, test_registry):
|
||||
|
||||
agent = worklock_deployer.make_agent()
|
||||
|
||||
# Retrieve the PolicyManagerAgent singleton
|
||||
another_worklock_agent = WorkLockAgent(registry=test_registry)
|
||||
assert agent == another_worklock_agent # __eq__
|
||||
|
||||
# Compare the contract address for equality
|
||||
assert agent.contract_address == another_worklock_agent.contract_address
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_deployment_parameters(worklock_deployer, test_registry, application_economics):
|
||||
|
||||
# Ensure restoration of deployment parameters
|
||||
agent = worklock_deployer.make_agent()
|
||||
params = agent.worklock_parameters()
|
||||
supply, start, end, end_cancellation, boost, locktime, min_bid = params
|
||||
assert application_economics.worklock_supply == supply
|
||||
assert application_economics.bidding_start_date == start
|
||||
assert application_economics.bidding_end_date == end
|
||||
assert application_economics.cancellation_end_date == end_cancellation
|
||||
assert application_economics.worklock_boosting_refund_rate == boost
|
||||
assert application_economics.worklock_commitment_duration == locktime
|
||||
assert application_economics.worklock_min_allowed_bid == min_bid
|
|
@ -28,7 +28,6 @@ from nucypher.blockchain.eth.constants import (
|
|||
ADJUDICATOR_CONTRACT_NAME,
|
||||
DISPATCHER_CONTRACT_NAME,
|
||||
NUCYPHER_TOKEN_CONTRACT_NAME,
|
||||
POLICY_MANAGER_CONTRACT_NAME,
|
||||
STAKING_ESCROW_CONTRACT_NAME, STAKING_ESCROW_STUB_CONTRACT_NAME
|
||||
)
|
||||
from nucypher.blockchain.eth.deployers import (
|
||||
|
@ -91,7 +90,6 @@ def test_transfer_ownership(click_runner, testerchain, agency_local_registry):
|
|||
adjudicator_agent = ContractAgency.get_agent(AdjudicatorAgent, registry=agency_local_registry)
|
||||
|
||||
assert staking_agent.owner == testerchain.etherbase_account
|
||||
assert policy_agent.owner == testerchain.etherbase_account
|
||||
assert adjudicator_agent.owner == testerchain.etherbase_account
|
||||
|
||||
maclane = testerchain.unassigned_accounts[0]
|
||||
|
@ -115,7 +113,6 @@ def test_transfer_ownership(click_runner, testerchain, agency_local_registry):
|
|||
assert result.exit_code == 0
|
||||
|
||||
assert staking_agent.owner == maclane
|
||||
assert policy_agent.owner == testerchain.etherbase_account
|
||||
assert adjudicator_agent.owner == testerchain.etherbase_account
|
||||
|
||||
michwill = testerchain.unassigned_accounts[1]
|
||||
|
@ -256,20 +253,6 @@ def test_manual_deployment_of_idle_network(click_runner):
|
|||
deployed_contracts.extend([STAKING_ESCROW_STUB_CONTRACT_NAME, DISPATCHER_CONTRACT_NAME])
|
||||
assert list(new_registry.enrolled_names) == deployed_contracts
|
||||
|
||||
# 3. Deploy PolicyManager
|
||||
command = ('contracts',
|
||||
'--contract-name', POLICY_MANAGER_CONTRACT_NAME,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--registry-infile', str(ALTERNATE_REGISTRY_FILEPATH_2.absolute()))
|
||||
|
||||
result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
deployed_contracts.extend([POLICY_MANAGER_CONTRACT_NAME, DISPATCHER_CONTRACT_NAME])
|
||||
assert list(new_registry.enrolled_names) == deployed_contracts
|
||||
|
||||
# 4. Deploy Adjudicator
|
||||
command = ('contracts',
|
||||
'--contract-name', ADJUDICATOR_CONTRACT_NAME,
|
||||
|
|
|
@ -1,316 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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 random
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from eth_utils import to_wei
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.eth.actors import Bidder, Staker
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
ContractAgency,
|
||||
WorkLockAgent
|
||||
)
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.cli.commands.worklock import worklock
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.policy.payment import SubscriptionManagerPayment
|
||||
from tests.constants import (INSECURE_DEVELOPMENT_PASSWORD, MOCK_IP_ADDRESS, TEST_PROVIDER_URI, MOCK_PROVIDER_URI)
|
||||
from tests.utils.ursula import select_test_port
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def bids(testerchain):
|
||||
bids_distribution = dict()
|
||||
|
||||
min_bid_eth_value = 1
|
||||
max_bid_eth_value = 10
|
||||
|
||||
whale = testerchain.client.accounts[0]
|
||||
bids_distribution[whale] = 50_000
|
||||
for bidder in testerchain.client.accounts[1:12]:
|
||||
bids_distribution[bidder] = random.randrange(min_bid_eth_value, max_bid_eth_value)
|
||||
|
||||
return bids_distribution
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_status(click_runner, testerchain, agency_local_registry, application_economics):
|
||||
command = ('status',
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert str(NU.from_units(application_economics.worklock_supply)) in result.output
|
||||
assert str(Web3.fromWei(application_economics.worklock_min_allowed_bid, 'ether')) in result.output
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_bid(click_runner, testerchain, agency_local_registry, application_economics, bids):
|
||||
|
||||
# Wait until biding window starts
|
||||
testerchain.time_travel(seconds=90)
|
||||
|
||||
base_command = ('escrow',
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
|
||||
total_bids = 0
|
||||
# Multiple bidders
|
||||
for bidder, bid_eth_value in bids.items():
|
||||
pre_bid_balance = testerchain.client.get_balance(bidder)
|
||||
assert pre_bid_balance > to_wei(bid_eth_value, 'ether')
|
||||
|
||||
command = (*base_command, '--participant-address', bidder, '--value', bid_eth_value)
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
post_bid_balance = testerchain.client.get_balance(bidder)
|
||||
difference = pre_bid_balance - post_bid_balance
|
||||
assert difference >= to_wei(bid_eth_value, 'ether')
|
||||
|
||||
total_bids += to_wei(bid_eth_value, 'ether')
|
||||
assert testerchain.client.get_balance(worklock_agent.contract_address) == total_bids
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_cancel_bid(click_runner, testerchain, agency_local_registry, application_economics, bids):
|
||||
|
||||
bidders = list(bids.keys())
|
||||
|
||||
bidder = bidders[-1]
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
|
||||
|
||||
command = ('cancel-escrow',
|
||||
'--participant-address', bidder,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert not agent.get_deposited_eth(bidder) # No more bid
|
||||
|
||||
# Wait until the end of the bidding period
|
||||
testerchain.time_travel(seconds=application_economics.bidding_duration + 2)
|
||||
|
||||
bidder = bidders[-2]
|
||||
command = ('cancel-escrow',
|
||||
'--participant-address', bidder,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert not agent.get_deposited_eth(bidder) # No more bid
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_enable_claiming(click_runner, testerchain, agency_local_registry, application_economics):
|
||||
|
||||
# Wait until the end of the cancellation period
|
||||
testerchain.time_travel(seconds=application_economics.cancellation_window_duration + 2)
|
||||
|
||||
bidder = testerchain.client.accounts[0]
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
|
||||
assert not agent.is_claiming_available()
|
||||
assert not agent.bidders_checked()
|
||||
|
||||
command = ('enable-claiming',
|
||||
'--participant-address', bidder,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--force',
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--gas-limit', 100000)
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert agent.is_claiming_available()
|
||||
assert agent.bidders_checked()
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_claim(click_runner, testerchain, agency_local_registry, application_economics):
|
||||
agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
|
||||
|
||||
bidder = testerchain.client.accounts[2]
|
||||
command = ('claim',
|
||||
'--participant-address', bidder,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
whale = testerchain.client.accounts[0]
|
||||
assert agent.get_available_compensation(checksum_address=whale) > 0
|
||||
command = ('claim',
|
||||
'--participant-address', whale,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert agent.get_available_compensation(checksum_address=whale) == 0
|
||||
|
||||
# TODO: Check successful new stake in StakingEscrow
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_remaining_work(click_runner, testerchain, agency_local_registry, application_economics):
|
||||
bidder = testerchain.client.accounts[2]
|
||||
|
||||
# Ensure there is remaining work one layer below
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
|
||||
remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
|
||||
assert remaining_work > 0
|
||||
|
||||
command = ('remaining-work',
|
||||
'--participant-address', bidder,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Ensure were displaying the bidder address and remaining work in the output
|
||||
assert bidder in result.output
|
||||
assert str(remaining_work) in result.output
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_refund(click_runner, testerchain, agency_local_registry, application_economics):
|
||||
|
||||
bidder = testerchain.client.accounts[2]
|
||||
operator_address = testerchain.unassigned_accounts[-1]
|
||||
|
||||
#
|
||||
# WorkLock Staker-Operator
|
||||
#
|
||||
|
||||
worklock_agent = ContractAgency.get_agent(WorkLockAgent, registry=agency_local_registry)
|
||||
|
||||
# Bidder is now STAKER. Bond a worker.
|
||||
tpower = TransactingPower(account=bidder, signer=Web3Signer(testerchain.client))
|
||||
staker = Staker(transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN,
|
||||
registry=agency_local_registry)
|
||||
receipt = staker.bond_worker(operator_address=operator_address)
|
||||
assert receipt['status'] == 1
|
||||
|
||||
worker = Ursula(is_me=True,
|
||||
domain=TEMPORARY_DOMAIN,
|
||||
provider_uri=MOCK_PROVIDER_URI,
|
||||
registry=agency_local_registry,
|
||||
checksum_address=bidder,
|
||||
signer=Web3Signer(testerchain.client),
|
||||
operator_address=operator_address,
|
||||
rest_host=MOCK_IP_ADDRESS,
|
||||
rest_port=select_test_port(),
|
||||
db_filepath=tempfile.mkdtemp(),
|
||||
payment_method=SubscriptionManagerPayment(provider=TEST_PROVIDER_URI, network=TEMPORARY_DOMAIN))
|
||||
|
||||
# Ensure there is work to do
|
||||
remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
|
||||
assert remaining_work > 0
|
||||
|
||||
# Do some work
|
||||
testerchain.time_travel(periods=1)
|
||||
for i in range(3):
|
||||
txhash = worker.commit_to_next_period()
|
||||
testerchain.wait_for_receipt(txhash)
|
||||
assert receipt['status'] == 1
|
||||
testerchain.time_travel(periods=1)
|
||||
|
||||
command = ('refund',
|
||||
'--participant-address', bidder,
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + 'Y\n'
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Less work to do...
|
||||
new_remaining_work = worklock_agent.get_remaining_work(checksum_address=bidder)
|
||||
assert new_remaining_work < remaining_work
|
||||
|
||||
|
||||
@pytest.mark.skip()
|
||||
def test_participant_status(click_runner, testerchain, agency_local_registry, application_economics):
|
||||
|
||||
tpower = TransactingPower(account=testerchain.client.accounts[2],
|
||||
signer=Web3Signer(testerchain.client))
|
||||
bidder = Bidder(transacting_power=tpower,
|
||||
domain=TEMPORARY_DOMAIN,
|
||||
registry=agency_local_registry)
|
||||
|
||||
command = ('status',
|
||||
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
|
||||
'--participant-address', bidder.checksum_address,
|
||||
'--provider', TEST_PROVIDER_URI,
|
||||
'--signer', TEST_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Bidder-specific data is displayed
|
||||
assert bidder.checksum_address in result.output
|
||||
assert str(bidder.remaining_work) in result.output
|
||||
assert str(bidder.available_refund) in result.output
|
||||
|
||||
# Worklock economics are displayed
|
||||
assert str(application_economics.worklock_boosting_refund_rate) in result.output
|
||||
assert str(NU.from_units(application_economics.worklock_supply)) in result.output
|
|
@ -1,60 +0,0 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
|
||||
import "contracts/NuCypherToken.sol";
|
||||
|
||||
|
||||
/**
|
||||
* @notice Contract for using in WorkLock tests
|
||||
*/
|
||||
contract StakingEscrowForWorkLockMock {
|
||||
|
||||
struct StakerInfo {
|
||||
uint256 value;
|
||||
bool measureWork;
|
||||
uint256 completedWork;
|
||||
uint16 periods;
|
||||
}
|
||||
|
||||
NuCypherToken public immutable token;
|
||||
uint32 public immutable secondsPerPeriod = 1;
|
||||
uint256 public immutable minAllowableLockedTokens;
|
||||
uint256 public immutable maxAllowableLockedTokens;
|
||||
uint16 public immutable minLockedPeriods;
|
||||
|
||||
mapping (address => StakerInfo) public stakerInfo;
|
||||
|
||||
constructor(
|
||||
NuCypherToken _token,
|
||||
uint256 _minAllowableLockedTokens,
|
||||
uint256 _maxAllowableLockedTokens,
|
||||
uint16 _minLockedPeriods
|
||||
) {
|
||||
token = _token;
|
||||
minAllowableLockedTokens = _minAllowableLockedTokens;
|
||||
maxAllowableLockedTokens = _maxAllowableLockedTokens;
|
||||
minLockedPeriods = _minLockedPeriods;
|
||||
}
|
||||
|
||||
function getCompletedWork(address _staker) external view returns (uint256) {
|
||||
return stakerInfo[_staker].completedWork;
|
||||
}
|
||||
|
||||
function setWorkMeasurement(address _staker, bool _measureWork) external returns (uint256) {
|
||||
stakerInfo[_staker].measureWork = _measureWork;
|
||||
return stakerInfo[_staker].completedWork;
|
||||
}
|
||||
|
||||
function depositFromWorkLock(address _staker, uint256 _value, uint16 _periods) external {
|
||||
stakerInfo[_staker].value = _value;
|
||||
stakerInfo[_staker].periods = _periods;
|
||||
token.transferFrom(msg.sender, address(this), _value);
|
||||
}
|
||||
|
||||
function setCompletedWork(address _staker, uint256 _completedWork) external {
|
||||
stakerInfo[_staker].completedWork = _completedWork;
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -20,22 +20,17 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
import requests
|
||||
from eth_utils import to_wei
|
||||
|
||||
from constant_sorrow import constants
|
||||
from web3.exceptions import ValidationError
|
||||
|
||||
from nucypher.blockchain.economics import Economics
|
||||
from nucypher.blockchain.eth.agents import StakingEscrowAgent, WorkLockAgent
|
||||
from nucypher.blockchain.eth.deployers import (
|
||||
AdjudicatorDeployer,
|
||||
BaseContractDeployer,
|
||||
NucypherTokenDeployer,
|
||||
StakingEscrowDeployer,
|
||||
WorklockDeployer
|
||||
StakingEscrowDeployer
|
||||
)
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, BaseContractRegistry
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.sol.compile.constants import SOLIDITY_SOURCE_ROOT, TEST_SOLIDITY_SOURCE_ROOT
|
||||
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
|
||||
|
|
|
@ -1,683 +0,0 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
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 random
|
||||
from unittest.mock import call, patch, PropertyMock
|
||||
|
||||
import maya
|
||||
import pytest
|
||||
from eth_utils import to_wei
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.actors import Bidder
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface
|
||||
from nucypher.blockchain.eth.utils import prettify_eth_amount
|
||||
from nucypher.cli.commands import worklock as worklock_command
|
||||
from nucypher.cli.commands.worklock import worklock
|
||||
from nucypher.cli.literature import (
|
||||
BID_AMOUNT_PROMPT_WITH_MIN_BID,
|
||||
BID_INCREASE_AMOUNT_PROMPT,
|
||||
BIDDING_WINDOW_CLOSED,
|
||||
CLAIMING_NOT_AVAILABLE,
|
||||
COLLECT_ETH_PASSWORD,
|
||||
CONFIRM_BID_VERIFICATION,
|
||||
CONFIRM_COLLECT_WORKLOCK_REFUND,
|
||||
CONFIRM_REQUEST_WORKLOCK_COMPENSATION,
|
||||
CONFIRM_WORKLOCK_CLAIM,
|
||||
GENERIC_SELECT_ACCOUNT,
|
||||
SELECTED_ACCOUNT,
|
||||
WORKLOCK_CLAIM_ADVISORY
|
||||
)
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from tests.constants import MOCK_PROVIDER_URI, YES, NO, INSECURE_DEVELOPMENT_PASSWORD
|
||||
from tests.mock.agents import MockContractAgent
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def surrogate_transacting_power(mock_testerchain):
|
||||
address = mock_testerchain.etherbase_account
|
||||
signer = Web3Signer(mock_testerchain.client)
|
||||
tpower = TransactingPower(account=address, signer=signer)
|
||||
return tpower
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def surrogate_bidder(mock_testerchain, test_registry, mock_worklock_agent, surrogate_transacting_power):
|
||||
bidder = Bidder(registry=test_registry,
|
||||
transacting_power=surrogate_transacting_power,
|
||||
domain=TEMPORARY_DOMAIN)
|
||||
return bidder
|
||||
|
||||
|
||||
def assert_successful_transaction_echo(bidder_address: str, cli_output: str):
|
||||
expected = (bidder_address,
|
||||
MockContractAgent.FAKE_RECEIPT['blockHash'].hex(),
|
||||
MockContractAgent.FAKE_RECEIPT['blockNumber'],
|
||||
MockContractAgent.FAKE_RECEIPT['transactionHash'].hex())
|
||||
for output in expected:
|
||||
assert str(output) in cli_output, f'"{output}" not in bidding output'
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_status(click_runner, mock_worklock_agent, test_registry_source_manager):
|
||||
command = ('status', '--provider', MOCK_PROVIDER_URI, '--network', TEMPORARY_DOMAIN)
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_account_selection(click_runner, mocker, mock_testerchain, mock_worklock_agent, test_registry_source_manager):
|
||||
accounts = list(mock_testerchain.client.accounts)
|
||||
index = random.choice(range(len(accounts)))
|
||||
the_chosen_one = accounts[index]
|
||||
|
||||
# I spy
|
||||
mock_select = mocker.spy(worklock_command, 'select_client_account')
|
||||
|
||||
command = ('cancel-escrow',
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
user_input = '\n'.join((str(index), INSECURE_DEVELOPMENT_PASSWORD, YES, YES))
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Check call
|
||||
mock_select.assert_called_once()
|
||||
|
||||
# Check output
|
||||
assert GENERIC_SELECT_ACCOUNT in result.output
|
||||
assert SELECTED_ACCOUNT.format(choice=index, chosen_account=the_chosen_one) in result.output
|
||||
assert COLLECT_ETH_PASSWORD.format(checksum_address=the_chosen_one) in result.output
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def bidding_command(application_economics, surrogate_bidder):
|
||||
minimum = application_economics.worklock_min_allowed_bid
|
||||
bid_value = random.randint(minimum, minimum*100)
|
||||
command = ('escrow',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--value', bid_value,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
return command
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_bid_too_soon(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
application_economics,
|
||||
test_registry_source_manager,
|
||||
surrogate_bidder,
|
||||
mock_testerchain,
|
||||
bidding_command):
|
||||
|
||||
a_month_in_seconds = 3600*24*30
|
||||
|
||||
# Bidding window is not open yet
|
||||
the_past = maya.now().epoch - a_month_in_seconds
|
||||
user_input = INSECURE_DEVELOPMENT_PASSWORD
|
||||
with patch.object(maya, 'now', return_value=mocker.Mock(epoch=the_past)):
|
||||
result = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=user_input)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert BIDDING_WINDOW_CLOSED in result.output
|
||||
|
||||
# Let's assume that the previous check pass for some reason. It still should fail, at the Bidder layer
|
||||
now = mock_testerchain.get_blocktime()
|
||||
a_month_too_soon = now - a_month_in_seconds
|
||||
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=a_month_too_soon)
|
||||
with pytest.raises(Bidder.BiddingIsClosed):
|
||||
_ = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_bid_too_late(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
application_economics,
|
||||
test_registry_source_manager,
|
||||
surrogate_bidder,
|
||||
mock_testerchain,
|
||||
bidding_command):
|
||||
|
||||
a_month_in_seconds = 3600*24*30
|
||||
|
||||
# Bidding window is closed
|
||||
the_future = maya.now().epoch + a_month_in_seconds
|
||||
user_input = INSECURE_DEVELOPMENT_PASSWORD
|
||||
with patch.object(maya, 'now', return_value=mocker.Mock(epoch=the_future)):
|
||||
result = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=user_input)
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert BIDDING_WINDOW_CLOSED in result.output
|
||||
|
||||
# Let's assume that the previous check pass for some reason. It still should fail, at the Bidder layer
|
||||
now = mock_testerchain.get_blocktime()
|
||||
a_month_too_late = now + a_month_in_seconds
|
||||
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=a_month_too_late)
|
||||
with pytest.raises(Bidder.BiddingIsClosed):
|
||||
_ = click_runner.invoke(worklock, bidding_command, catch_exceptions=False, input=INSECURE_DEVELOPMENT_PASSWORD)
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_valid_bid(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
application_economics,
|
||||
test_registry_source_manager,
|
||||
surrogate_bidder,
|
||||
mock_testerchain,
|
||||
surrogate_transacting_power):
|
||||
|
||||
now = mock_testerchain.get_blocktime()
|
||||
sometime_later = now + 100
|
||||
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
|
||||
|
||||
minimum = application_economics.worklock_min_allowed_bid
|
||||
bid_value = random.randint(minimum, minimum * 100)
|
||||
bid_value_in_eth = Web3.fromWei(bid_value, 'ether')
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_ensure = mocker.spy(Bidder, 'ensure_bidding_is_open')
|
||||
mock_place_bid = mocker.spy(Bidder, 'place_bid')
|
||||
|
||||
# Patch Bidder.get_deposited_eth so it returns what we expect, in the correct sequence
|
||||
deposited_eth_sequence = (
|
||||
0, # When deciding if it's a new bid or increasing the existing one
|
||||
0, # When placing the bid, inside Bidder.place_bid
|
||||
bid_value, # When printing the CLI result, after the bid is placed ..
|
||||
bid_value, # .. we use it twice
|
||||
)
|
||||
mocker.patch.object(Bidder, 'get_deposited_eth', new_callable=PropertyMock, side_effect=deposited_eth_sequence)
|
||||
|
||||
command = ('escrow',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--value', bid_value_in_eth,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
user_input = INSECURE_DEVELOPMENT_PASSWORD
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False, input=user_input)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# OK - Let's see what happened
|
||||
|
||||
# Bidder
|
||||
mock_ensure.assert_called_once() # checked that the bidding window was open via actors layer
|
||||
mock_place_bid.assert_called_once()
|
||||
mock_place_bid.assert_called_once_with(surrogate_bidder, value=bid_value)
|
||||
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
|
||||
|
||||
# Transactions
|
||||
mock_worklock_agent.assert_only_transactions(allowed=[mock_worklock_agent.bid])
|
||||
mock_worklock_agent.bid.assert_called_with(transacting_power=surrogate_transacting_power, value=bid_value)
|
||||
|
||||
# Calls
|
||||
expected_calls = (mock_worklock_agent.eth_to_tokens, )
|
||||
for expected_call in expected_calls:
|
||||
expected_call.assert_called()
|
||||
|
||||
# CLI output
|
||||
assert prettify_eth_amount(bid_value) in result.output
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_cancel_bid(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder,
|
||||
surrogate_transacting_power):
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_cancel = mocker.spy(Bidder, 'cancel_bid')
|
||||
|
||||
command = ('cancel-escrow',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
result = click_runner.invoke(worklock, command, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Bidder
|
||||
mock_cancel.assert_called_once()
|
||||
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
|
||||
|
||||
# Transactions
|
||||
mock_worklock_agent.assert_only_transactions(allowed=[mock_worklock_agent.cancel_bid])
|
||||
mock_worklock_agent.cancel_bid.called_once_with(transacting_power=surrogate_transacting_power)
|
||||
|
||||
# Calls
|
||||
mock_worklock_agent.get_deposited_eth.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_enable_claiming(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder,
|
||||
application_economics,
|
||||
mock_testerchain,
|
||||
surrogate_transacting_power):
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_force_refund = mocker.spy(Bidder, 'force_refund')
|
||||
mock_verify = mocker.spy(Bidder, 'verify_bidding_correctness')
|
||||
mock_get_whales = mocker.spy(Bidder, 'get_whales')
|
||||
|
||||
# Cancellation window is closed
|
||||
now = mock_testerchain.get_blocktime()
|
||||
sometime_later = now+(3600*30)
|
||||
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
|
||||
|
||||
# Prepare bidders
|
||||
bidders = mock_testerchain.client.accounts[0:10]
|
||||
num_bidders = len(bidders)
|
||||
bonus_lot_value = application_economics.worklock_supply - application_economics.min_authorization * num_bidders
|
||||
|
||||
bids_before = [to_wei(50_000, 'ether')]
|
||||
min_bid_eth_value = to_wei(1, 'ether')
|
||||
max_bid_eth_value = to_wei(10, 'ether')
|
||||
for i in range(num_bidders - 1):
|
||||
bids_before.append(random.randrange(min_bid_eth_value, max_bid_eth_value))
|
||||
bonus_eth_supply_before = sum(bids_before) - application_economics.worklock_min_allowed_bid * num_bidders
|
||||
|
||||
bids_after = [min_bid_eth_value] * num_bidders
|
||||
bonus_eth_supply_after = 0
|
||||
|
||||
min_bid = min(bids_before)
|
||||
bidder_to_exclude = bids_before.index(min_bid)
|
||||
bidders_to_check = bidders.copy()
|
||||
del bidders_to_check[bidder_to_exclude]
|
||||
|
||||
mock_worklock_agent.get_bonus_eth_supply.side_effect = [bonus_eth_supply_before, bonus_eth_supply_after, bonus_eth_supply_after]
|
||||
mock_worklock_agent.get_bonus_lot_value.return_value = bonus_lot_value
|
||||
mock_worklock_agent.get_bidders.return_value = bidders
|
||||
mock_worklock_agent.get_deposited_eth.side_effect = [*bids_before, *bids_after, *bids_after]
|
||||
mock_worklock_agent.bidders_checked.side_effect = [False, False, False, False, True]
|
||||
mock_worklock_agent.next_bidder_to_check.side_effect = [0, num_bidders // 2, num_bidders]
|
||||
mock_worklock_agent.estimate_verifying_correctness.side_effect = [3, 6]
|
||||
|
||||
command = ('enable-claiming',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
gas_limit_1 = 200000
|
||||
gas_limit_2 = 300000
|
||||
user_input = '\n'.join((INSECURE_DEVELOPMENT_PASSWORD, YES, str(gas_limit_1), NO, str(gas_limit_2), YES))
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
confirmation = CONFIRM_BID_VERIFICATION.format(bidder_address=surrogate_bidder.checksum_address,
|
||||
gas_limit=gas_limit_1,
|
||||
bidders_per_transaction=3)
|
||||
assert confirmation in result.output
|
||||
confirmation = CONFIRM_BID_VERIFICATION.format(bidder_address=surrogate_bidder.checksum_address,
|
||||
gas_limit=gas_limit_2,
|
||||
bidders_per_transaction=6)
|
||||
assert confirmation in result.output
|
||||
|
||||
# Bidder
|
||||
mock_force_refund.assert_called_once()
|
||||
mock_verify.assert_called_once()
|
||||
mock_get_whales.assert_called()
|
||||
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
|
||||
|
||||
# Manual checking of force_refund tx because of unpredictable order of actual bidders_to_check array
|
||||
transaction_executions = mock_worklock_agent.force_refund.call_args_list
|
||||
assert len(transaction_executions) == 1
|
||||
_agent_args, agent_kwargs = transaction_executions[0]
|
||||
tpower, addresses = agent_kwargs.values()
|
||||
assert tpower.account == surrogate_bidder.checksum_address
|
||||
assert sorted(addresses) == sorted(bidders_to_check)
|
||||
|
||||
mock_worklock_agent.verify_bidding_correctness.assert_has_calls([
|
||||
call(transacting_power=surrogate_transacting_power, gas_limit=gas_limit_2),
|
||||
call(transacting_power=surrogate_transacting_power, gas_limit=gas_limit_2)
|
||||
])
|
||||
mock_worklock_agent.assert_only_transactions([mock_worklock_agent.force_refund,
|
||||
mock_worklock_agent.verify_bidding_correctness])
|
||||
|
||||
# Calls
|
||||
mock_worklock_agent.estimate_verifying_correctness.assert_has_calls([
|
||||
call(gas_limit=gas_limit_1),
|
||||
call(gas_limit=gas_limit_2)
|
||||
])
|
||||
mock_worklock_agent.get_bidders.assert_called()
|
||||
mock_worklock_agent.get_bonus_lot_value.assert_called()
|
||||
mock_worklock_agent.get_bonus_eth_supply.assert_called()
|
||||
mock_worklock_agent.next_bidder_to_check.assert_called()
|
||||
mock_worklock_agent.get_deposited_eth.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_initial_claim(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder,
|
||||
surrogate_transacting_power):
|
||||
|
||||
bidder_address = surrogate_bidder.checksum_address
|
||||
command = ('claim',
|
||||
'--participant-address', bidder_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
# First, let's test that if claiming is not available, command fails
|
||||
mock_worklock_agent.is_claiming_available.return_value = False
|
||||
result = click_runner.invoke(worklock, command, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False)
|
||||
assert result.exit_code == 1
|
||||
assert CLAIMING_NOT_AVAILABLE in result.output
|
||||
|
||||
# Let's continue with our test and try the command again. But don't forget to restore the previous mock
|
||||
mock_worklock_agent.is_claiming_available.return_value = True
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_withdraw_compensation = mocker.spy(Bidder, 'withdraw_compensation')
|
||||
mock_claim = mocker.spy(Bidder, 'claim')
|
||||
|
||||
# TODO: Test this functionality in isolation
|
||||
mocker.patch.object(Bidder, '_ensure_cancellation_window')
|
||||
|
||||
# Bidder has not claimed yet
|
||||
mocker.patch.object(
|
||||
Bidder, 'has_claimed',
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=False
|
||||
)
|
||||
|
||||
# Customize mock worklock agent method worklock parameters so position -2 returns lock periods
|
||||
mock_worklock_agent.worklock_parameters.return_value = [0xAA, 0xBB, 30, 0xCC]
|
||||
|
||||
user_input = '\n'.join((INSECURE_DEVELOPMENT_PASSWORD, YES, YES))
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
assert CONFIRM_REQUEST_WORKLOCK_COMPENSATION.format(bidder_address=bidder_address) in result.output
|
||||
assert WORKLOCK_CLAIM_ADVISORY.format(lock_duration=30) in result.output
|
||||
assert CONFIRM_WORKLOCK_CLAIM.format(bidder_address=bidder_address) in result.output
|
||||
|
||||
mock_worklock_agent.claim.assert_called_once_with(transacting_power=surrogate_transacting_power)
|
||||
|
||||
# Bidder
|
||||
mock_withdraw_compensation.assert_called_once()
|
||||
mock_claim.assert_called_once()
|
||||
assert_successful_transaction_echo(bidder_address=bidder_address, cli_output=result.output)
|
||||
|
||||
# Transactions
|
||||
mock_worklock_agent.withdraw_compensation.assert_called_with(transacting_power=surrogate_transacting_power)
|
||||
mock_worklock_agent.claim.assert_called_with(transacting_power=surrogate_transacting_power)
|
||||
|
||||
# Calls
|
||||
expected_calls = (mock_worklock_agent.get_deposited_eth,
|
||||
mock_worklock_agent.eth_to_tokens)
|
||||
for expected_call in expected_calls:
|
||||
expected_call.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_already_claimed(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder,
|
||||
surrogate_transacting_power):
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_withdraw_compensation = mocker.spy(Bidder, 'withdraw_compensation')
|
||||
mock_claim = mocker.spy(Bidder, 'claim')
|
||||
|
||||
# TODO: Test this functionality in isolation
|
||||
mocker.patch.object(Bidder, '_ensure_cancellation_window')
|
||||
|
||||
# Bidder already claimed
|
||||
mocker.patch.object(
|
||||
Bidder, 'has_claimed',
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=True
|
||||
)
|
||||
|
||||
command = ('claim',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,
|
||||
'--force')
|
||||
|
||||
result = click_runner.invoke(worklock, command, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False)
|
||||
assert result.exit_code == 1 # TODO: Decide if this case should error (like now) or simply do nothing
|
||||
|
||||
# Bidder
|
||||
mock_withdraw_compensation.assert_called_once()
|
||||
assert_successful_transaction_echo(bidder_address=surrogate_bidder.checksum_address, cli_output=result.output)
|
||||
mock_claim.assert_not_called()
|
||||
|
||||
# Transactions
|
||||
mock_worklock_agent.withdraw_compensation.assert_called_with(transacting_power=surrogate_transacting_power)
|
||||
mock_worklock_agent.claim.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_remaining_work(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder):
|
||||
|
||||
remaining_work = 100
|
||||
mock_remaining_work = mocker.patch.object(Bidder,
|
||||
'remaining_work',
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=remaining_work)
|
||||
|
||||
command = ('remaining-work',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
assert str(remaining_work) in result.output, "Remaining work was not echoed."
|
||||
|
||||
# Bidder
|
||||
mock_remaining_work.assert_called_once()
|
||||
|
||||
# Transactions
|
||||
mock_worklock_agent.assert_no_transactions()
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_refund(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder,
|
||||
surrogate_transacting_power):
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_refund = mocker.spy(Bidder, 'refund_deposit')
|
||||
|
||||
bidder_address = surrogate_bidder.checksum_address
|
||||
command = ('refund',
|
||||
'--participant-address', bidder_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
user_input = INSECURE_DEVELOPMENT_PASSWORD + '\n' + YES
|
||||
result = click_runner.invoke(worklock, command, input=user_input, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Output
|
||||
assert CONFIRM_COLLECT_WORKLOCK_REFUND.format(bidder_address=bidder_address) in result.output
|
||||
|
||||
# Bidder
|
||||
mock_refund.assert_called_once()
|
||||
assert_successful_transaction_echo(bidder_address=bidder_address, cli_output=result.output)
|
||||
|
||||
# Transactions
|
||||
mock_worklock_agent.assert_only_transactions(allowed=[mock_worklock_agent.refund])
|
||||
mock_worklock_agent.refund.assert_called_with(transacting_power=surrogate_transacting_power)
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
@pytest.mark.usefixtures("test_registry_source_manager")
|
||||
def test_participant_status(click_runner,
|
||||
mock_worklock_agent,
|
||||
surrogate_bidder):
|
||||
command = ('status',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN)
|
||||
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False)
|
||||
assert result.exit_code == 0
|
||||
|
||||
expected_calls = (mock_worklock_agent.check_claim,
|
||||
mock_worklock_agent.eth_to_tokens,
|
||||
mock_worklock_agent.get_deposited_eth,
|
||||
mock_worklock_agent.get_eth_supply,
|
||||
mock_worklock_agent.get_base_deposit_rate,
|
||||
mock_worklock_agent.get_bonus_lot_value,
|
||||
mock_worklock_agent.get_bonus_deposit_rate,
|
||||
mock_worklock_agent.get_bonus_refund_rate,
|
||||
mock_worklock_agent.get_base_refund_rate,
|
||||
mock_worklock_agent.get_remaining_work,
|
||||
mock_worklock_agent.get_refunded_work)
|
||||
# Calls
|
||||
for expected_call in expected_calls:
|
||||
expected_call.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_interactive_new_bid(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
application_economics,
|
||||
test_registry_source_manager,
|
||||
surrogate_bidder,
|
||||
mock_testerchain):
|
||||
now = mock_testerchain.get_blocktime()
|
||||
sometime_later = now + 100
|
||||
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
|
||||
|
||||
minimum = application_economics.worklock_min_allowed_bid
|
||||
bid_value = random.randint(minimum, minimum * 100)
|
||||
bid_value_in_eth = Web3.fromWei(bid_value, 'ether')
|
||||
wrong_bid = random.randint(1, minimum - 1)
|
||||
wrong_bid_in_eth = Web3.fromWei(wrong_bid, 'ether')
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_place_bid = mocker.spy(Bidder, 'place_bid')
|
||||
|
||||
# Patch Bidder.get_deposited_eth so it returns what we expect, in the correct sequence
|
||||
deposited_eth_sequence = (
|
||||
0, # When deciding if it's a new bid or increasing the new one (in this case, a new bid)
|
||||
0, # When placing the bid, inside Bidder.place_bid
|
||||
bid_value, # When printing the CLI result, after the bid is placed ..
|
||||
bid_value, # .. we use it twice
|
||||
)
|
||||
mocker.patch.object(Bidder, 'get_deposited_eth', new_callable=PropertyMock, side_effect=deposited_eth_sequence)
|
||||
|
||||
command = ('escrow',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,)
|
||||
|
||||
user_input = "\n".join((INSECURE_DEVELOPMENT_PASSWORD, str(wrong_bid_in_eth), str(bid_value_in_eth), YES))
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False, input=user_input)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# OK - Let's see what happened
|
||||
|
||||
# Bidder
|
||||
mock_place_bid.assert_called_once()
|
||||
|
||||
# Output
|
||||
minimum_in_eth = Web3.fromWei(minimum, 'ether')
|
||||
expected_error = f"Error: {wrong_bid_in_eth} is smaller than the minimum valid value {minimum_in_eth}"
|
||||
assert expected_error in result.output
|
||||
expected_prompt = BID_AMOUNT_PROMPT_WITH_MIN_BID.format(minimum_bid_in_eth=Web3.fromWei(minimum, 'ether'))
|
||||
assert 2 == result.output.count(expected_prompt)
|
||||
|
||||
|
||||
@pytest.mark.skip('remove me')
|
||||
def test_interactive_increase_bid(click_runner,
|
||||
mocker,
|
||||
mock_worklock_agent,
|
||||
application_economics,
|
||||
test_registry_source_manager,
|
||||
surrogate_bidder,
|
||||
mock_testerchain):
|
||||
|
||||
now = mock_testerchain.get_blocktime()
|
||||
sometime_later = now + 100
|
||||
mocker.patch.object(BlockchainInterface, 'get_blocktime', return_value=sometime_later)
|
||||
|
||||
minimum = application_economics.worklock_min_allowed_bid
|
||||
bid_value = random.randint(1, minimum - 1)
|
||||
bid_value_in_eth = Web3.fromWei(bid_value, 'ether')
|
||||
|
||||
# Spy on the corresponding CLI function we are testing
|
||||
mock_place_bid = mocker.spy(Bidder, 'place_bid')
|
||||
|
||||
# Patch Bidder.get_deposited_eth so it returns what we expect, in the correct sequence
|
||||
deposited_eth_sequence = (
|
||||
minimum, # When deciding if it's a new bid or increasing the existing one (in this case, increasing)
|
||||
minimum, # When placing the bid, inside Bidder.place_bid
|
||||
minimum + bid_value, # When printing the CLI result, after the bid is placed ..
|
||||
minimum + bid_value, # .. we use it twice
|
||||
)
|
||||
mocker.patch.object(Bidder, 'get_deposited_eth', new_callable=PropertyMock, side_effect=deposited_eth_sequence)
|
||||
|
||||
command = ('escrow',
|
||||
'--participant-address', surrogate_bidder.checksum_address,
|
||||
'--provider', MOCK_PROVIDER_URI,
|
||||
'--signer', MOCK_PROVIDER_URI,
|
||||
'--network', TEMPORARY_DOMAIN,)
|
||||
|
||||
user_input = "\n".join((INSECURE_DEVELOPMENT_PASSWORD, str(bid_value_in_eth), YES))
|
||||
result = click_runner.invoke(worklock, command, catch_exceptions=False, input=user_input)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# OK - Let's see what happened
|
||||
|
||||
# Bidder
|
||||
mock_place_bid.assert_called_once()
|
||||
|
||||
# Output
|
||||
expected_prompt = BID_INCREASE_AMOUNT_PROMPT
|
||||
assert 1 == result.output.count(expected_prompt)
|
|
@ -24,8 +24,7 @@ from nucypher.blockchain.eth.agents import (
|
|||
AdjudicatorAgent,
|
||||
ContractAgency,
|
||||
NucypherTokenAgent,
|
||||
StakingEscrowAgent,
|
||||
WorkLockAgent
|
||||
StakingEscrowAgent
|
||||
)
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
|
|
Loading…
Reference in New Issue