Ejemplo n.º 1
0
def deploy(click_config, action, poa, provider_uri, geth, enode,
           deployer_address, contract_name, allocation_infile,
           allocation_outfile, registry_infile, registry_outfile, no_compile,
           amount, recipient_address, config_root, sync, force):
    """Manage contract and registry deployment"""

    ETH_NODE = None

    #
    # Validate
    #

    # Ensure config root exists, because we need a default place to put output files.
    config_root = config_root or DEFAULT_CONFIG_ROOT
    if not os.path.exists(config_root):
        os.makedirs(config_root)

    #
    # Connect to Blockchain
    #

    # Establish a contract registry from disk if specified
    registry, registry_filepath = None, (registry_outfile or registry_infile)
    if registry_filepath is not None:
        registry = EthereumContractRegistry(
            registry_filepath=registry_filepath)

    if geth:
        # Spawn geth child process
        ETH_NODE = NuCypherGethDevnetProcess(config_root=config_root)
        ETH_NODE.ensure_account_exists(password=click_config.get_password(
            confirm=True))
        if not ETH_NODE.initialized:
            ETH_NODE.initialize_blockchain()
        ETH_NODE.start()  # TODO: Graceful shutdown
        provider_uri = ETH_NODE.provider_uri

    # Deployment-tuned blockchain connection
    blockchain = Blockchain.connect(provider_uri=provider_uri,
                                    poa=poa,
                                    registry=registry,
                                    compile=not no_compile,
                                    deployer=True,
                                    fetch_registry=False,
                                    sync=sync)

    #
    # Deployment Actor
    #

    if not deployer_address:
        for index, address in enumerate(blockchain.interface.w3.eth.accounts):
            click.secho(f"{index} --- {address}")
        choices = click.IntRange(0, len(blockchain.interface.w3.eth.accounts))
        deployer_address_index = click.prompt("Select deployer address",
                                              default=0,
                                              type=choices)
        deployer_address = blockchain.interface.w3.eth.accounts[
            deployer_address_index]

    # Verify Address
    if not force:
        click.confirm("Selected {} - Continue?".format(deployer_address),
                      abort=True)
    deployer = Deployer(blockchain=blockchain,
                        deployer_address=deployer_address)

    # Verify ETH Balance
    click.secho(f"\n\nDeployer ETH balance: {deployer.eth_balance}")
    if deployer.eth_balance == 0:
        click.secho("Deployer address has no ETH.", fg='red', bold=True)
        raise click.Abort()

    if not blockchain.interface.is_local:
        # (~ dev mode; Assume accounts are already unlocked)
        password = click.prompt("Enter ETH node password", hide_input=True)
        blockchain.interface.w3.geth.personal.unlockAccount(
            deployer_address, password)

    # Add ETH Bootnode or Peer
    if enode:
        if geth:
            blockchain.interface.w3.geth.admin.addPeer(enode)
            click.secho(f"Added ethereum peer {enode}")
        else:
            raise NotImplemented  # TODO: other backends

    #
    # Action switch
    #

    if action == 'upgrade':
        if not contract_name:
            raise click.BadArgumentUsage(
                message="--contract-name is required when using --upgrade")
        existing_secret = click.prompt(
            'Enter existing contract upgrade secret', hide_input=True)
        new_secret = click.prompt('Enter new contract upgrade secret',
                                  hide_input=True,
                                  confirmation_prompt=True)
        deployer.upgrade_contract(contract_name=contract_name,
                                  existing_plaintext_secret=existing_secret,
                                  new_plaintext_secret=new_secret)

    elif action == 'rollback':
        existing_secret = click.prompt(
            'Enter existing contract upgrade secret', hide_input=True)
        new_secret = click.prompt('Enter new contract upgrade secret',
                                  hide_input=True,
                                  confirmation_prompt=True)
        deployer.rollback_contract(contract_name=contract_name,
                                   existing_plaintext_secret=existing_secret,
                                   new_plaintext_secret=new_secret)

    elif action == "contracts":

        registry_filepath = deployer.blockchain.interface.registry.filepath
        if os.path.isfile(registry_filepath):
            click.secho(
                f"\nThere is an existing contract registry at {registry_filepath}.\n"
                f"Did you mean 'nucypher-deploy upgrade'?\n",
                fg='yellow')
            click.confirm(
                "Optionally, destroy existing local registry and continue?",
                abort=True)
            click.confirm(
                f"Confirm deletion of contract registry '{registry_filepath}'?",
                abort=True)
            os.remove(registry_filepath)

        #
        # Deploy Single Contract
        #

        if contract_name:
            # TODO: Handle secret collection for single contract deployment

            try:
                deployer_func = deployer.deployers[contract_name]
            except KeyError:
                message = f"No such contract {contract_name}. Available contracts are {deployer.deployers.keys()}"
                click.secho(message, fg='red', bold=True)
                raise click.Abort()
            else:
                # Deploy single contract
                _txs, _agent = deployer_func()

            # TODO: Painting for single contract deployment
            if ETH_NODE:
                ETH_NODE.stop()
            return

        #
        # Stage Deployment
        #

        # Track tx hashes, and new agents
        __deployment_transactions = dict()
        __deployment_agents = dict()

        secrets = click_config.collect_deployment_secrets()
        click.clear()
        click.secho(NU_BANNER)

        w3 = deployer.blockchain.interface.w3
        click.secho(f"Current Time ........ {maya.now().iso8601()}")
        click.secho(
            f"Web3 Provider ....... {deployer.blockchain.interface.provider_uri}"
        )
        click.secho(f"Block ............... {w3.eth.blockNumber}")
        click.secho(f"Gas Price ........... {w3.eth.gasPrice}")

        click.secho(f"Deployer Address .... {deployer.checksum_address}")
        click.secho(f"ETH ................. {deployer.eth_balance}")
        click.secho(
            f"CHAIN ID............. {deployer.blockchain.interface.chain_id}")
        click.secho(
            f"CHAIN................ {deployer.blockchain.interface.chain_name}"
        )

        # Ask - Last chance to gracefully abort
        if not force:
            click.secho(
                "\nDeployment successfully staged. Take a deep breath. \n",
                fg='green')
            if click.prompt("Type 'DEPLOY' to continue") != 'DEPLOY':
                raise click.Abort()

        # Delay - Last chance to crash and abort
        click.secho(f"Starting deployment in 3 seconds...", fg='red')
        time.sleep(1)
        click.secho(f"2...", fg='yellow')
        time.sleep(1)
        click.secho(f"1...", fg='green')
        time.sleep(1)
        click.secho(f"Deploying...", bold=True)

        #
        # DEPLOY < -------
        #

        txhashes, deployers = deployer.deploy_network_contracts(
            miner_secret=secrets.miner_secret,
            policy_secret=secrets.policy_secret,
            adjudicator_secret=secrets.mining_adjudicator_secret,
            user_escrow_proxy_secret=secrets.escrow_proxy_secret)

        # Success
        __deployment_transactions.update(txhashes)

        #
        # Paint
        #

        total_gas_used = 0  # TODO: may be faulty
        for contract_name, transactions in __deployment_transactions.items():

            # Paint heading
            heading = '\n{} ({})'.format(
                contract_name, deployers[contract_name].contract_address)
            click.secho(heading, bold=True)
            click.echo('*' * (42 + 3 + len(contract_name)))

            for tx_name, txhash in transactions.items():

                # Wait for inclusion in the blockchain
                try:
                    receipt = deployer.blockchain.wait_for_receipt(
                        txhash=txhash)
                except TimeExhausted:
                    raise  # TODO: Option to wait longer or retry

                # Examine Receipt # TODO: This currently cannot receive failed transactions
                if receipt['status'] == 1:
                    click.secho("OK", fg='green', nl=False, bold=True)
                else:
                    click.secho("Failed", fg='red', nl=False, bold=True)

                # Accumulate gas
                total_gas_used += int(receipt['gasUsed'])

                # Paint
                click.secho(" | {}".format(tx_name), fg='yellow', nl=False)
                click.secho(" | {}".format(txhash.hex()),
                            fg='yellow',
                            nl=False)
                click.secho(" ({} gas)".format(receipt['cumulativeGasUsed']))
                click.secho("Block #{} | {}\n".format(
                    receipt['blockNumber'], receipt['blockHash'].hex()))

        # Paint outfile paths
        click.secho(
            "Cumulative Gas Consumption: {} gas".format(total_gas_used),
            bold=True,
            fg='blue')
        registry_outfile = deployer.blockchain.interface.registry.filepath
        click.secho('Generated registry {}'.format(registry_outfile),
                    bold=True,
                    fg='blue')

        # Save transaction metadata
        receipts_filepath = deployer.save_deployment_receipts(
            transactions=__deployment_transactions)
        click.secho(f"Saved deployment receipts to {receipts_filepath}",
                    fg='blue',
                    bold=True)

        #
        # Publish Contract Registry
        #

        if not deployer.blockchain.interface.is_local:
            if click.confirm("Publish new contract registry?"):
                try:
                    response = registry.publish(
                    )  # TODO: Handle non-200 response and dehydrate
                except EthereumContractRegistry.RegistryError as e:
                    click.secho("Registry publication failed.",
                                fg='red',
                                bold=True)
                    click.secho(str(e))
                    raise click.Abort()
                click.secho(f"Published new contract registry.", fg='green')

    elif action == "allocations":
        if not allocation_infile:
            allocation_infile = click.prompt("Enter allocation data filepath")
        click.confirm("Continue deploying and allocating?", abort=True)
        deployer.deploy_beneficiaries_from_file(
            allocation_data_filepath=allocation_infile,
            allocation_outfile=allocation_outfile)

    elif action == "transfer":
        token_agent = NucypherTokenAgent(blockchain=blockchain)
        click.confirm(
            f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?",
            abort=True)
        txhash = token_agent.transfer(
            amount=amount,
            sender_address=token_agent.contract_address,
            target_address=recipient_address)
        click.secho(f"OK | {txhash}")

    elif action == "publish-registry":
        registry = deployer.blockchain.interface.registry
        click.confirm(
            f"Publish {registry.filepath} to GitHub (Authentication Required)?",
            abort=True)
        try:
            response = registry.publish(
            )  # TODO: Handle non-200 response and dehydrate
        except EthereumContractRegistry.RegistryError as e:
            click.secho(str(e))
            raise click.Abort()
        click.secho(f"Published new contract registry.", fg='green')

    elif action == "destroy-registry":
        registry_filepath = deployer.blockchain.interface.registry.filepath
        click.confirm(
            f"Are you absolutely sure you want to destroy the contract registry at {registry_filepath}?",
            abort=True)
        os.remove(registry_filepath)
        click.secho(f"Successfully destroyed {registry_filepath}", fg='red')

    else:
        raise click.BadArgumentUsage(message=f"Unknown action '{action}'")

    if ETH_NODE:
        ETH_NODE.stop()
