def payout(self, simulate=False): """ Calculates payouts for users from share records for found blocks. """ try: if simulate: logger.debug("Running in simulate mode, no commit will be performed") logger.setLevel(logging.DEBUG) # find the oldest un-processed block block = (Block.query.filter_by(processed=False). order_by(Block.height).first()) if block is None: logger.debug("No block found, exiting...") return logger.debug("Processing block height {}".format(block.height)) mult = int(current_app.config['last_n']) logger.debug("Looking for up to {} total shares".format(total_shares)) total_shares = (bits_to_shares(block.bits) * mult) remain = total_shares start = block.last_share.id logger.debug("Identified last matching share id as {}".format(start)) user_shares = {} for share in Share.query.order_by(Share.id.desc()).filter(Share.id <= start).yield_per(100): user_shares.setdefault(share.user, 0) if remain > share.shares: user_shares[share.user] += share.shares remain -= share.shares else: user_shares[share.user] += remain remain = 0 break # if we found less than n, use what we found as the total total_shares -= remain logger.debug("Found {} shares".format(total_shares)) if simulate: out = "\n".join(["\t".join((user, str(amount))) for user, amount in user_shares.iteritems()]) logger.debug("Share distribution:\n{}".format(out)) # Calculate the portion going to the miners by truncating the # fractional portions and giving the remainder to the pool owner logger.debug("Distribute_amnt: {}".format(block.total_value)) # Below calculates the truncated portion going to each miner. because # of fractional pieces the total accrued wont equal the disitrubte_amnt # so we will distribute that round robin the miners in dictionary order accrued = 0 user_payouts = {} for user, share_count in user_shares.iteritems(): user_payouts.setdefault(share.user, 0) user_payouts[user] = (share_count * block.total_value) // total_shares accrued += user_payouts[user] logger.debug("Total accrued after trunated iteration {}; {}%" .format(accrued, (accrued / float(block.total_value)) * 100)) # loop over the dictionary indefinitely until we've distributed # all the remaining funds i = 0 while accrued < block.total_value: for key in user_payouts: i += 1 user_payouts[key] += 1 accrued += 1 # exit if we've exhausted if accrued >= block.total_value: break logger.debug("Ran round robin distro {} times to finish distrib" .format(i)) # now handle donation or bonus distribution for each user donation_total = 0 bonus_total = 0 user_perc_applied = {} user_perc = {} default_perc = current_app.config.get('default_perc', 0) # convert our custom percentages that apply to these users into an # easy to access dictionary custom_percs = DonationPercent.query.filter(DonationPercent.user.in_(user_shares.keys())) custom_percs = {d.user: d.perc for d in custom_percs} for user, payout in user_payouts.iteritems(): # use the custom perc, or fallback to the default perc = custom_percs.get(user, default_perc) user_perc[user] = perc # if the perc is greater than 0 it's calced as a donation if perc > 0: donation = int(ceil((perc / 100.0) * payout)) logger.debug("Donation of\t{}\t({}%)\tcollected from\t{}" .format(donation / 100000000.0, perc, user)) donation_total += donation user_payouts[user] -= donation user_perc_applied[user] = donation # if less than zero it's a bonus payout elif perc < 0: perc *= -1 bonus = int(floor((perc / 100.0) * payout)) logger.debug("Bonus of\t{}\t({}%)\tpaid to\t{}" .format(bonus / 100000000.0, perc, user)) user_payouts[user] += bonus bonus_total += bonus user_perc_applied[user] = -1 * bonus # percentages of 0 are no-ops logger.info("Payed out {} in bonus payment" .format(bonus_total / 100000000.0)) logger.info("Received {} in donation payment" .format(donation_total / 100000000.0)) logger.info("Net income from block {}" .format((donation_total - bonus_total) / 100000000.0)) assert accrued == block.total_value logger.info("Successfully distributed all rewards among {} users." .format(len(user_payouts))) # run another safety check user_sum = sum(user_payouts.values()) assert user_sum == (block.total_value + bonus_total - donation_total) logger.info("Double check for payout distribution." " Total user payouts {}, total block value {}." .format(user_sum, block.total_value)) if simulate: out = "\n".join(["\t".join((user, str(amount / 100000000.0))) for user, amount in user_payouts.iteritems()]) logger.debug("Payout distribution:\n{}".format(out)) db.session.rollback() else: # record the payout for each user for user, amount in user_payouts.iteritems(): Payout.create(user, amount, block, user_shares[user], user_perc[user], user_perc_applied.get(user, 0)) # update the block status and collected amounts block.processed = True block.donated = donation_total block.bonus_payed = bonus_total # record the donations as a bonus payout to the donate address if donation_total > 0: donate_address = current_app.config['donate_address'] BonusPayout.create(donate_address, donation_total, "Total donations from block {}" .format(block.height)) logger.info("Added bonus payout to donation address {} for {}" .format(donate_address, donation_total / 100000000.0)) block_bonus = current_app.config.get('block_bonus', 0) if block_bonus > 0: BonusPayout.create(block.user, block_bonus, "Blockfinder bonus for block {}" .format(block.height)) logger.info("Added bonus payout for blockfinder {} for {}" .format(block.user, block_bonus / 100000000.0)) db.session.commit() except Exception as exc: logger.error("Unhandled exception in payout", exc_info=True) db.session.rollback() raise self.retry(exc=exc)
def payout(hash=None, simulate=False): """ Calculates payouts for users from share records for the latest found block. """ if simulate: logger.debug("Running in simulate mode, no commit will be performed") logger.setLevel(logging.DEBUG) # find the oldest un-processed block if hash: block = Block.query.filter_by(processed=False, hash=hash).first() else: block = (Block.query.filter_by(processed=False).order_by(Block.found_at).first()) if block is None: logger.debug("No block found, exiting...") return logger.debug("Processing block height {}".format(block.height)) mult = int(current_app.config['last_n']) total_shares = int((bits_to_shares(block.bits) * mult) / current_app.config.get('share_multiplier', 1)) logger.debug("Looking for up to {} total shares".format(total_shares)) if block.last_share_id is None: logger.error("Can't process this block, it's shares have been deleted!") return logger.debug("Identified last matching share id as {}".format(block.last_share_id)) # if we found less than n, use what we found as the total user_shares, total_shares = get_sharemap(block.last_share_id, total_shares) logger.debug("Found {} shares".format(total_shares)) if simulate: out = "\n".join(["\t".join((user, str((amount * 100.0) / total_shares), str((amount * block.total_value) // total_shares), str(amount))) for user, amount in user_shares.iteritems()]) logger.debug("Share distribution:\nUSR\t%\tBLK_PAY\tSHARE\n{}".format(out)) logger.debug("Distribute_amnt: {}".format(block.total_value)) if block.merged_type: merge_cfg = current_app.config['merged_cfg'][block.merged_type] new_user_shares = {merge_cfg['donate_address']: 0} # build a map of regular addresses to merged addresses query = (MergeAddress.query.filter_by(merged_type=block.merged_type). filter(MergeAddress.user.in_(user_shares.keys()))) merge_addr_map = {m.user: m.merge_address for m in query} logger.debug("Looking up merged mappings for merged_type {}, found {}" .format(block.merged_type, len(merge_addr_map))) for user in user_shares: merge_addr = merge_addr_map.get(user) # if this user didn't set a merged mining address if not merge_addr: # give the excess to the donation address if set to not # distribute unassigned if not merge_cfg['distribute_unassigned']: new_user_shares[merge_cfg['donate_address']] += user_shares[user] else: total_shares -= user_shares[user] continue new_user_shares.setdefault(merge_addr, 0) new_user_shares[merge_addr] += user_shares[user] user_shares = new_user_shares assert total_shares == sum(user_shares.itervalues()) # Below calculates the truncated portion going to each miner. because # of fractional pieces the total accrued wont equal the disitrubte_amnt # so we will distribute that round robin the miners in dictionary order accrued = 0 user_payouts = {} for user, share_count in user_shares.iteritems(): user_payouts[user] = float(share_count * block.total_value) // total_shares accrued += user_payouts[user] logger.debug("Total accrued after trunated iteration {}; {}%" .format(accrued, (accrued / float(block.total_value)) * 100)) # loop over the dictionary indefinitely until we've distributed # all the remaining funds i = 0 while accrued < block.total_value: for key in user_payouts: i += 1 user_payouts[key] += 1 accrued += 1 # exit if we've exhausted if accrued >= block.total_value: break logger.debug("Ran round robin distro {} times to finish distrib".format(i)) # now handle donation or bonus distribution for each user donation_total = 0 bonus_total = 0 # dictionary keyed by address to hold donate/bonus percs and amnts user_perc_applied = {} user_perc = {} if not block.merged_type: default_perc = current_app.config.get('default_perc', 0) # convert our custom percentages that apply to these users into an # easy to access dictionary custom_percs = DonationPercent.query.filter(DonationPercent.user.in_(user_shares.keys())) custom_percs = {d.user: d.perc for d in custom_percs} else: default_perc = merge_cfg.get('default_perc', 0) custom_percs = {} for user, payout in user_payouts.iteritems(): # use the custom perc, or fallback to the default perc = custom_percs.get(user, default_perc) user_perc[user] = perc # if the perc is greater than 0 it's calced as a donation if perc > 0: donation = int(ceil((perc / 100.0) * payout)) logger.debug("Donation of\t{}\t({}%)\tcollected from\t{}" .format(donation / 100000000.0, perc, user)) donation_total += donation user_payouts[user] -= donation user_perc_applied[user] = donation # if less than zero it's a bonus payout elif perc < 0: perc *= -1 bonus = int(floor((perc / 100.0) * payout)) logger.debug("Bonus of\t{}\t({}%)\tpaid to\t{}" .format(bonus / 100000000.0, perc, user)) user_payouts[user] += bonus bonus_total += bonus user_perc_applied[user] = -1 * bonus # percentages of 0 are no-ops logger.info("Payed out {} in bonus payment" .format(bonus_total / 100000000.0)) logger.info("Received {} in donation payment" .format(donation_total / 100000000.0)) logger.info("Net income from block {}" .format((donation_total - bonus_total) / 100000000.0)) assert accrued == block.total_value logger.info("Successfully distributed all rewards among {} users." .format(len(user_payouts))) # run another safety check user_sum = sum(user_payouts.values()) assert user_sum == (block.total_value + bonus_total - donation_total) logger.info("Double check for payout distribution." " Total user payouts {}, total block value {}." .format(user_sum, block.total_value)) if simulate: out = "\n".join(["\t".join((user, str(amount / 100000000.0))) for user, amount in user_payouts.iteritems()]) logger.debug("Final payout distribution:\nUSR\tAMNT\n{}".format(out)) db.session.rollback() else: # record the payout for each user for user, amount in user_payouts.iteritems(): if amount == 0.0: logger.info("Skip zero payout for USR: {}".format(user)) continue Payout.create(user, amount, block, user_shares[user], user_perc[user], user_perc_applied.get(user, 0), merged_type=block.merged_type) # update the block status and collected amounts block.processed = True block.donated = donation_total block.bonus_payed = bonus_total # record the donations as a bonus payout to the donate address if donation_total > 0: if block.merged_type: donate_address = merge_cfg['donate_address'] else: donate_address = current_app.config['donate_address'] BonusPayout.create(donate_address, donation_total, "Total donations from block {}" .format(block.height), block, merged_type=block.merged_type) logger.info("Added bonus payout to donation address {} for {}" .format(donate_address, donation_total / 100000000.0)) block_bonus = current_app.config.get('block_bonus', 0) if block_bonus > 0 and not block.merged_type: BonusPayout.create(block.user, block_bonus, "Blockfinder bonus for block {}" .format(block.height), block) logger.info("Added bonus payout for blockfinder {} for {}" .format(block.user, block_bonus / 100000000.0)) db.session.commit()
def give_bonus(user, amount, description, blockhash): """ Manually create a BonusPayout for a user """ block = Block.query.filter_by(hash=blockhash).one() BonusPayout.create(user, amount, description, block) db.session.commit()
def payout(hash=None, simulate=False): """ Calculates payouts for users from share records for the latest found block. """ if simulate: logger.debug("Running in simulate mode, no commit will be performed") logger.setLevel(logging.DEBUG) # find the oldest un-processed block if hash: block = Block.query.filter_by(processed=False, hash=hash).first() else: block = (Block.query.filter_by(processed=False).order_by( Block.found_at).first()) if block is None: logger.debug("No block found, exiting...") return logger.debug("Processing block height {}".format(block.height)) mult = int(current_app.config['last_n']) total_shares = int((bits_to_shares(block.bits) * mult) / current_app.config.get('share_multiplier', 1)) logger.debug("Looking for up to {} total shares".format(total_shares)) if block.last_share_id is None: logger.error( "Can't process this block, it's shares have been deleted!") return logger.debug("Identified last matching share id as {}".format( block.last_share_id)) # if we found less than n, use what we found as the total user_shares, total_shares = get_sharemap(block.last_share_id, total_shares) logger.debug("Found {} shares".format(total_shares)) if simulate: out = "\n".join([ "\t".join((user, str((amount * 100.0) / total_shares), str((amount * block.total_value) // total_shares), str(amount))) for user, amount in user_shares.iteritems() ]) logger.debug( "Share distribution:\nUSR\t%\tBLK_PAY\tSHARE\n{}".format(out)) logger.debug("Distribute_amnt: {}".format(block.total_value)) if block.merged_type: merge_cfg = current_app.config['merged_cfg'][block.merged_type] new_user_shares = {merge_cfg['donate_address']: 0} # build a map of regular addresses to merged addresses query = (MergeAddress.query.filter_by( merged_type=block.merged_type).filter( MergeAddress.user.in_(user_shares.keys()))) merge_addr_map = {m.user: m.merge_address for m in query} logger.debug( "Looking up merged mappings for merged_type {}, found {}".format( block.merged_type, len(merge_addr_map))) for user in user_shares: merge_addr = merge_addr_map.get(user) # if this user didn't set a merged mining address if not merge_addr: # give the excess to the donation address if set to not # distribute unassigned if not merge_cfg['distribute_unassigned']: new_user_shares[ merge_cfg['donate_address']] += user_shares[user] else: total_shares -= user_shares[user] continue new_user_shares.setdefault(merge_addr, 0) new_user_shares[merge_addr] += user_shares[user] user_shares = new_user_shares assert total_shares == sum(user_shares.itervalues()) # Below calculates the truncated portion going to each miner. because # of fractional pieces the total accrued wont equal the disitrubte_amnt # so we will distribute that round robin the miners in dictionary order accrued = 0 user_payouts = {} for user, share_count in user_shares.iteritems(): user_payouts[user] = float( share_count * block.total_value) // total_shares accrued += user_payouts[user] logger.debug("Total accrued after trunated iteration {}; {}%".format( accrued, (accrued / float(block.total_value)) * 100)) # loop over the dictionary indefinitely until we've distributed # all the remaining funds i = 0 while accrued < block.total_value: for key in user_payouts: i += 1 user_payouts[key] += 1 accrued += 1 # exit if we've exhausted if accrued >= block.total_value: break logger.debug("Ran round robin distro {} times to finish distrib".format(i)) # now handle donation or bonus distribution for each user donation_total = 0 bonus_total = 0 # dictionary keyed by address to hold donate/bonus percs and amnts user_perc_applied = {} user_perc = {} if not block.merged_type: default_perc = current_app.config.get('default_perc', 0) # convert our custom percentages that apply to these users into an # easy to access dictionary custom_percs = DonationPercent.query.filter( DonationPercent.user.in_(user_shares.keys())) custom_percs = {d.user: d.perc for d in custom_percs} else: default_perc = merge_cfg.get('default_perc', 0) custom_percs = {} for user, payout in user_payouts.iteritems(): # use the custom perc, or fallback to the default perc = custom_percs.get(user, default_perc) user_perc[user] = perc # if the perc is greater than 0 it's calced as a donation if perc > 0: donation = int(ceil((perc / 100.0) * payout)) logger.debug("Donation of\t{}\t({}%)\tcollected from\t{}".format( donation / 100000000.0, perc, user)) donation_total += donation user_payouts[user] -= donation user_perc_applied[user] = donation # if less than zero it's a bonus payout elif perc < 0: perc *= -1 bonus = int(floor((perc / 100.0) * payout)) logger.debug("Bonus of\t{}\t({}%)\tpaid to\t{}".format( bonus / 100000000.0, perc, user)) user_payouts[user] += bonus bonus_total += bonus user_perc_applied[user] = -1 * bonus # percentages of 0 are no-ops logger.info("Payed out {} in bonus payment".format(bonus_total / 100000000.0)) logger.info("Received {} in donation payment".format(donation_total / 100000000.0)) logger.info("Net income from block {}".format( (donation_total - bonus_total) / 100000000.0)) assert accrued == block.total_value logger.info("Successfully distributed all rewards among {} users.".format( len(user_payouts))) # run another safety check user_sum = sum(user_payouts.values()) assert user_sum == (block.total_value + bonus_total - donation_total) logger.info("Double check for payout distribution." " Total user payouts {}, total block value {}.".format( user_sum, block.total_value)) if simulate: out = "\n".join([ "\t".join((user, str(amount / 100000000.0))) for user, amount in user_payouts.iteritems() ]) logger.debug("Final payout distribution:\nUSR\tAMNT\n{}".format(out)) db.session.rollback() else: # record the payout for each user for user, amount in user_payouts.iteritems(): if amount == 0.0: logger.info("Skip zero payout for USR: {}".format(user)) continue Payout.create(user, amount, block, user_shares[user], user_perc[user], user_perc_applied.get(user, 0), merged_type=block.merged_type) # update the block status and collected amounts block.processed = True block.donated = donation_total block.bonus_payed = bonus_total # record the donations as a bonus payout to the donate address if donation_total > 0: if block.merged_type: donate_address = merge_cfg['donate_address'] else: donate_address = current_app.config['donate_address'] BonusPayout.create(donate_address, donation_total, "Total donations from block {}".format( block.height), block, merged_type=block.merged_type) logger.info( "Added bonus payout to donation address {} for {}".format( donate_address, donation_total / 100000000.0)) block_bonus = current_app.config.get('block_bonus', 0) if block_bonus > 0 and not block.merged_type: BonusPayout.create( block.user, block_bonus, "Blockfinder bonus for block {}".format(block.height), block) logger.info("Added bonus payout for blockfinder {} for {}".format( block.user, block_bonus / 100000000.0)) db.session.commit()
def give_bonus(user, amount, description): """ Manually create a BonusPayout for a user """ BonusPayout.create(user, amount, description) db.session.commit()