Merge pull request #2848 from KPrasch/felix-eol

Retire Faucet
pull/2879/head
KPrasch 2022-02-08 07:35:46 -08:00 committed by GitHub
commit eff28ee328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 14 additions and 1287 deletions

View File

@ -40,9 +40,6 @@ appdirs = "*"
click = ">=7.0"
colorama = "*"
tabulate = "*"
# Felix
flask_sqlalchemy = "*"
sqlalchemy = "*"
[dev-packages]
# Pytest

View File

@ -1,117 +0,0 @@
- name: "Start Felix"
hosts: "{{ 'tag_Role_' + lookup('env', 'NUCYPHER_NETWORK_NAME') + '_felix' }}"
user: ubuntu
gather_facts: false
pre_tasks:
- name: "Install Python2.7 for Ansible Control"
raw: sudo apt -y update && sudo apt install -y python2.7-minimal python2.7-setuptools
- include_vars: "{{ lookup('env', 'ANSIBLE_VARIABLES') }}"
- include_vars:
file: "{{ networks_filepath }}"
name: networks
tasks:
- name: "Register Ethereum PPA"
become: yes
become_flags: "-H -S"
apt_repository:
repo: 'ppa:ethereum/ethereum'
state: present
- name: "Install System Dependencies"
become: yes
become_flags: "-H -S"
apt:
name: "{{ packages }}"
update_cache: yes
state: latest
vars:
packages:
- python-pip
- python3
- python3-pip
- python3-dev
- python3-setuptools
- libffi-dev
- software-properties-common
- ethereum
- npm
- git:
repo: "{{ git_repo }}"
dest: ./code
version: "{{ git_version }}"
- pip:
chdir: ./code
name: '.'
editable: true
virtualenv: '/home/ubuntu/venv'
virtualenv_python: python3.6
virtualenv_site_packages: true
environment:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
- name: "Check if 'felix.config' Exists"
become: yes
become_flags: "-H -S"
stat:
path: "~/.local/share/nucypher/felix.config"
register: config_stat_result
- name: "Initialize Felix Configuration"
become: yes
become_flags: "-H -S"
shell: "{{ nucypher_exec }} felix init --geth --network {{ network }}"
environment:
NUCYPHER_KEYSTORE_PASSWORD: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
vars:
nucypher_exec: "/home/ubuntu/venv/bin/nucypher"
network: "{{ lookup('env', 'NUCYPHER_NETWORK_NAME') }}"
when: config_stat_result.stat.exists == False
- name: "Check if 'felix.db' Exists"
become: yes
become_flags: "-H -S"
stat:
path: "~/.local/share/nucypher/felix.db"
register: db_stat_result
- name: "Initialize Felix Database"
become: yes
become_flags: "-H -S"
shell: "{{ nucypher_exec }} felix createdb --geth --network {{ network }}"
environment:
NUCYPHER_KEYSTORE_PASSWORD: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
NUCYPHER_FELIX_DB_SECRET: "{{ lookup('env', 'NUCYPHER_FELIX_DB_SECRET') }}"
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
vars:
nucypher_exec: "/home/ubuntu/venv/bin/nucypher"
network: "{{ lookup('env', 'NUCYPHER_NETWORK_NAME') }}"
when: db_stat_result.stat.exists == False
- name: "Open Felix HTTP Port"
become: yes
become_flags: "-H -S"
shell: 'iptables -A INPUT -p tcp -m conntrack --dport {{ felix_http_port }} --ctstate NEW,ESTABLISHED -j ACCEPT'
vars:
felix_http_port: 6151
- name: "Render Felix's Node Service"
become: yes
become_flags: "-H -S"
template:
src: ../../services/felix_faucet.j2
dest: /etc/systemd/system/felix_faucet.service
mode: 0755
vars:
keystore_password: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
db_secret: "{{ lookup('env', 'NUCYPHER_FELIX_DB_SECRET') }}"
virtualenv_path: '/home/ubuntu/venv'
nucypher_network_domain: "{{ lookup('env', 'NUCYPHER_NETWORK_NAME') }}"

View File

@ -1,51 +0,0 @@
- name: "Start Felix"
hosts: "{{ 'tag_Role_' + lookup('env', 'NUCYPHER_NETWORK_NAME') + '_felix' }}"
user: ubuntu
gather_facts: false
pre_tasks:
- name: "Install Python2.7 for Ansible Control"
raw: sudo apt -y update && sudo apt install -y python2.7-minimal python2.7-setuptools
- include_vars: "{{ lookup('env', 'ANSIBLE_VARIABLES') }}"
- include_vars:
file: "{{ networks_filepath }}"
name: networks
tasks:
- git:
repo: "{{ git_repo }}"
dest: ./code
version: "{{ git_version }}"
- name: "Render Felix's Node Service"
become: yes
become_flags: "-H -S"
template:
src: ../../services/felix_faucet.j2
dest: /etc/systemd/system/felix_faucet.service
mode: 0755
vars:
keystore_password: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
db_secret: "{{ lookup('env', 'NUCYPHER_FELIX_DB_SECRET') }}"
virtualenv_path: '/home/ubuntu/venv'
nucypher_network_domain: "{{ lookup('env', 'NUCYPHER_NETWORK_NAME') }}"
teacher_uri: "{{ networks[lookup('env', 'NUCYPHER_NETWORK_NAME')][0] }}"
- name: "Open Felix HTTP Port"
become: yes
become_flags: "-H -S"
shell: 'iptables -A INPUT -p tcp -m conntrack --dport {{ felix_http_port }} --ctstate NEW,ESTABLISHED -j ACCEPT'
vars:
felix_http_port: 6151
- name: "Enable and Start Distribution"
become: yes
become_flags: "-H -S"
systemd:
daemon_reload: yes
no_block: yes
enabled: yes
state: restarted
name: "felix_faucet"

View File

@ -1,44 +0,0 @@
- name: "Start Felix"
hosts: "{{ 'tag_Role_' + lookup('env', 'NUCYPHER_NETWORK_NAME') + '_felix' }}"
user: ubuntu
gather_facts: false
pre_tasks:
- name: "Install Python2.7 for Ansible Control"
raw: sudo apt -y update && sudo apt install -y python2.7-minimal python2.7-setuptools
- include_vars: "{{ lookup('env', 'ANSIBLE_VARIABLES') }}"
- include_vars:
file: "{{ networks_filepath }}"
name: networks
tasks:
- git:
repo: "{{ git_repo }}"
dest: ./code
version: "{{ git_version }}"
- name: "Open Felix HTTP Port"
become: yes
become_flags: "-H -S"
shell: 'iptables -A INPUT -p tcp -m conntrack --dport {{ felix_http_port }} --ctstate NEW,ESTABLISHED -j ACCEPT'
vars:
felix_http_port: 80
- name: "Open Felix HTTP Port"
become: yes
become_flags: "-H -S"
shell: 'iptables -A INPUT -p tcp -m conntrack --dport {{ felix_http_port }} --ctstate NEW,ESTABLISHED -j ACCEPT'
vars:
felix_http_port: 6151
- name: "Enable and Start Distribution"
become: yes
become_flags: "-H -S"
systemd:
daemon_reload: yes
no_block: yes
enabled: yes
state: restarted
name: "felix_faucet"

View File

@ -1,12 +0,0 @@
[Unit]
Description="Run 'Felix', A NuCypher Test-ERC20 Faucet."
[Service]
User=root
Type=simple
Environment="NUCYPHER_KEYSTORE_PASSWORD={{ keystore_password }}"
Environment="NUCYPHER_FELIX_DB_SECRET={{ db_secret }}"
ExecStart={{ virtualenv_path }}/bin/nucypher felix run --debug --network {{ nucypher_network_domain }} --geth
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1 @@
Retires and removes eth/token faucet.

View File

@ -94,18 +94,6 @@ the Untrusted Re-Encryption Proxy.
{}
'''
FELIX_BANNER = r"""
'||''''| '||`
|| . || ''
||''| .|''|, || || \\ //
|| ||..|| || || ><
.||. `|... .||. .||. // \\
the Unlicensed Faucet Plumber
{}
"""
STAKEHOLDER_BANNER = r"""
____ __ __

View File