Ejemplo n.º 2
0
def deploy(action, poa, etherscan, provider_uri, gas, deployer_address,
           contract_name, allocation_infile, allocation_outfile,
           registry_infile, registry_outfile, amount, recipient_address,
           config_root, hw_wallet, force, dev):
    """
    Manage contract and registry deployment.

    \b
    Actions
    -----------------------------------------------------------------------------
    contracts              Compile and deploy contracts.
    allocations            Deploy pre-allocation contracts.
    upgrade                Upgrade NuCypher existing proxy contract deployments.
    rollback               Rollback a proxy contract's target.
    status                 Echo owner information and bare contract metadata.
    transfer-tokens        Transfer tokens to another address.
    transfer-ownership     Transfer ownership of contracts to another address.
    """

    emitter = StdoutEmitter()

    #
    # Validate
    #

    # Ensure config root exists, because we need a default place to put output files.
    config_root = config_root or DEFAULT_CONFIG_ROOT
    if not os.path.exists(config_root):
        os.makedirs(config_root)

    #
    # Pre-Launch Warnings
    #

    if not hw_wallet:
        emitter.echo("WARNING: --no-hw-wallet is enabled.", color='yellow')

    if etherscan:
        emitter.echo(
            "WARNING: --etherscan is enabled. "
            "A browser tab will be opened with deployed contracts and TXs as provided by Etherscan.",
            color='yellow')
    else:
        emitter.echo(
            "WARNING: --etherscan is disabled. "
            "If you want to see deployed contracts and TXs in your browser, activate --etherscan.",
            color='yellow')

    #
    # Connect to Registry
    #

    # Establish a contract registry from disk if specified
    registry_filepath = registry_outfile or registry_infile
    if dev:
        # TODO: Need a way to detect a geth--dev registry filepath here. (then deprecate the --dev flag)
        registry_filepath = os.path.join(DEFAULT_CONFIG_ROOT,
                                         'dev_contract_registry.json')
    registry = EthereumContractRegistry(registry_filepath=registry_filepath)
    emitter.echo(f"Using contract registry filepath {registry.filepath}")

    #
    # Connect to Blockchain
    #

    blockchain = BlockchainDeployerInterface(provider_uri=provider_uri,
                                             poa=poa,
                                             registry=registry)
    try:
        blockchain.connect(fetch_registry=False, sync_now=False)
    except BlockchainDeployerInterface.ConnectionFailed as e:
        emitter.echo(str(e), color='red', bold=True)
        raise click.Abort()

    #
    # Make Authenticated Deployment Actor
    #

    # Verify Address & collect password
    if not deployer_address:
        prompt = "Select deployer account"
        deployer_address = select_client_account(emitter=emitter,
                                                 blockchain=blockchain,
                                                 prompt=prompt)

    if not force:
        click.confirm("Selected {} - Continue?".format(deployer_address),
                      abort=True)

    password = None
    if not hw_wallet and not blockchain.client.is_local:
        password = get_client_password(checksum_address=deployer_address)

    # Produce Actor
    DEPLOYER = DeployerActor(blockchain=blockchain,
                             client_password=password,
                             deployer_address=deployer_address)

    # Verify ETH Balance
    emitter.echo(f"\n\nDeployer ETH balance: {DEPLOYER.eth_balance}")
    if DEPLOYER.eth_balance == 0:
        emitter.echo("Deployer address has no ETH.", color='red', bold=True)
        raise click.Abort()

    #
    # Action switch
    #

    if action == 'upgrade':
        if not contract_name:
            raise click.BadArgumentUsage(
                message="--contract-name is required when using --upgrade")
        existing_secret = click.prompt(
            'Enter existing contract upgrade secret', hide_input=True)
        new_secret = click.prompt('Enter new contract upgrade secret',
                                  hide_input=True,
                                  confirmation_prompt=True)
        DEPLOYER.upgrade_contract(contract_name=contract_name,
                                  existing_plaintext_secret=existing_secret,
                                  new_plaintext_secret=new_secret)
        return  # Exit

    elif action == 'rollback':
        if not contract_name:
            raise click.BadArgumentUsage(
                message="--contract-name is required when using --rollback")
        existing_secret = click.prompt(
            'Enter existing contract upgrade secret', hide_input=True)
        new_secret = click.prompt('Enter new contract upgrade secret',
                                  hide_input=True,
                                  confirmation_prompt=True)
        DEPLOYER.rollback_contract(contract_name=contract_name,
                                   existing_plaintext_secret=existing_secret,
                                   new_plaintext_secret=new_secret)
        return  # Exit

    elif action == "contracts":

        #
        # Deploy Single Contract (Amend Registry)
        #

        if contract_name:
            try:
                contract_deployer = DEPLOYER.deployers[contract_name]
            except KeyError:
                message = f"No such contract {contract_name}. Available contracts are {DEPLOYER.deployers.keys()}"
                emitter.echo(message, color='red', bold=True)
                raise click.Abort()
            else:
                emitter.echo(f"Deploying {contract_name}")
                if contract_deployer._upgradeable:
                    secret = DEPLOYER.collect_deployment_secret(
                        deployer=contract_deployer)
                    receipts, agent = DEPLOYER.deploy_contract(
                        contract_name=contract_name, plaintext_secret=secret)
                else:
                    receipts, agent = DEPLOYER.deploy_contract(
                        contract_name=contract_name, gas_limit=gas)
                paint_contract_deployment(
                    contract_name=contract_name,
                    contract_address=agent.contract_address,
                    receipts=receipts,
                    emitter=emitter,
                    chain_name=blockchain.client.chain_name,
                    open_in_browser=etherscan)
            return  # Exit

        #
        # Deploy Automated Series (Create Registry)
        #

        # Confirm filesystem registry writes.
        registry_filepath = DEPLOYER.blockchain.registry.filepath
        if os.path.isfile(registry_filepath):
            emitter.echo(
                f"\nThere is an existing contract registry at {registry_filepath}.\n"
                f"Did you mean 'nucypher-deploy upgrade'?\n",
                color='yellow')
            click.confirm("*DESTROY* existing local registry and continue?",
                          abort=True)
            os.remove(registry_filepath)

        # Stage Deployment
        secrets = DEPLOYER.collect_deployment_secrets()
        paint_staged_deployment(deployer=DEPLOYER, emitter=emitter)

        # Confirm Trigger Deployment
        if not actions.confirm_deployment(emitter=emitter, deployer=DEPLOYER):
            raise click.Abort()

        # Delay - Last chance to abort via KeyboardInterrupt
        paint_deployment_delay(emitter=emitter)

        # Execute Deployment
        deployment_receipts = DEPLOYER.deploy_network_contracts(
            secrets=secrets,
            emitter=emitter,
            interactive=not force,
            etherscan=etherscan)

        # Paint outfile paths
        registry_outfile = DEPLOYER.blockchain.registry.filepath
        emitter.echo('Generated registry {}'.format(registry_outfile),
                     bold=True,
                     color='blue')

        # Save transaction metadata
        receipts_filepath = DEPLOYER.save_deployment_receipts(
            receipts=deployment_receipts)
        emitter.echo(f"Saved deployment receipts to {receipts_filepath}",
                     color='blue',
                     bold=True)
        return  # Exit

    elif action == "allocations":
        if not allocation_infile:
            allocation_infile = click.prompt("Enter allocation data filepath")
        click.confirm("Continue deploying and allocating?", abort=True)
        DEPLOYER.deploy_beneficiaries_from_file(
            allocation_data_filepath=allocation_infile,
            allocation_outfile=allocation_outfile)
        return  # Exit

    elif action == "transfer":
        token_agent = NucypherTokenAgent(blockchain=blockchain)
        missing_options = list()
        if recipient_address is None:
            missing_options.append("--recipient-address")
        if amount is None:
            missing_options.append("--amount")
        if missing_options:
            raise click.BadOptionUsage(
                f"Need {' and '.join(missing_options)} to transfer tokens.")

        click.confirm(
            f"Transfer {amount} from {deployer_address} to {recipient_address}?",
            abort=True)
        receipt = token_agent.transfer(amount=amount,
                                       sender_address=deployer_address,
                                       target_address=recipient_address)
        emitter.echo(f"OK | Receipt: {receipt['transactionHash'].hex()}")
        return  # Exit

    else:
        raise click.BadArgumentUsage(message=f"Unknown action '{action}'")
Ejemplo n.º 3
0
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, BlockchainPower]

    TEMPLATE_NAME = 'felix.html'

    # Intervals
    DISTRIBUTION_INTERVAL = 60 * 60  # seconds (60*60=1Hr)
    DISBURSEMENT_INTERVAL = 24  # (24) hours
    STAGING_DELAY = 10  # seconds

    # Disbursement
    BATCH_SIZE = 10  # transactions
    MULTIPLIER = 0.95  # 5% reduction of previous stake is 0.95, for example
    MINIMUM_DISBURSEMENT = 1e18  # NuNits
    ETHER_AIRDROP_AMOUNT = int(2e18)  # Wei

    # 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: str,
                 rest_host: str,
                 rest_port: int,
                 crash_on_error: bool = False,
                 economics: TokenEconomics = None,
                 distribute_ether: bool = True,
                 *args,
                 **kwargs):

        # Character
        super().__init__(*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}',
                                       convert_unicode=True)

        # Blockchain
        self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
        self.reserved_addresses = [
            self.checksum_address, Blockchain.NULL_ADDRESS
        ]

        # Update reserved addresses with deployed contracts
        existing_entries = list(
            self.blockchain.interface.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

        if not economics:
            economics = TokenEconomics()
        self.economics = economics

        self.MAXIMUM_DISBURSEMENT = economics.maximum_allowed_locked
        self.INITIAL_DISBURSEMENT = economics.minimum_allowed_locked

        # 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 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}'
        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,
                                     unique=True,
                                     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
        limiter = Limiter(self.rest_app,
                          key_func=get_remote_address,
                          headers_enabled=True)

        #
        # REST Routes
        #

        @rest_app.route("/", methods=['GET'])
        @limiter.limit("100/day;20/hour;1/minute")
        def home():
            rendering = render_template(self.TEMPLATE_NAME)
            return rendering

        @rest_app.route("/register", methods=['POST'])
        @limiter.limit("5 per day")
        def register():
            """Handle new recipient registration via POST request."""
            try:
                new_address = request.form['address']
            except KeyError:
                return Response(status=400)  # TODO

            if not eth_utils.is_checksum_address(new_address):
                return Response(status=400)  # TODO

            if new_address in self.reserved_addresses:
                return Response(status=400)  # TODO

            try:
                with ThreadedSession(self.db_engine) as session:

                    existing = Recipient.query.filter_by(
                        address=new_address).all()
                    if existing:
                        # Address already exists; Abort
                        self.log.debug(f"{new_address} is already enrolled.")
                        return Response(status=400)

                    # 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=200)  # 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}
        deployer = HendrixDeploy(action="start", options=payload)
        click.secho(f"Running {self.__class__.__name__} on {host}:{port}")

        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) -> 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
        txhash = self.token_agent.transfer(
            amount=disbursement,
            target_address=recipient_address,
            sender_address=self.checksum_address)

        if self.distribute_ether:
            ether = self.ETHER_AIRDROP_AMOUNT
            transaction = {
                'to': recipient_address,
                'from': self.checksum_address,
                'value': ether,
                'gasPrice': self.blockchain.interface.w3.eth.gasPrice
            }
            ether_txhash = self.blockchain.interface.w3.eth.sendTransaction(
                transaction)

            self.log.info(
                f"Disbursement #{self.__disbursement} OK | NU {txhash.hex()[-6:]} | ETH {ether_txhash.hex()[:6]} "
                f"({str(NU(disbursement, 'NuNit'))} + {self.ETHER_AIRDROP_AMOUNT} wei) -> {recipient_address}"
            )

        else:
            self.log.info(
                f"Disbursement #{self.__disbursement} OK | {txhash.hex()[-6:]} |"
                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 is 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
Ejemplo n.º 4
0
def deploy(click_config, action, poa, provider_uri, deployer_address,
           contract_name, allocation_infile, allocation_outfile,
           registry_infile, registry_outfile, no_compile, amount,
           recipient_address, config_root, force):
    """Manage contract and registry deployment"""

    # Ensure config root exists, because we need a default place to put outfiles.
    config_root = config_root or DEFAULT_CONFIG_ROOT
    if not os.path.exists(config_root):
        os.makedirs(config_root)

    # Establish a contract Registry
    registry, registry_filepath = None, (registry_outfile or registry_infile)
    if registry_filepath is not None:
        registry = EthereumContractRegistry(
            registry_filepath=registry_filepath)

    # Connect to Blockchain
    blockchain = Blockchain.connect(provider_uri=provider_uri,
                                    registry=registry,
                                    deployer=True,
                                    compile=not no_compile,
                                    poa=poa)

    # OK - Let's init a Deployment actor
    if not deployer_address:
        etherbase = blockchain.interface.w3.eth.accounts[0]
        deployer_address = etherbase  # TODO: Make this required instead, perhaps interactive

    click.confirm(
        "Deployer Address is {} - Continue?".format(deployer_address),
        abort=True)
    deployer = Deployer(blockchain=blockchain,
                        deployer_address=deployer_address)

    # The Big Three
    if action == "contracts":
        secrets = click_config.collect_deployment_secrets()

        # Track tx hashes, and new agents
        __deployment_transactions = dict()
        __deployment_agents = dict()

        if force:
            deployer.blockchain.interface.registry._destroy()

        try:
            txhashes, agents = deployer.deploy_network_contracts(
                miner_secret=bytes(secrets.miner_secret, encoding='utf-8'),
                policy_secret=bytes(secrets.policy_secret, encoding='utf-8'),
                adjudicator_secret=bytes(secrets.mining_adjudicator_secret,
                                         encoding='utf-8'))
        except BlockchainInterface.InterfaceError:
            raise  # TODO: Handle registry management here (contract may already exist)
        else:
            __deployment_transactions.update(txhashes)

        # User Escrow Proxy
        deployer.deploy_escrow_proxy(
            secret=bytes(secrets.escrow_proxy_secret, encoding='utf-8'))
        click.secho("Deployed!", fg='green', bold=True)

        #
        # Deploy Single Contract
        #
        if contract_name:

            try:
                deployer_func = deployer.deployers[contract_name]
            except KeyError:
                message = "No such contract {}. Available contracts are {}".format(
                    contract_name, deployer.deployers.keys())
                click.secho(message, fg='red', bold=True)
                raise click.Abort()
            else:
                _txs, _agent = deployer_func()

        registry_outfile = deployer.blockchain.interface.registry.filepath
        click.secho(
            '\nDeployment Transaction Hashes for {}'.format(registry_outfile),
            bold=True,
            fg='blue')
        for contract_name, transactions in __deployment_transactions.items():

            heading = '\n{} ({})'.format(
                contract_name, agents[contract_name].contract_address)
            click.secho(heading, bold=True)
            click.echo('*' * (42 + 3 + len(contract_name)))

            total_gas_used = 0
            for tx_name, txhash in transactions.items():
                receipt = deployer.blockchain.wait_for_receipt(txhash=txhash)
                total_gas_used += int(receipt['gasUsed'])

                if receipt['status'] == 1:
                    click.secho("OK", fg='green', nl=False, bold=True)
                else:
                    click.secho("Failed", fg='red', nl=False, bold=True)
                click.secho(" | {}".format(tx_name), fg='yellow', nl=False)
                click.secho(" | {}".format(txhash.hex()),
                            fg='yellow',
                            nl=False)
                click.secho(" ({} gas)".format(receipt['cumulativeGasUsed']))

                click.secho("Block #{} | {}\n".format(
                    receipt['blockNumber'], receipt['blockHash'].hex()))

        click.secho(
            "Cumulative Gas Consumption: {} gas\n".format(total_gas_used),
            bold=True,
            fg='blue')

    elif action == "allocations":
        if not allocation_infile:
            allocation_infile = click.prompt("Enter allocation data filepath")
        click.confirm("Continue deploying and allocating?", abort=True)
        deployer.deploy_beneficiaries_from_file(
            allocation_data_filepath=allocation_infile,
            allocation_outfile=allocation_outfile)

    elif action == "transfer":
        token_agent = NucypherTokenAgent(blockchain=blockchain)
        click.confirm(
            f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?",
            abort=True)
        txhash = token_agent.transfer(
            amount=amount,
            sender_address=token_agent.contract_address,
            target_address=recipient_address)
        click.secho(f"OK | {txhash}")
        return

    elif action == "destroy-registry":
        registry_filepath = deployer.blockchain.interface.registry.filepath
        click.confirm(
            f"Are you absolutely sure you want to destroy the contract registry at {registry_filepath}?",
            abort=True)
        os.remove(registry_filepath)
        click.secho(f"Successfully destroyed {registry_filepath}", fg='red')

    else:
        raise click.BadArgumentUsage(message=f"Unknown action '{action}'")