def prepare_csv(stream, address_key, label_key) -> List[Entry]: """Process CSV reclaim file. Make sure all Ethereum addresses are valid. Filter out duplicates. :param token: Token contract :param owner: ETH account set as the owner of the token :param stream: File stream for CSV :param address_key: Column holding ETH address in the CSV :param label_key: Column holding human readable description of the address in CSV :return: Number of total reclaims performed """ reader = csv.DictReader(stream) rows = [row for row in reader] output_rows = [] uniq = set() # Prevalidate addresses # Here we do it inline and make skip addresses that are not valid. for idx, row in enumerate(rows): addr = row[address_key].strip() label = row[label_key].strip() if not addr: # Empty cell / row continue if not addr.startswith("0x"): addr = "0x" + addr try: if addr: validate_ethereum_address(addr) except ValueError as e: logger.error( "Invalid Ethereum address on row:%d address:%s label:%s reason:%s", idx + 1, addr, label, str(e)) continue if addr in uniq: logger.warn("Address has duplicates: %s", addr) continue uniq.add(addr) output_row = Entry(address=addr, label=label) output_rows.append(output_row) return output_rows
def reclaim_address(token: Contract, entry: Entry, tx_params: dict) -> Tuple[int, str]: """Reclsaim tokens for a single participant. :param token: Token contract we reclaim :param owner: Token owner account :param address: Etherereum address :param label: User notification label regarding this address :param tx_params: Ethereum transaction parameters to use :return: 1 on reclaim, 0 on skip """ # Make sure we are not fed bad input, raises validate_ethereum_address(entry.address) if token.call().balanceOf(entry.address) == 0: logger.info("%s: looks like already reclaimed %s", entry.address, entry.label) return 0, None txid = token.transact(tx_params).transferToOwner(entry.address) logger.info("%s: reclaiming %s in txid %s", entry.address, entry.label, txid) return 1, txid
def main(chain, address, token, csv_file, limit, start_from, issuer_address, address_column, amount_column, external_id_column, allow_addresless, master_address, gas_price, solc_version): """Distribute tokens to centrally issued crowdsale participant or bounty program participants using an external key. Reads in distribution data as CSV. Then uses Issuer contract to distribute tokens. All token counts are multiplied by token contract decimal specifier. E.g. if CSV has amount 15.5, token has 2 decimal places, we will issue out 1550 raw token amount. The external id uniquely identifies participants. This is different from the distribute-tokens where the Ethereum address uniquely identifies participants. To speed up the issuance, transactions are verified in batches. Each batch is 16 transactions at a time. First have a issuer contract created: distribute-tokens-ext-id \ --chain=mainnet \ --address=0xccba4928c4e9d10242788d9cf144d865348c6c7f \ --token=0x1a7a8bd9106f2b8d977e08582dc7d24c723ab0db \ --master-address=0xa684a3371e0d46bca4a6db1ff538a44f1440a855 \ --csv-file=data.csv \ --address-column="Ethereum address" \ --amount-column="Token amount" \ --external-id-column="External ID number" \ --solc-version="v0.4.16+commit.d7661dd9" \ --gas-price=100 Then perform EIP-20 approve() to give tokens to the issuer contract deployed in the last command. Then run the distribution: distribute-tokens-ext-id \ --chain=mainnet \ --address=0xccba4928c4e9d10242788d9cf144d865348c6c7f \ --token=0x1a7a8bd9106f2b8d977e08582dc7d24c723ab0db \ --master-address=0xa684a3371e0d46bca4a6db1ff538a44f1440a855 \ --issuer-address=0x60cfb02266310e66dd99b1635e702c519a564726 \ --csv-file=combined.csv \ --address-column="Address" \ --amount-column="Amount" \ --gas-price=70 \ --start-from=670 """ project = Project() with project.get_chain(chain) as c: web3 = c.web3 print("Web3 provider is", web3.currentProvider) print("Deployer account address is", address) print("Deployer account balance is", from_wei(web3.eth.getBalance(address), "ether"), "ETH") # Goes through geth account unlock process if needed if is_account_locked(web3, address): request_account_unlock(c, address, timeout=3600 * 6) assert not is_account_locked(web3, address) Token = c.provider.get_base_contract_factory('CentrallyIssuedToken') token = Token(address=token) print("Token is", token.address) print("Total supply is", token.functions.totalSupply().call()) print("Upgrade master is", token.functions.upgradeMaster().call()) print("Deployer account token balance is", token.functions.balanceOf(address).call()) decimals = token.functions.decimals().call() print("Token decimal places is", decimals) assert decimals >= 0 decimal_multiplier = 10**decimals if gas_price: gas_price = int(gas_price) * 10**9 else: gas_price = web3.eth.gasPrice * 2 transaction = {"from": address, "gasPrice": gas_price} print("Using gas price of", gas_price / 10**9, "GWei") IssuerWithId = c.provider.get_base_contract_factory('IssuerWithId') if not issuer_address: # TODO: Fix Populus support this via an deploy argument if "JSONFile" in c.registrar.registrar_backends: del c.registrar.registrar_backends["JSONFile"] # Create issuer contract assert master_address, "You need to give master-address" args = [address, master_address, token.address] print("Deploying new issuer contract", args, "transaction parameters", transaction) issuer, txhash = c.provider.deploy_contract( "IssuerWithId", deploy_transaction=transaction, deploy_args=args) print("Deployment transaction is", txhash) print("Waiting contract to be deployed") check_succesful_tx(web3, txhash) const_args = get_constructor_arguments(issuer, args) print("Contract constructor arguments are", const_args) chain_name = chain fname = "IssuerWithId.sol" browser_driver = "chrome" verify_contract( project=project, libraries={}, # TODO: Figure out how to pass around chain_name=chain_name, address=issuer.address, contract_name="IssuerWithId", contract_filename=fname, constructor_args=const_args, browser_driver=browser_driver, compiler=solc_version) link = get_etherscan_link(chain_name, issuer.address) print("Issuer verified contract is", link) else: print("Using existing issuer contract") issuer = IssuerWithId(address=issuer_address) print("Issuer contract is", issuer.address) print("Currently issued", issuer.functions.issuedCount().call()) if not master_address: sys.exit( "Please use Token.approve() to give some allowance for the issuer contract by master address" ) allowance = token.functions.allowance(master_address, issuer.address).call() print("Issuer allowance", allowance) if allowance == 0 or not master_address: sys.exit( "Please use Token.approve() to give some allowance for the issuer contract by master address" ) print("Reading data", csv_file) with open(csv_file, "rt") as inp: reader = csv.DictReader(inp) rows = [row for row in reader] # Prevalidate addresses # For distributetokens.py this is done by combine-csv # Here we do it inline and make skip addresses that are not valid. for idx, row in enumerate(rows): addr = row[address_column].strip() try: if addr: validate_ethereum_address(addr) except ValueError as e: print("Invalid Ethereum address on row:", idx + 1, "address:", addr, "reason:", str(e), "external_id:", row[external_id_column]) # Proceed regardless of invalid data row[address_column] = "" # Start distribution start_time = time.time() start_balance = from_wei(web3.eth.getBalance(address), "ether") tx_to_confirm = [] # List of txids to confirm tx_batch_size = 16 # How many transactions confirm once print("Total rows", len(rows)) for i in range(start_from, min(start_from + limit, len(rows))): data = rows[i] addr = data[address_column].strip() external_id = data[external_id_column].strip() tokens = Decimal(data[amount_column].strip()) tokens *= decimal_multiplier end_balance = from_wei(web3.eth.getBalance(address), "ether") spent = start_balance - end_balance if addr == "": if not allow_addresless: raise RuntimeError("Encountered missing address") else: continue if not external_id: raise RuntimeError("Missing external id on row #{}".format(i + 1)) # http://stackoverflow.com/a/19965088/315168 if not tokens % 1 == 0: raise RuntimeError( "Could not issue tokens because after multiplication was not integer" ) transaction = { "from": address, "gasPrice": gas_price, "gas": 100000, # Use 100k gas unit limit } tokens = int(tokens) external_id = int(external_id) if not external_id > 0: raise RuntimeError( "External id must be a positive integer on row #{}".format( i + 1)) print("Row", i, "giving", tokens, "to", addr, "issuer", issuer.address, "time passed", time.time() - start_time, "ETH passed", spent, "gas price", transaction["gasPrice"] / (10**9)) if issuer.functions.issued(external_id).call(): print("Already issued, skipping") continue txid = issuer.functions.issue(addr, tokens, external_id).transact(transaction) tx_to_confirm.append(txid) # Confirm N transactions when batch max size is reached if len(tx_to_confirm) >= tx_batch_size: check_multiple_succesful_txs(web3, tx_to_confirm) tx_to_confirm = [] # Confirm dangling transactions check_multiple_succesful_txs(web3, tx_to_confirm) end_balance = from_wei(web3.eth.getBalance(address), "ether") print("Deployment cost is", start_balance - end_balance, "ETH") print("All done! Enjoy your decentralized future.")