@ -1,475 +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 http import HTTPStatus
import json
from pathlib import Path
import eth_utils
import math
import maya
import os
import time
from constant_sorrow.constants import NOT_RUNNING, NO_DATABASE_AVAILABLE
from datetime import datetime, timedelta
from decimal import Decimal
from eth_typing.evm import ChecksumAddress
from flask import Flask, Response
from hendrix.deploy.base import HendrixDeploy
from nacl.hash import sha256
from sqlalchemy import create_engine, or_
from twisted.internet import reactor, threads
from twisted.internet.task import LoopingCall
from nucypher.blockchain.economics import EconomicsFactory
from nucypher.blockchain.eth.actors import NucypherTokenActor
from nucypher.blockchain.eth.agents import (ContractAgency, NucypherTokenAgent)
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.blockchain.eth.token import NU
from nucypher.characters.banners import FELIX_BANNER, NU_BANNER
from nucypher.characters.base import Character
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH, TEMPLATES_DIR
from nucypher.crypto.powers import SigningPower, TransactingPower
from nucypher.datastore.deprecated import ThreadedSession
from nucypher.utilities.logging import Logger
from nucypher.network.resources import get_static_resources
class Felix(Character, NucypherTokenActor):
"""
A NuCypher ERC20 faucet / Airdrop scheduler.
Felix is a web application that gives NuCypher *testnet* tokens to registered addresses
with a scheduled reduction of disbursement amounts, and an HTTP endpoint
for handling new address registration.
The main goal of Felix is to provide a source of testnet tokens for
research and the development of production-ready nucypher dApps.
"""
_default_crypto_powerups = [SigningPower]
# Intervals
DISTRIBUTION_INTERVAL = 60 # seconds
DISBURSEMENT_INTERVAL = 24 * 365 # only distribute tokens to the same address once each YEAR.
STAGING_DELAY = 10 # seconds
# Disbursement
BATCH_SIZE = 10 # transactions
MULTIPLIER = Decimal('0.9') # 10% reduction of previous disbursement is 0.9
# this is not relevant until the year of time declared above, passes.
MINIMUM_DISBURSEMENT = int(1e18) # NuNits (1 NU)
ETHER_AIRDROP_AMOUNT = int(1e17) # Wei (.1 ether)
MAX_INDIVIDUAL_REGISTRATIONS = 3 # Registration Limit
# Node Discovery
LEARNING_TIMEOUT = 30 # seconds
_SHORT_LEARNING_DELAY = 60 # seconds
_LONG_LEARNING_DELAY = 120 # seconds
_ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN = 1
# Twisted
_CLOCK = reactor
_AIRDROP_QUEUE = dict()
class NoDatabase(RuntimeError):
pass
def __init__(self,
db_filepath: Path,
rest_host: str,
rest_port: int,
client_password: str = None,
crash_on_error: bool = False,
distribute_ether: bool = True,
registry: BaseContractRegistry = None,
*args, **kwargs):
# Character
super().__init__(registry=registry, *args, **kwargs)
self.log = Logger(f"felix-{self.checksum_address[-6::]}")
# Network
self.rest_port = rest_port
self.rest_host = rest_host
self.rest_app = NOT_RUNNING
self.crash_on_error = crash_on_error
# Database
self.db_filepath = db_filepath
self.db = NO_DATABASE_AVAILABLE
self.db_engine = create_engine(f'sqlite:///{self.db_filepath.absolute()}', convert_unicode=True)
# Blockchain
self.transacting_power = TransactingPower(password=client_password,
account=self.checksum_address,
signer=self.signer,
cache=True)
self._crypto_power.consume_power_up(self.transacting_power)
self.token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=registry)
self.blockchain = self.token_agent.blockchain
self.reserved_addresses = [self.checksum_address, NULL_ADDRESS]
# Update reserved addresses with deployed contracts
existing_entries = list(registry.enrolled_addresses)
self.reserved_addresses.extend(existing_entries)
# Distribution
self.__distributed = 0 # Track NU Output
self.__airdrop = 0 # Track Batch
self.__disbursement = 0 # Track Quantity
self._distribution_task = LoopingCall(f=self.airdrop_tokens)
self._distribution_task.clock = self._CLOCK
self.start_time = NOT_RUNNING
self.economics = EconomicsFactory.get_economics(registry=registry)
self.MAXIMUM_DISBURSEMENT = self.economics.maximum_allowed_locked
self.INITIAL_DISBURSEMENT = self.economics.minimum_allowed_locked * 3
# Optionally send ether with each token transaction
self.distribute_ether = distribute_ether
# Banner
self.log.info(FELIX_BANNER.format(self.checksum_address))
def __repr__(self):
class_name = self.__class__.__name__
r = f'{class_name}(checksum_address={self.checksum_address}, db_filepath={self.db_filepath})'
return r
def start_learning_loop(self, now=False):
"""
Felix needs to not even be a Learner, but since it is at the moment, it certainly needs not to learn.
"""
def make_web_app(self):
from flask import request
from flask_sqlalchemy import SQLAlchemy
# WSGI/Flask Service
short_name = bytes(self.stamp).hex()[:6]
self.rest_app = Flask(f"faucet-{short_name}", template_folder=TEMPLATES_DIR)
self.rest_app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{self.db_filepath}'
self.rest_app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_CONTENT_LENGTH
try:
self.rest_app.secret_key = sha256(os.environ['NUCYPHER_FELIX_DB_SECRET'].encode()) # uses envvar
except KeyError:
raise OSError("The 'NUCYPHER_FELIX_DB_SECRET' is not set. Export your application secret and try again.")
# Database
self.db = SQLAlchemy(self.rest_app)
# Database Tables
class Recipient(self.db.Model):
"""
The one and only table in Felix's database; Used to track recipients and airdrop metadata.
"""
__tablename__ = 'recipient'
id = self.db.Column(self.db.Integer, primary_key=True)
address = self.db.Column(self.db.String, nullable=False)
joined = self.db.Column(self.db.DateTime, nullable=False, default=datetime.utcnow)
total_received = self.db.Column(self.db.String, default='0', nullable=False)
last_disbursement_amount = self.db.Column(self.db.String, nullable=False, default=0)
last_disbursement_time = self.db.Column(self.db.DateTime, nullable=True, default=None)
is_staking = self.db.Column(self.db.Boolean, nullable=False, default=False)
def __repr__(self):
return f'{self.__class__.__name__}(id={self.id})'
self.Recipient = Recipient # Bind to outer class
# Flask decorators
rest_app = self.rest_app
#
# REST Routes
#
@rest_app.route("/status", methods=['GET'])
def status():
with ThreadedSession(self.db_engine) as session:
total_recipients = session.query(self.Recipient).count()
last_recipient = session.query(self.Recipient).filter(
self.Recipient.last_disbursement_time.isnot(None)
).order_by('last_disbursement_time').first()
last_address = last_recipient.address if last_recipient else None
last_transaction_date = last_recipient.last_disbursement_time.isoformat() if last_recipient else None
unfunded = session.query(self.Recipient).filter(
self.Recipient.last_disbursement_time.is_(None)).count()
return json.dumps(
{
"total_recipients": total_recipients,
"latest_recipient": last_address,
"latest_disburse_date": last_transaction_date,
"unfunded_recipients": unfunded,
"state": {
"eth": str(self.eth_balance),
"NU": str(self.token_balance),
"address": self.checksum_address,
"contract_address": self.token_agent.contract_address,
}
}
)
@rest_app.route("/register", methods=['POST'])
def register():
"""Handle new recipient registration via POST request."""
new_address = (
request.form.get('address') or
request.get_json().get('address')
)
if not new_address:
return Response(response="no address was supplied", status=HTTPStatus.LENGTH_REQUIRED)
if not eth_utils.is_address(new_address):
return Response(response="an invalid ethereum address was supplied. please ensure the address is a proper checksum.", status=HTTPStatus.BAD_REQUEST)
else:
new_address = eth_utils.to_checksum_address(new_address)
if new_address in self.reserved_addresses:
return Response(response="sorry, that address is reserved and cannot receive funds.", status=HTTPStatus.FORBIDDEN)
try:
with ThreadedSession(self.db_engine) as session:
existing = Recipient.query.filter_by(address=new_address).all()
if len(existing) > self.MAX_INDIVIDUAL_REGISTRATIONS:
# Address already exists; Abort
self.log.debug(f"{new_address} is already enrolled {self.MAX_INDIVIDUAL_REGISTRATIONS} times.")
return Response(response=f"{new_address} requested too many times - Please use another address.", status=HTTPStatus.CONFLICT)
# Create the record
recipient = Recipient(address=new_address, joined=datetime.now())
session.add(recipient)
session.commit()
except Exception as e:
# Pass along exceptions to the logger
self.log.critical(str(e))
raise
else:
return Response(status=HTTPStatus.OK) # TODO
return rest_app
def create_tables(self) -> None:
self.make_web_app()
return self.db.create_all(app=self.rest_app)
def start(self,
host: str,
port: int,
web_services: bool = True,
distribution: bool = True,
crash_on_error: bool = False):
self.crash_on_error = crash_on_error
if self.start_time is not NOT_RUNNING:
raise RuntimeError("Felix is already running.")
self.start_time = maya.now()
payload = {"wsgi": self.rest_app, "http_port": port, "resources": get_static_resources()}
deployer = HendrixDeploy(action="start", options=payload)
if distribution is True:
self.start_distribution()
if web_services is True:
deployer.run() # <-- Blocking call (Reactor)
def start_distribution(self, now: bool = True) -> bool:
"""Start token distribution"""
self.log.info(NU_BANNER)
self.log.info("Starting NU Token Distribution | START")
if self.token_balance == NU.ZERO():
raise self.ActorError(f"Felix address {self.checksum_address} has 0 NU tokens.")
self._distribution_task.start(interval=self.DISTRIBUTION_INTERVAL, now=now)
return True
def stop_distribution(self) -> bool:
"""Start token distribution"""
self.log.info("Stopping NU Token Distribution | STOP")
self._distribution_task.stop()
return True
def __calculate_disbursement(self, recipient: ChecksumAddress) -> int:
"""Calculate the next reward for a recipient once the are selected for distribution"""
# Initial Reward - sets the future rates
if recipient.last_disbursement_time is None:
amount = self.INITIAL_DISBURSEMENT
# Cap reached, We'll continue to leak the minimum disbursement
elif int(recipient.total_received) >= self.MAXIMUM_DISBURSEMENT:
amount = self.MINIMUM_DISBURSEMENT
# Calculate the next disbursement
else:
amount = math.ceil(int(recipient.last_disbursement_amount) * self.MULTIPLIER)
if amount < self.MINIMUM_DISBURSEMENT:
amount = self.MINIMUM_DISBURSEMENT
return int(amount)
def __transfer(self, disbursement: int, recipient_address: str) -> str:
"""Perform a single token transfer transaction from one account to another."""
self.__disbursement += 1
receipt = self.token_agent.transfer(amount=disbursement,
target_address=recipient_address,
transacting_power=self.transacting_power)
txhash = receipt['transactionHash']
if self.distribute_ether:
ether = self.ETHER_AIRDROP_AMOUNT
transaction = {'to': recipient_address,
'from': self.checksum_address,
'value': ether,
'gasPrice': self.blockchain.client.gas_price_for_transaction()}
transaction_dict = self.blockchain.build_payload(sender_address=self.checksum_address,
payload=transaction,
transaction_gas_limit=22000)
_receipt = self.blockchain.sign_and_broadcast_transaction(transacting_power=self.transacting_power,
transaction_dict=transaction_dict,
transaction_name='transfer')
self.log.info(f"Disbursement #{self.__disbursement} OK | NU {txhash.hex()[-6:]}"
f"({str(NU(disbursement, 'NuNit'))} + {self.ETHER_AIRDROP_AMOUNT} wei) -> {recipient_address}")
else:
self.log.info(
f"Disbursement #{self.__disbursement} OK"
f"({str(NU(disbursement, 'NuNit'))} -> {recipient_address}")
return txhash
def airdrop_tokens(self):
"""
Calculate airdrop eligibility via faucet registration
and transfer tokens to selected recipients.
"""
with ThreadedSession(self.db_engine) as session:
population = session.query(self.Recipient).count()
message = f"{population} registered faucet recipients; " \
f"Distributed {str(NU(self.__distributed, 'NuNit'))} since {self.start_time.slang_time()}."
self.log.debug(message)
if population == 0:
return # Abort - no recipients are registered.
# For filtration
since = datetime.now() - timedelta(hours=self.DISBURSEMENT_INTERVAL)
datetime_filter = or_(self.Recipient.last_disbursement_time <= since,
self.Recipient.last_disbursement_time == None) # This must be `==` not `is`
with ThreadedSession(self.db_engine) as session:
candidates = session.query(self.Recipient).filter(datetime_filter).all()
if not candidates:
self.log.info("No eligible recipients this round.")
return
# Discard invalid addresses, in-depth
invalid_addresses = list()
def siphon_invalid_entries(candidate):
address_is_valid = eth_utils.is_checksum_address(candidate.address)
if not address_is_valid:
invalid_addresses.append(candidate.address)
return address_is_valid
candidates = list(filter(siphon_invalid_entries, candidates))
if invalid_addresses:
self.log.info(f"{len(invalid_addresses)} invalid entries detected. Pruning database.")
# TODO: Is this needed? - Invalid entries are rejected at the endpoint view.
# Prune database of invalid records
# with ThreadedSession(self.db_engine) as session:
# bad_eggs = session.query(self.Recipient).filter(self.Recipient.address in invalid_addresses).all()
# for egg in bad_eggs:
# session.delete(egg.id)
# session.commit()
if not candidates:
self.log.info("No eligible recipients this round.")
return
d = threads.deferToThread(self.__do_airdrop, candidates=candidates)
self._AIRDROP_QUEUE[self.__airdrop] = d
return d
def __do_airdrop(self, candidates: list):
self.log.info(f"Staging Airdrop #{self.__airdrop}.")
# Staging
staged_disbursements = [(r, self.__calculate_disbursement(recipient=r)) for r in candidates]
batches = list(staged_disbursements[index:index+self.BATCH_SIZE] for index in range(0, len(staged_disbursements), self.BATCH_SIZE))
total_batches = len(batches)
self.log.info("====== Staged Airdrop ======")
for recipient, disbursement in staged_disbursements:
self.log.info(f"{recipient.address} ... {str(disbursement)[:-18]}")
self.log.info("==========================")
# Staging Delay
self.log.info(f"Airdrop will commence in {self.STAGING_DELAY} seconds...")
if self.STAGING_DELAY > 3:
time.sleep(self.STAGING_DELAY - 3)
for i in range(3):
time.sleep(1)
self.log.info(f"NU Token airdrop starting in {3 - i} seconds...")
# Slowly, in series...
for batch, staged_disbursement in enumerate(batches, start=1):
self.log.info(f"======= Batch #{batch} ========")
for recipient, disbursement in staged_disbursement:
# Perform the transfer... leaky faucet.
self.__transfer(disbursement=disbursement, recipient_address=recipient.address)
self.__distributed += disbursement
# Update the database record
recipient.last_disbursement_amount = str(disbursement)
recipient.total_received = str(int(recipient.total_received) + disbursement)
recipient.last_disbursement_time = datetime.now()
self.db.session.add(recipient)
self.db.session.commit()
# end inner loop
self.log.info(f"Completed Airdrop #{self.__airdrop} Batch #{batch} of {total_batches}.")
# end outer loop
now = maya.now()
next_interval_slang = now.add(seconds=self.DISTRIBUTION_INTERVAL).slang_time()
self.log.info(f"Completed Airdrop #{self.__airdrop}; Next airdrop is {next_interval_slang}.")
del self._AIRDROP_QUEUE[self.__airdrop]
self.__airdrop += 1

