Merge pull request #1446 from cygnusv/csv

Touchups to the CLI allocation process: support for CSV, bugfixes, etc.
pull/1458/head
David Núñez 2019-11-12 01:08:25 +01:00 committed by GitHub
commit 6ea77a393b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 46 deletions

View File

@ -16,6 +16,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import csv
import json
import os
from datetime import datetime
@ -31,7 +32,7 @@ from constant_sorrow.constants import (
NO_WORKER_ASSIGNED,
)
from eth_tester.exceptions import TransactionFailed
from eth_utils import keccak, is_checksum_address
from eth_utils import keccak, is_checksum_address, to_checksum_address
from twisted.logger import Logger
from nucypher.blockchain.economics import TokenEconomics, StandardTokenEconomics, TokenEconomicsFactory
@ -383,13 +384,20 @@ class ContractAdministrator(NucypherTokenActor):
emitter: StdoutEmitter = None,
) -> Dict[str, dict]:
"""
The allocation file is a JSON file containing a list of allocations. Each allocation has a:
The allocation file contains a list of allocations, each of them composed of:
* 'beneficiary_address': Checksum address of the beneficiary
* 'name': User-friendly name of the beneficiary (Optional)
* 'amount': Amount of tokens locked, in NuNits
* 'duration_seconds': Lock duration expressed in seconds
Example allocation file:
It accepts both CSV and JSON formats. Example allocation file in CSV format:
"beneficiary_address","name","amount","duration_seconds"
"0xdeadbeef","H. E. Pennypacker",100,31536000
"0xabced120","",133432,31536000
"0xf7aefec2","",999,31536000
Example allocation file in JSON format:
[ {'beneficiary_address': '0xdeadbeef', 'name': 'H. E. Pennypacker', 'amount': 100, 'duration_seconds': 31536000},
{'beneficiary_address': '0xabced120', 'amount': 133432, 'duration_seconds': 31536000},
@ -434,6 +442,12 @@ class ContractAdministrator(NucypherTokenActor):
if emitter:
emitter.echo(f"Saved allocation template file to {template_filepath}", color='blue', bold=True)
already_enrolled = [a['beneficiary_address'] for a in allocations
if allocation_registry.is_beneficiary_enrolled(a['beneficiary_address'])]
if already_enrolled:
raise ValueError(f"The following beneficiaries are already enrolled in allocation registry "
f"({allocation_registry.filepath}): {already_enrolled}")
# Deploy each allocation contract
with click.progressbar(length=total_deployment_transactions,
label="Allocation progress",
@ -455,25 +469,29 @@ class ContractAdministrator(NucypherTokenActor):
bar._last_line = None
bar.render_progress()
deployer = self.deploy_preallocation_escrow(allocation_registry=allocation_registry,
progress=bar)
amount = allocation['amount']
duration = allocation['duration_seconds']
try:
receipts = deployer.deliver(value=amount,
duration=duration,
beneficiary_address=beneficiary,
progress=bar)
deployer = self.deploy_preallocation_escrow(allocation_registry=allocation_registry,
progress=bar)
deployer.deliver(value=amount,
duration=duration,
beneficiary_address=beneficiary,
progress=bar)
except TransactionFailed as e:
if crash_on_failure:
raise
self.log.debug(f"Failed allocation transaction for {NU.from_nunits(amount)} to {beneficiary}: {e}")
message = f"Failed allocation transaction for {NU.from_nunits(amount)} to {beneficiary}: {e}"
self.log.debug(message)
if emitter:
emitter.echo(message=message, color='red', bold=True)
failed.append(allocation)
continue
else:
allocation_receipts[beneficiary] = receipts
allocation_receipts[beneficiary] = deployer.deployment_receipts
allocation_contract_address = deployer.contract_address
self.log.info(f"Created {deployer.contract_name} contract at {allocation_contract_address} "
f"for beneficiary {beneficiary}.")
@ -492,7 +510,7 @@ class ContractAdministrator(NucypherTokenActor):
if emitter:
blockchain = BlockchainInterfaceFactory.get_interface()
paint_contract_deployment(contract_name=deployer.contract_name,
receipts=receipts,
receipts=deployer.deployment_receipts,
contract_address=deployer.contract_address,
emitter=emitter,
chain_name=blockchain.client.chain_name,
@ -503,14 +521,13 @@ class ContractAdministrator(NucypherTokenActor):
if emitter:
paint_deployed_allocations(emitter, allocated, failed)
csv_filename = f'allocations-{self.deployer_address[:6]}-{maya.now().epoch}.csv'
csv_filename = f'allocation-summary-{self.deployer_address[:6]}-{maya.now().epoch}.csv'
csv_filepath = os.path.join(parent_path, csv_filename)
write_deployed_allocations_to_csv(csv_filepath, allocated, failed)
if emitter:
emitter.echo(f"Saved allocation summary CSV to {csv_filepath}", color='blue', bold=True)
if failed:
# TODO: More with these failures: send to isolated logfile, and reattempt
self.log.critical(f"FAILED TOKEN ALLOCATION - {len(failed)} allocations failed.")
return allocation_receipts
@ -518,11 +535,18 @@ class ContractAdministrator(NucypherTokenActor):
@staticmethod
def __read_allocation_data(filepath: str) -> list:
with open(filepath, 'r') as allocation_file:
data = allocation_file.read()
try:
allocation_data = json.loads(data)
except JSONDecodeError:
raise
if filepath.endswith(".csv"):
reader = csv.DictReader(allocation_file)
allocation_data = list(reader)
else: # Assume it's JSON by default
allocation_data = json.load(allocation_file)
# Pre-process allocation data
for entry in allocation_data:
entry['beneficiary_address'] = to_checksum_address(entry['beneficiary_address'])
entry['amount'] = int(entry['amount'])
entry['duration_seconds'] = int(entry['duration_seconds'])
return allocation_data
def deploy_beneficiaries_from_file(self,
@ -535,7 +559,8 @@ class ContractAdministrator(NucypherTokenActor):
receipts = self.deploy_beneficiary_contracts(allocations=allocations,
allocation_outfile=allocation_outfile,
emitter=emitter,
interactive=interactive)
interactive=interactive,
crash_on_failure=False)
# Save transaction metadata
receipts_filepath = self.save_deployment_receipts(receipts=receipts, filename_prefix='allocation')
if emitter:
@ -549,12 +574,12 @@ class ContractAdministrator(NucypherTokenActor):
os.makedirs(DEFAULT_CONFIG_ROOT, exist_ok=True)
with open(filepath, 'w') as file:
data = dict()
for contract_name, receipts in receipts.items():
for contract_name, contract_receipts in receipts.items():
contract_records = dict()
for tx_name, receipt in receipts.items():
for tx_name, receipt in contract_receipts.items():
# Formatting
receipt = {item: str(result) for item, result in receipt.items()}
contract_records.update({tx_name: receipt for tx_name in receipts})
pretty_receipt = {item: str(result) for item, result in receipt.items()}
contract_records[tx_name] = pretty_receipt
data[contract_name] = contract_records
data = json.dumps(data, indent=4)
file.write(data)

View File

@ -16,6 +16,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from collections import OrderedDict
from typing import Tuple, Dict
from constant_sorrow.constants import CONTRACT_NOT_DEPLOYED, NO_DEPLOYER_CONFIGURED, NO_BENEFICIARY
@ -73,7 +74,7 @@ class BaseContractDeployer:
# Defaults
#
self.registry = registry
self.deployment_receipts = dict()
self.deployment_receipts = OrderedDict()
self._contract = CONTRACT_NOT_DEPLOYED
self.__proxy_contract = NotImplemented
self.__deployer_address = deployer_address
@ -794,7 +795,7 @@ class PreallocationEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin
agency = PreallocationEscrowAgent
contract_name = agency.registry_contract_name
deployment_steps = ('contract_deployment', )
deployment_steps = ('contract_deployment', 'approve_transfer', 'initial_deposit', 'transfer_ownership')
_router_deployer = StakingInterfaceRouterDeployer
__allocation_registry = AllocationRegistry
@ -828,16 +829,18 @@ class PreallocationEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin
sender_address=self.deployer_address,
payload=payload)
self.__beneficiary_address = checksum_address
self.deployment_receipts.update({self.deployment_steps[3]: transfer_owner_receipt})
return transfer_owner_receipt
def initial_deposit(self, value: int, duration_seconds: int, progress=None) -> dict:
def initial_deposit(self, value: int, duration_seconds: int, progress=None):
"""Allocate an amount of tokens with lock time in seconds, and transfer ownership to the beneficiary"""
# Approve
allocation_receipts = dict()
approve_function = self.token_contract.functions.approve(self.contract.address, value)
approve_function = self.token_contract.functions.approve(self.contract.address, value)
approve_receipt = self.blockchain.send_transaction(contract_function=approve_function,
sender_address=self.deployer_address) # TODO: Gas
allocation_receipts['approve'] = approve_receipt
self.deployment_receipts.update({self.deployment_steps[1]: approve_receipt})
if progress:
progress.update(1)
# Deposit
@ -848,13 +851,10 @@ class PreallocationEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin
sender_address=self.deployer_address,
payload=args)
# TODO: Do something with allocation_receipts. Perhaps it should be returned instead of only the last receipt.
allocation_receipts['initial_deposit'] = deposit_receipt
self.deployment_receipts.update({self.deployment_steps[2]: deposit_receipt})
if progress:
progress.update(1)
return deposit_receipt
def enroll_principal_contract(self):
if self.__beneficiary_address is NO_BENEFICIARY:
raise self.ContractDeploymentError("No beneficiary assigned to {}".format(self.contract.address))
@ -863,7 +863,7 @@ class PreallocationEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin
contract_abi=self.contract.abi)
@validate_checksum_address
def deliver(self, value: int, duration: int, beneficiary_address: str, progress=None) -> dict:
def deliver(self, value: int, duration: int, beneficiary_address: str, progress=None):
"""
Transfer allocated tokens and hand-off the contract to the beneficiary.
@ -874,12 +874,11 @@ class PreallocationEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin
"""
deposit_receipt = self.initial_deposit(value=value, duration_seconds=duration, progress=progress)
assign_receipt = self.assign_beneficiary(checksum_address=beneficiary_address)
self.initial_deposit(value=value, duration_seconds=duration, progress=progress)
self.assign_beneficiary(checksum_address=beneficiary_address)
if progress:
progress.update(1)
self.enroll_principal_contract()
return dict(deposit_receipt=deposit_receipt, assign_receipt=assign_receipt)
def deploy(self, initial_deployment: bool = True, gas_limit: int = None, progress=None) -> dict:
"""Deploy a new instance of PreallocationEscrow to the blockchain."""
@ -892,12 +891,14 @@ class PreallocationEscrowDeployer(BaseContractDeployer, UpgradeableContractMixin
router_contract.address,
self.token_contract.address)
preallocation_escrow_contract, deploy_receipt = self.blockchain.deploy_contract(*args, gas_limit=gas_limit, enroll=False)
preallocation_escrow_contract, deploy_receipt = self.blockchain.deploy_contract(*args,
gas_limit=gas_limit,
enroll=False)
if progress:
progress.update(1)
self._contract = preallocation_escrow_contract
self.deployment_receipts.update({'deployment': deploy_receipt})
self.deployment_receipts.update({self.deployment_steps[0]: deploy_receipt})
return deploy_receipt
def get_contract_abi(self):

View File

@ -404,7 +404,7 @@ class AllocationRegistry(LocalContractRegistry):
try:
_ = self.search(beneficiary_address=beneficiary_address)
return True
except self.UnknownBeneficiary:
except (self.UnknownBeneficiary, self.NoAllocationRegistry):
return False
# TODO: Tests!

View File

@ -55,14 +55,12 @@ def test_deploy_and_allocate(agency, token_economics, test_registry):
assert token_agent.get_balance(address=origin) > 1
# Start allocating tokens
deposit_receipts, approve_hashes = list(), dict()
for address, deployer in deployments.items():
assert deployer.deployer_address == origin
deposit_receipt = deployer.initial_deposit(value=allocation, duration_seconds=token_economics.maximum_rewarded_periods)
deposit_receipts.append(deposit_receipt)
deployer.initial_deposit(value=allocation, duration_seconds=token_economics.maximum_rewarded_periods)
beneficiary = random.choice(testerchain.unassigned_accounts)
_assign_receipt = deployer.assign_beneficiary(beneficiary)
assert len(deposit_receipts) == number_of_deployments == len(deployments)
assert number_of_deployments == len(deployments)