mirror of https://github.com/nucypher/nucypher.git
commit
eff28ee328
3
Pipfile
3
Pipfile
|
@ -40,9 +40,6 @@ appdirs = "*"
|
|||
click = ">=7.0"
|
||||
colorama = "*"
|
||||
tabulate = "*"
|
||||
# Felix
|
||||
flask_sqlalchemy = "*"
|
||||
sqlalchemy = "*"
|
||||
|
||||
[dev-packages]
|
||||
# Pytest
|
||||
|
|
|
@ -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') }}"
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
Retires and removes eth/token faucet.
|
|
@ -94,18 +94,6 @@ the Untrusted Re-Encryption Proxy.
|
|||
{}
|
||||
'''
|
||||
|
||||
FELIX_BANNER = r"""
|
||||
|
||||
'||''''| '||`
|
||||
|| . || ''
|
||||
||''| .|''|, || || \\ //
|
||||
|| ||..|| || || ><
|
||||
.||. `|... .||. .||. // \\
|
||||
|
||||
|
||||
the Unlicensed Faucet Plumber
|
||||
{}
|
||||
"""
|
||||
|
||||
STAKEHOLDER_BANNER = r"""
|
||||
____ __ __
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue