Skip to content

Commit

Permalink
Faster token distribution with batched transaction verification.
Browse files Browse the repository at this point in the history
  • Loading branch information
miohtama committed May 13, 2017
1 parent 6e9f5e9 commit af475b5
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 12 deletions.
8 changes: 6 additions & 2 deletions ico/cmd/deploytoken.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
@click.option('--minting-agent', nargs=1, help='Address that acts as a minting agent (can be same as owner)', default=None)
@click.option('--name', nargs=1, required=True, help='Token name', type=str)
@click.option('--symbol', nargs=1, required=True, help='Token symbol', type=str)
@click.option('--supply', nargs=1, default=21000000, help='What is the initial token supply', type=int)
@click.option('--supply', nargs=1, default=21000000, help='Initial token supply (multipled with decimals)', type=int)
@click.option('--decimals', nargs=1, default=0, help='How many decimal points the token has', type=int)
@click.option('--verify/--no-verify', default=False, help='Verify contract on EtherScan.io')
@click.option('--verify-filename', nargs=1, help='Solidity source file of the token contract for verification', default=None)
Expand All @@ -52,8 +52,10 @@ def main(chain, address, contract_name, name, symbol, supply, decimals, minting_
if is_account_locked(web3, address):
request_account_unlock(c, address, None)

decimal_multiplier = 10 ** decimals

transaction = {"from": address}
args = [name, symbol, supply, decimals]
args = [name, symbol, supply * decimal_multiplier, decimals]

if contract_name == "CentrallyIssuedToken":
# TODO: Generalize settings contract args
Expand Down Expand Up @@ -99,6 +101,8 @@ def main(chain, address, contract_name, name, symbol, supply, decimals, minting_

print("Verified contract is", link)

print("Token supply:", contract.call().totalSupply())

# Do some contract reads to see everything looks ok
try:
print("Token owner:", contract.call().owner())
Expand Down
57 changes: 48 additions & 9 deletions ico/cmd/distributetokens.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
"""Distribute tokens in centrally issued crowdsale."""
import csv
import time
import sys
import datetime

import click
from decimal import Decimal
from eth_utils import from_wei
from eth_utils import to_wei
from populus.utils.accounts import is_account_locked
from populus import Project
from populus.utils.cli import request_account_unlock

from ico.utils import check_succesful_tx
from ico.utils import check_multiple_succesful_txs
from ico.etherscan import verify_contract
from ico.etherscan import get_etherscan_link%cpas
from ico.utils import get_constructor_arguments


Expand All @@ -34,6 +34,8 @@ def main(chain, address, token, csv_file, limit, start_from, issuer_address, add
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.
To speed up the issuance, transactions are verified in batches. Each batch is 16 transactions at a time.
Example (first run):
distribute-tokens --chain=kovan --address=0x001FC7d7E506866aEAB82C11dA515E9DD6D02c25 --token=0x1644a421ae0a0869bac127fa4cce8513bd666705 --csv-file=input.csv --allow-zero --address-column="Ethereum address" --amount-column="Golden tickets earned"
Expand All @@ -56,6 +58,7 @@ def main(chain, address, token, csv_file, limit, start_from, issuer_address, add
# 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)
Expand All @@ -68,6 +71,8 @@ def main(chain, address, token, csv_file, limit, start_from, issuer_address, add
print("Token decimal places is", decimals)
assert decimals >= 0

decimal_multiplier = 10**decimals

transaction = {"from": address}

Issuer = c.provider.get_base_contract_factory('Issuer')
Expand All @@ -76,15 +81,35 @@ def main(chain, address, token, csv_file, limit, start_from, issuer_address, add
args = [address, address, token.address]
print("Deploying new issuer contract", args)
issuer, txhash = c.provider.deploy_contract("Issuer", deploy_transaction=transaction, deploy_args=args)
token.transact({"from": address}).approve(issuer.address, token.call().totalSupply())
check_succesful_tx(web3, txhash)

txid = token.transact({"from": address}).approve(issuer.address, token.call().totalSupply())
check_succesful_tx(web3, txid)

const_args = get_constructor_arguments(issuer, args)
chain_name = chain
fname = "Issuer.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="Issuer",
contract_filename=fname,
constructor_args=const_args,
# libraries=runtime_data["contracts"][name]["libraries"],
browser_driver=browser_driver)
link = get_etherscan_link(chain_name, issuer.address)

print("Issuer verified contract is", link)
else:
print("Reusing existing issuer contract")
issuer = Issuer(address=issuer_address)

print("Issuer contract is", issuer.address)
print("Currently issued", issuer.call().issuedCount())

multiplier = 10**decimals
print("Issuer allowance", token.call().allowance(address, issuer.address))

print("Reading data", csv_file)
with open(csv_file, "rt") as inp:
Expand All @@ -102,16 +127,19 @@ def main(chain, address, token, csv_file, limit, start_from, issuer_address, add
# 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

for i in range(start_from, min(start_from+limit, len(rows))):
data = rows[i]
addr = data[address_column].strip()
tokens = Decimal(data[amount_column].strip())

tokens *= multiplier
tokens *= decimal_multiplier

end_balance = from_wei(web3.eth.getBalance(address), "ether")
spent = start_balance - end_balance
print("Row", i, "giving", tokens, "to", addr, "issuer", issuer.address, "time passed", time.time() - start_time, "ETH passed", spent)

if tokens == 0:
if not allow_zero:
Expand All @@ -125,12 +153,23 @@ def main(chain, address, token, csv_file, limit, start_from, issuer_address, add

tokens = int(tokens)

print("Row", i, "giving", tokens, "to", addr, "issuer", issuer.address, "time passed", time.time() - start_time, "ETH passed", spent)

if issuer.call().issued(addr):
print("Already issued, skipping")
continue

txid = issuer.transact(transaction).issue(addr, tokens)
check_succesful_tx(web3, txid)

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")
Expand Down
17 changes: 16 additions & 1 deletion ico/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0'))


class TransactionFailure(Exception):
"""We waited transaction to be mined and it did not happen.
Usually throw statement in Solidity code or not enough gas.
"""


def asbool(s):
""" Return the boolean value ``True`` if the case-lowered value of string
input ``s`` is a :term:`truthy string`. If ``s`` is already one of the
Expand All @@ -45,10 +52,18 @@ def check_succesful_tx(web3: Web3, txid: str, timeout=180) -> dict:
txinfo = web3.eth.getTransaction(txid)

# EVM has only one error mode and it's consume all gas
assert txinfo["gas"] != receipt["gasUsed"]
if txinfo["gas"] == receipt["gasUsed"]:
raise TransactionFailure("Transaction failed: {}".format(txid))
return receipt


def check_multiple_succesful_txs(web3: Web3, tx_list: list, timeout=180):
"""Check that multiple transactions confirmed"""
for tx in tx_list:
check_succesful_tx(web3, tx, timeout)



def get_constructor_arguments(contract: Contract, args: Optional[list]=None, kwargs: Optional[dict]=None):
"""Get constructor arguments for Etherscan verify.
Expand Down

0 comments on commit af475b5

Please sign in to comment.