View File

@ -1,296 +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 os
from nucypher.control.emitters import StdoutEmitter
from nucypher.cli.actions.auth import (
get_client_password,
get_nucypher_password,
unlock_nucypher_keystore
)
from nucypher.cli.actions.configure import destroy_configuration, handle_missing_configuration_file
from nucypher.cli.actions.select import select_config_file
from nucypher.cli.config import group_general_config
from nucypher.cli.literature import (
CONFIRM_OVERWRITE_DATABASE,
FELIX_RUN_MESSAGE,
SUCCESSFUL_DATABASE_CREATION,
SUCCESSFUL_DATABASE_DESTRUCTION
)
from nucypher.cli.options import (
group_options,
option_checksum_address,
option_config_file,
option_config_root,
option_db_filepath,
option_dev,
option_discovery_port,
option_dry_run,
option_force,
option_middleware,
option_min_stake,
option_network,
option_poa,
option_provider_uri,
option_registry_filepath,
option_teacher_uri,
option_signer_uri,
)
from nucypher.cli.painting.help import paint_new_installation_help
from nucypher.cli.types import NETWORK_PORT
from nucypher.cli.utils import setup_emitter
from nucypher.config.characters import FelixConfiguration
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD
from nucypher.utilities.networking import LOOPBACK_ADDRESS
option_port = click.option('--port', help="The host port to run Felix HTTP services on", type=NETWORK_PORT, default=FelixConfiguration.DEFAULT_REST_PORT)
class FelixConfigOptions:
__option_name__ = 'config_options'
def __init__(self,
dev,
network,
provider_uri,
signer_uri,
host,
db_filepath: Path,
checksum_address,
registry_filepath: Path,
poa,
port):
self.provider_uri = provider_uri
self.signer_uri = signer_uri
self.domain = network
self.dev = dev
self.host = host
self.db_filepath = db_filepath
self.checksum_address = checksum_address
self.registry_filepath = registry_filepath
self.poa = poa
self.port = port
def create_config(self, emitter, config_file):
# Load Felix from Configuration File with overrides
if not config_file:
config_file = select_config_file(emitter=emitter,
checksum_address=self.checksum_address,
config_class=FelixConfiguration)
try:
return FelixConfiguration.from_configuration_file(
emitter=emitter,
filepath=config_file,
domain=self.domain,
registry_filepath=self.registry_filepath,
provider_uri=self.provider_uri,
signer=self.signer_uri,
rest_host=self.host,
rest_port=self.port,
db_filepath=self.db_filepath,
poa=self.poa)
except FileNotFoundError:
return handle_missing_configuration_file(
character_config_class=FelixConfiguration,
config_file=config_file
)
def generate_config(self, config_root: Path, discovery_port):
return FelixConfiguration.generate(
password=get_nucypher_password(emitter=StdoutEmitter(), confirm=True),
config_root=config_root,
rest_host=self.host,
rest_port=discovery_port,
db_filepath=self.db_filepath,
domain=self.domain,
checksum_address=self.checksum_address,
registry_filepath=self.registry_filepath,
provider_uri=self.provider_uri,
signer_uri=self.signer_uri,
poa=self.poa)
group_config_options = group_options(
FelixConfigOptions,
dev=option_dev,
network=option_network(),
provider_uri=option_provider_uri(),
signer_uri=option_signer_uri,
host=click.option('--host', help="The host to run Felix HTTP services on", type=click.STRING,
default=LOOPBACK_ADDRESS),
db_filepath=option_db_filepath,
checksum_address=option_checksum_address,
registry_filepath=option_registry_filepath,
poa=option_poa,
port=option_port,
)
class FelixCharacterOptions:
__option_name__ = 'character_options'
def __init__(self, config_options, teacher_uri, min_stake, middleware):
self.config_options = config_options
self.teacher_uris = [teacher_uri] if teacher_uri else None
self.min_stake = min_stake
self.middleware = middleware
def create_character(self, emitter, config_file, debug):
felix_config = self.config_options.create_config(emitter, config_file)
try:
# Authenticate
unlock_nucypher_keystore(emitter,
character_configuration=felix_config,
password=get_nucypher_password(emitter=emitter, confirm=False))
client_password = get_client_password(checksum_address=felix_config.checksum_address,
envvar=NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD)
# Produce Felix
FELIX = felix_config.produce(domain=self.config_options.domain)
FELIX.make_web_app() # attach web application, but dont start service
return FELIX
except Exception as e:
if debug:
raise
else:
emitter.echo(str(e), color='red', bold=True)
raise click.Abort
group_character_options = group_options(
FelixCharacterOptions,
config_options=group_config_options,
teacher_uri=option_teacher_uri,
min_stake=option_min_stake,
middleware=option_middleware,
)
@click.group()
def felix():
""""Felix the Faucet" management commands."""
@felix.command()
@group_general_config
@option_config_root
@option_discovery_port(default=FelixConfiguration.DEFAULT_LEARNER_PORT)
@group_config_options
def init(general_config, config_options, config_root, discovery_port):
"""Create a brand-new Felix."""
emitter = setup_emitter(general_config=general_config, banner=config_options.checksum_address)
if not config_root: # Flag
config_root = DEFAULT_CONFIG_ROOT # Envvar or init-only default
try:
new_felix_config = config_options.generate_config(config_root, discovery_port)
except Exception as e:
if general_config.debug:
raise
else:
emitter.echo(str(e), color='red', bold=True)
raise click.Abort
filepath = new_felix_config.to_configuration_file()
paint_new_installation_help(emitter, new_configuration=new_felix_config, filepath=filepath)
@felix.command()
@group_config_options
@option_config_file
@option_force
@group_general_config
def destroy(general_config, config_options, config_file, force):
"""Destroy Felix Configuration."""
emitter = setup_emitter(general_config, config_options.checksum_address)
felix_config = config_options.create_config(emitter, config_file)
destroy_configuration(emitter, character_config=felix_config, force=force)
@felix.command()
@group_character_options
@option_config_file
@option_force
@group_general_config
def createdb(general_config, character_options, config_file, force):
"""Create Felix DB."""
emitter = setup_emitter(general_config, character_options.config_options.checksum_address)
FELIX = character_options.create_character(emitter, config_file, general_config.debug)
if FELIX.db_filepath.is_file():
if not force:
click.confirm(CONFIRM_OVERWRITE_DATABASE, abort=True)
FELIX.db_filepath.unlink()
emitter.echo(SUCCESSFUL_DATABASE_DESTRUCTION.format(path=FELIX.db_filepath))
FELIX.create_tables()
emitter.echo(SUCCESSFUL_DATABASE_CREATION.format(path=FELIX.db_filepath), color='green')
@felix.command()
@group_character_options
@option_config_file
@group_general_config
def view(general_config, character_options, config_file):
"""View Felix token balance."""
emitter = setup_emitter(general_config, character_options.config_options.checksum_address)
FELIX = character_options.create_character(emitter, config_file, general_config.debug)
token_balance = FELIX.token_balance
eth_balance = FELIX.eth_balance
emitter.echo(f"""
Address .... {FELIX.checksum_address}
NU ......... {str(token_balance)}
ETH ........ {str(eth_balance)}
""")
@felix.command()
@group_character_options
@option_config_file
@group_general_config
def accounts(general_config, character_options, config_file):
"""View Felix known accounts."""
emitter = setup_emitter(general_config, character_options.config_options.checksum_address)
FELIX = character_options.create_character(emitter, config_file, general_config.debug)
accounts = FELIX.blockchain.client.accounts
for account in accounts:
emitter.echo(account)
@felix.command()
@group_character_options
@option_config_file
@option_dry_run
@group_general_config
def run(general_config, character_options, config_file, dry_run):
"""Run Felix services."""
emitter = setup_emitter(general_config, character_options.config_options.checksum_address)
FELIX = character_options.create_character(emitter, config_file, general_config.debug)
host = character_options.config_options.host
port = character_options.config_options.port
emitter.message(FELIX_RUN_MESSAGE.format(host=host, port=port))
FELIX.start(host=host,
port=port,
web_services=not dry_run,
distribution=True,
crash_on_error=general_config.debug)

View File

@ -699,12 +699,6 @@ As a first step, you need to bond a worker to your stake by running:
"""
#
# Felix
#
FELIX_RUN_MESSAGE = "Running Felix on {host}:{port}"
#
# Ursula
#

View File

@ -22,7 +22,6 @@ from nucypher.cli.commands import (
bob,
dao,
enrico,
felix,
multisig,
stake,
status,
@ -84,7 +83,6 @@ ENTRY_POINTS = (
status.status, # Network Status
dao.dao, # NuCypher DAO
multisig.multisig, # MultiSig operations
felix.felix, # Faucet
cloudworkers.cloudworkers, # Remote Worker node management
contacts.contacts, # Character "card" management
porter.porter

View File

@ -15,6 +15,7 @@ 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 click
import maya
from constant_sorrow.constants import NO_KEYSTORE_ATTACHED
@ -87,14 +88,8 @@ Path to Keystore: {new_configuration.keystore_dir}
emitter.message(f'* NOTE: for a non-default configuration filepath use `--config-file "{filepath}"` '
f'with subsequent `{character_name}` CLI commands', color='yellow')
# Felix
if character_name == 'felix':
hint = '''
To initialize a new faucet recipient database run: nucypher felix createdb
'''
# Ursula
elif character_name == 'ursula':
if character_name == 'ursula':
hint = '''
* Review configuration -> nucypher ursula config
* Start working -> nucypher ursula run
@ -120,6 +115,7 @@ To initialize a new faucet recipient database run: nucypher felix createdb
else:
raise ValueError(f'Unknown character type "{character_name}"')
emitter.echo(hint, color='green')

View File

@ -243,46 +243,6 @@ class BobConfiguration(CharacterConfiguration):
return {**super().static_payload(), **payload}
class FelixConfiguration(CharacterConfiguration):
from nucypher.characters.chaotic import Felix
# Character
CHARACTER_CLASS = Felix
NAME = CHARACTER_CLASS.__name__.lower()
DEFAULT_DB_NAME = '{}.db'.format(NAME)
DEFAULT_REST_PORT = 6151
DEFAULT_LEARNER_PORT = 9151
DEFAULT_REST_HOST = LOOPBACK_ADDRESS
__DEFAULT_TLS_CURVE = ec.SECP384R1
def __init__(self,
db_filepath: Optional[Path] = None,
rest_host: str = None,
rest_port: int = None,
tls_curve: EllipticCurve = None,
certificate: Certificate = None,
*args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if not rest_port:
rest_port = self.DEFAULT_REST_PORT
self.rest_port = rest_port or self.DEFAULT_REST_PORT
self.rest_host = rest_host or self.DEFAULT_REST_HOST
self.tls_curve = tls_curve or self.__DEFAULT_TLS_CURVE
self.certificate = certificate
self.db_filepath = db_filepath or self.config_root / self.DEFAULT_DB_NAME
def static_payload(self) -> dict:
payload = dict(
rest_host=self.rest_host,
rest_port=self.rest_port,
db_filepath=self.db_filepath.absolute(),
signer_uri=self.signer_uri
)
return {**super().static_payload(), **payload}
class StakeHolderConfiguration(CharacterConfiguration):
NAME = 'stakeholder'

View File

@ -1,182 +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
from unittest import mock
import os
import pytest_twisted
from twisted.internet import threads
from twisted.internet.task import Clock
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.actors import Staker
from nucypher.blockchain.eth.registry import LocalContractRegistry
from nucypher.blockchain.eth.token import NU
from nucypher.characters.chaotic import Felix
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import FelixConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN
from tests.constants import (INSECURE_DEVELOPMENT_PASSWORD, MOCK_CUSTOM_INSTALLATION_PATH_2, TEST_PROVIDER_URI)
@mock.patch('nucypher.config.characters.FelixConfiguration.default_filepath', return_value=Path('/non/existent/file'))
def test_missing_configuration_file(default_filepath_mock, click_runner):
cmd_args = ('felix', 'view')
result = click_runner.invoke(nucypher_cli, cmd_args, catch_exceptions=False)
assert result.exit_code != 0
assert default_filepath_mock.called
assert "nucypher felix init" in result.output # TODO: Move install hints to a constants
@pytest_twisted.inlineCallbacks
def test_run_felix(click_runner, testerchain, agency_local_registry):
clock = Clock()
Felix._CLOCK = clock
Felix.DISTRIBUTION_INTERVAL = 5 # seconds
Felix.DISBURSEMENT_INTERVAL = 0.01 # hours
Felix.STAGING_DELAY = 2 # seconds
# Main thread (Flask)
os.environ['NUCYPHER_FELIX_DB_SECRET'] = INSECURE_DEVELOPMENT_PASSWORD
# Test subproc (Click)
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
'NUCYPHER_FELIX_DB_SECRET': INSECURE_DEVELOPMENT_PASSWORD,
'NUCYPHER_WORKER_ETH_PASSWORD': INSECURE_DEVELOPMENT_PASSWORD,
'FLASK_DEBUG': '1'}
# Felix creates a system configuration
init_args = ('felix', 'init',
'--debug',
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--checksum-address', testerchain.client.accounts[0],
'--config-root', str(MOCK_CUSTOM_INSTALLATION_PATH_2.absolute()),
'--provider', TEST_PROVIDER_URI)
_original_read_function = LocalContractRegistry.read
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
configuration_file_location = MOCK_CUSTOM_INSTALLATION_PATH_2 / FelixConfiguration.generate_filename()
# Felix Creates a Database
db_args = ('felix', 'createdb',
'--debug',
'--config-file', str(configuration_file_location.absolute()),
'--provider', TEST_PROVIDER_URI)
result = click_runner.invoke(nucypher_cli, db_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
# Felix Runs Web Services
def run_felix():
args = ('felix', 'run',
'--debug',
'--config-file', str(configuration_file_location.absolute()),
'--provider', TEST_PROVIDER_URI,
'--dry-run')
run_result = click_runner.invoke(nucypher_cli, args, catch_exceptions=False, env=envvars)
assert run_result.exit_code == 0
return run_result
# A (mocked) client requests Felix's services
def request_felix_landing_page(_result):
# Init an equal Felix to the already running one.
felix_config = FelixConfiguration.from_configuration_file(filepath=configuration_file_location,
registry_filepath=agency_local_registry.filepath)
felix_config.keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
felix = felix_config.produce()
# Make a flask app
web_app = felix.make_web_app()
test_client = web_app.test_client()
# Register a new recipient
response = test_client.post('/register', data={'address': testerchain.client.accounts[-1]})
assert response.status_code == 200
return
def time_travel(_result):
clock.advance(amount=60)
# Record starting ether balance
recipient = testerchain.client.accounts[-1]
staker_power = TransactingPower(account=recipient, signer=Web3Signer(testerchain.client))
staker = Staker(registry=agency_local_registry,
domain=TEMPORARY_DOMAIN,
transacting_power=staker_power)
original_eth_balance = staker.eth_balance
# Run the callbacks
d = threads.deferToThread(run_felix)
d.addCallback(request_felix_landing_page)
d.addCallback(time_travel)
yield d
def confirm_airdrop(_results):
recipient = testerchain.client.accounts[-1]
staker = Staker(registry=agency_local_registry,
domain=TEMPORARY_DOMAIN,
transacting_power=staker_power)
assert staker.token_balance == NU(45000, 'NU')
# TODO: Airdrop Testnet Ethers?
new_eth_balance = original_eth_balance + testerchain.w3.fromWei(Felix.ETHER_AIRDROP_AMOUNT, 'ether')
assert staker.eth_balance == new_eth_balance
staged_airdrops = Felix._AIRDROP_QUEUE
next_airdrop = staged_airdrops[0]
next_airdrop.addCallback(confirm_airdrop)
yield next_airdrop
# Felix view
view_args = ('felix', 'view',
'--config-file', str(configuration_file_location.absolute()),
'--provider', TEST_PROVIDER_URI)
result = click_runner.invoke(nucypher_cli, view_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert "Address" in result.output
assert "NU" in result.output
assert "ETH" in result.output
# Felix accounts
accounts_args = ('felix', 'accounts',
'--config-file', str(configuration_file_location.absolute()),
'--provider', TEST_PROVIDER_URI)
result = click_runner.invoke(nucypher_cli, accounts_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert testerchain.client.accounts[-1] in result.output
# Felix destroy
destroy_args = ('felix', 'destroy',
'--config-file', str(configuration_file_location.absolute()),
'--provider', TEST_PROVIDER_URI,
'--force')
result = click_runner.invoke(nucypher_cli, destroy_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert SUCCESSFUL_DESTRUCTION in result.output
assert not configuration_file_location.exists(), "Felix configuration file was deleted"

View File

@ -23,7 +23,7 @@ import pytest
from nucypher.blockchain.eth.actors import Worker
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import AliceConfiguration, FelixConfiguration, UrsulaConfiguration
from nucypher.config.characters import AliceConfiguration, UrsulaConfiguration
from nucypher.config.constants import (
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
TEMPORARY_DOMAIN,
@ -77,7 +77,7 @@ def test_coexisting_configurations(click_runner,
# Parse node addresses
# TODO: Is testerchain & Full contract deployment needed here (causes massive slowdown)?
alice, ursula, another_ursula, felix, staker, *all_yall = testerchain.unassigned_accounts
alice, ursula, another_ursula, staker, *all_yall = testerchain.unassigned_accounts
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
@ -106,26 +106,9 @@ def test_coexisting_configurations(click_runner,
#
# Expected config files
felix_file_location = custom_filepath / FelixConfiguration.generate_filename()
alice_file_location = custom_filepath / AliceConfiguration.generate_filename()
ursula_file_location = custom_filepath / UrsulaConfiguration.generate_filename()
# Felix creates a system configuration
felix_init_args = ('felix', 'init',
'--config-root', str(custom_filepath.absolute()),
'--network', TEMPORARY_DOMAIN,
'--provider', TEST_PROVIDER_URI,
'--checksum-address', felix,
'--registry-filepath', str(agency_local_registry.filepath.absolute()),
'--debug')
result = click_runner.invoke(nucypher_cli, felix_init_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
# All configuration files still exist.
assert custom_filepath.is_dir()
assert felix_file_location.is_file()
# Use a custom local filepath to init a persistent Alice
alice_init_args = ('alice', 'init',
'--network', TEMPORARY_DOMAIN,
@ -138,7 +121,6 @@ def test_coexisting_configurations(click_runner,
assert result.exit_code == 0
# All configuration files still exist.
assert felix_file_location.is_file()
assert alice_file_location.is_file()
# Use the same local filepath to init a persistent Ursula
@ -154,7 +136,6 @@ def test_coexisting_configurations(click_runner,
assert result.exit_code == 0, result.output
# All configuration files still exist.
assert felix_file_location.is_file()
assert alice_file_location.is_file()
assert ursula_file_location.is_file()
@ -174,7 +155,6 @@ def test_coexisting_configurations(click_runner,
assert result.exit_code == 0
# All configuration files still exist.
assert felix_file_location.is_file()
assert alice_file_location.is_file()
kid = key_spy.spy_return.id[:8]
@ -204,7 +184,6 @@ def test_coexisting_configurations(click_runner,
Worker.READY_TIMEOUT = None
# All configuration files still exist.
assert felix_file_location.is_file()
assert alice_file_location.is_file()
assert another_ursula_configuration_file_location.is_file()
assert ursula_file_location.is_file()
@ -234,11 +213,6 @@ def test_coexisting_configurations(click_runner,
assert result.exit_code == 0
assert not alice_file_location.is_file()
felix_destruction_args = ('felix', 'destroy', '--force', '--config-file', str(felix_file_location.absolute()))
result = click_runner.invoke(nucypher_cli, felix_destruction_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert not felix_file_location.is_file()
def test_corrupted_configuration(click_runner,
custom_filepath,
@ -254,7 +228,7 @@ def test_corrupted_configuration(click_runner,
shutil.rmtree(custom_filepath, ignore_errors=True)
assert not custom_filepath.exists()
alice, ursula, another_ursula, felix, staker, *all_yall = testerchain.unassigned_accounts
alice, ursula, another_ursula, staker, *all_yall = testerchain.unassigned_accounts
#
# Chaos

View File

@ -171,7 +171,7 @@ def test_persistent_node_storage_integration(click_runner,
blockchain_ursulas,
agency_local_registry):
alice, ursula, another_ursula, felix, staker, *all_yall = testerchain.unassigned_accounts
alice, ursula, another_ursula, staker, *all_yall = testerchain.unassigned_accounts
filename = UrsulaConfiguration.generate_filename()
another_ursula_configuration_file_location = custom_filepath / filename

View File

@ -15,17 +15,14 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
from pathlib import Path
from unittest.mock import Mock
import pytest
import tempfile
from pathlib import Path
import pytest
from constant_sorrow.constants import CERTIFICATE_NOT_SAVED, NO_KEYSTORE_ATTACHED
from nucypher_core.umbral import SecretKey
from nucypher.blockchain.eth.actors import StakeHolder
from nucypher.characters.chaotic import Felix
from nucypher.characters.lawful import Alice, Bob, Ursula
from nucypher.cli.actions.configure import destroy_configuration
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION
@ -33,7 +30,6 @@ from nucypher.config.base import CharacterConfiguration
from nucypher.config.characters import (
AliceConfiguration,
BobConfiguration,
FelixConfiguration,
StakeHolderConfiguration,
UrsulaConfiguration
)
@ -48,13 +44,13 @@ configurations = (AliceConfiguration, BobConfiguration, UrsulaConfiguration)
characters = (Alice, Bob, Ursula)
# Auxiliary Support
blockchain_only_configurations = (FelixConfiguration, StakeHolderConfiguration)
blockchain_only_characters = (Felix, StakeHolder)
blockchain_only_configurations = (StakeHolderConfiguration, )
blockchain_only_characters = (StakeHolder, )
# Assemble
characters_and_configurations = list(zip(characters, configurations))
all_characters = tuple(characters + blockchain_only_characters)
all_configurations = tuple(configurations + blockchain_only_configurations)
all_characters = tuple(characters, )
all_configurations = tuple(configurations, )
@pytest.mark.parametrize("character,configuration", characters_and_configurations)