Skip to content

Instantly share code, notes, and snippets.

@baketzforme
Last active July 6, 2020 20:35
Show Gist options
  • Save baketzforme/7a34f8061386698f7f2ed0c504080f29 to your computer and use it in GitHub Desktop.
Save baketzforme/7a34f8061386698f7f2ed0c504080f29 to your computer and use it in GitHub Desktop.
Python script for Tezos payouts
# Useful Python 2.x script for Tezos bakers to calculate payouts for their delegators
# Original credit goes to /u/8x1_ from https://reddit.com/r/tezos/comments/98jwb4/paypy_there_is_no_more_excuse_for_not_paying/
# Released under the MIT License - See bottom of file for license information
import urllib
import json
import random
import sys
####################################################################
# Edit the values below to customize the script for your own needs #
####################################################################
baker_address = 'tz1...' # the address of the baker
baker_alias = 'baker' # alias of the baker wallet
hot_wallet_address = '' # if payouts are made from a non-baker address, enter it here (could be either tz1 or KT1)
wallet_alias = '' # alias of the hot wallet
default_fee_percent = 10 # default delegation service fee
special_addresses = ['tz1...', 'KT1...'] # special accounts that get a different fee, set to '' if none
special_fee_percent = 0 # delegation service fee for special addresses
precision = 6 # Tezos supports up to 6 decimal places of precision
minimum_delegation_threshold = 0 # required amount of tz delegated to baker to qualify for payouts
minimum_payout_threshold = 0 # delegator's gross reward (in tz) must be at least this high for payout to occur
#######################################################
# You shouldn't need to edit anything below this line #
#######################################################
# API URLs
api_url_head = 'https://api.tzkt.io/v1/head' # info about current status
api_url_rewards = 'http://api.tzkt.io/v1/rewards/split/' # info about rewards at specific cycle
# get current cycle info
response = urllib.urlopen(api_url_head)
data = json.loads(response.read())
level = int(data['level'])
cycle = (level - 1) // 4096
cycle_progress = round((float(level - 1) / 4096 - cycle) * 100, 2)
# display some info about status of current cycle and ETA until next cycle begins
print 'Currently {}% through cycle {}.'.format(cycle_progress, cycle)
next_cycle = cycle + 1
# calculate how many minutes, hours, days until next cycle begins
eta_minutes = 4096 * next_cycle - level + 1
eta_hours = eta_minutes / 60
eta_days = eta_hours / 24
# make sure hours and minutes aren't greater than next larger unit of time
eta_hours = eta_hours % 24
eta_minutes = eta_minutes % 60
# prepare to print the ETA until the next cycle in a nice way
status_text = 'Cycle {} begins in about '.format(next_cycle)
if eta_days > 0:
status_text += '{} day'.format(eta_days)
if eta_days > 1:
status_text += 's'
status_text += ' '
if eta_hours > 0:
status_text += '{} hour'.format(eta_hours)
if eta_hours > 1:
status_text += 's'
status_text += ' '
if eta_minutes > 0:
status_text += '{} minute'.format(eta_minutes)
if eta_minutes > 1:
status_text += 's'
# actually print the ETA info
print '{}.'.format(status_text)
print ''
# determine which cycle to use to calculate payouts
if len(sys.argv) != 2:
cycle -= 6
print 'No cycle passed in. Using most-recently unlocked rewards (cycle N-6) from cycle {}.'.format(cycle)
else:
# a few sanity checks for the passed-in value
is_valid = True
error_txt = ''
# make sure the value passed in is an integer
if sys.argv[1].isdigit():
# parameter is an int, now make sure we can use it
tmp_cycle = int(sys.argv[1])
if tmp_cycle > cycle: # cycle is in the future
is_valid = False
error_txt = 'ERROR: Cycle {} hasn\'t happened yet! We\'re still on cycle {}!\n'.format(tmp_cycle, cycle)
error_txt += 'What do you think I am? Some kind of time traveler?'
elif tmp_cycle == cycle: # cycle is in progress
error_txt = 'WARNING: Cycle {} hasn\'t finished yet, so this data may not reflect final results.'.format(cycle)
else:
# value is not an int (or is negative, which looks like a string to the parser)
is_valid = False
error_txt = 'ERROR: The value passed in ({}) is not an integer, or is negative!'.format(sys.argv[1])
# print the error message if necessary
if error_txt != '':
print ''
print '==================================================================================='
print error_txt
print '==================================================================================='
print ''
# quit if the value is invalid
if is_valid == False:
sys.exit()
cycle = tmp_cycle
print 'Calculating earnings and payouts for cycle {}.'.format(cycle)
# initialize some values outside of the upcoming loop
paid_delegators = 0
total_payouts_gross = 0
total_payouts = 0
total_fees = 0
page = 0
pages = 10
# get rewards data
while page < pages:
response = urllib.urlopen('{}{}/{}?offset={}'.format(api_url_rewards, baker_address, cycle, page * 100))
data = json.loads(response.read())
print ''
total_delegators = int(data['numDelegators'])
if total_delegators == 0:
print 'No delegators for cycle {}.'.format(cycle)
elif total_delegators > 100:
pages = total_delegators / 100;
else:
pages = 0
# increment the page number for the next loop
page += 1
total_staking_balance = long(data['stakingBalance'])
baker_balance = total_staking_balance
total_rewards = long(data['ownBlockRewards']) + \
long(data['extraBlockRewards']) + \
long(data['endorsementRewards']) + \
long(data['ownBlockFees']) + \
long(data['extraBlockFees']) + \
long(data['futureBlockRewards']) + \
long(data['futureEndorsementRewards']) + \
long(data['doubleBakingRewards']) - \
long(data['doubleBakingLostDeposits']) - \
long(data['doubleBakingLostRewards']) - \
long(data['doubleBakingLostFees']) + \
long(data['doubleEndorsingRewards']) - \
long(data['doubleEndorsingLostDeposits']) - \
long(data['doubleEndorsingLostRewards']) - \
long(data['doubleEndorsingLostFees']) + \
long(data['revelationRewards']) - \
long(data['revelationLostRewards']) - \
long(data['revelationLostFees'])
# make sure there's actually something to pay out
if total_rewards <= 0:
print 'WARNING: Total rewards this cycle is {}, so there\'s nothing to pay out. :('.format(total_rewards)
sys.exit()
net_earnings = total_rewards
# calculate and print out payment commands
for delegator_info in data['delegators']:
delegator_address = delegator_info['address']
bal = int(delegator_info['balance'])
# API orders addresses by amount staked, so skip all the rest if we encounter a 0 balance
if bal == 0:
page = pages
break
# don't include your hot wallet when calculating payouts (in case your hot wallet is a KT1 address delegated to yourself)
if delegator_address == hot_wallet_address:
continue
# don't make payouts to accounts which have delegated below the threshold
if bal < minimum_delegation_threshold * 1000000:
continue
baker_balance -= bal
fee_percent = default_fee_percent
# handle any special addresses
for address in special_addresses:
if delegator_address == address:
fee_percent = special_fee_percent
break
# calculate gross payout amount
payout_gross = (float(bal) / total_staking_balance) * total_rewards
total_payouts_gross += payout_gross
# subtract fee
payout = (payout_gross * (100 - fee_percent)) / 100
total_fees += payout_gross - payout
net_earnings -= payout
# convert from mutez (0.000001 XTZ) to XTZ
payout = round(payout / 1000000, precision)
# display the payout command to pay this delegator, filtering out any too-small payouts
if (payout >= minimum_payout_threshold) or (payout >= 0.000001 and cycle < 64):
total_payouts += payout
paid_delegators += 1
payout_string = '{0:.6f}'.format(payout) # force tiny values to show all digits
if wallet_alias:
payout_alias = wallet_alias
else:
payout_alias = baker_alias
print 'tz transfer {} from {} to {}'.format(payout_string, payout_alias, delegator_address)
# print some information about all payouts made for this cyle
if total_payouts > 0:
result_txt = '\nTotal payouts made: {} to {} delegator'.format(total_payouts, paid_delegators)
if paid_delegators > 1:
print result_txt + 's\n' # pluralize it!
else:
print result_txt + '\n'
# display the command to transfer total payout amount to the hot wallet
if hot_wallet_address:
print './tezos-client transfer {} from {} to {}'.format(total_payouts, baker_alias, wallet_alias)
# convert the amounts to a human readable format
total_rewards = round(float(total_rewards) / 1000000, precision)
net_earnings = round(float(net_earnings) / 1000000, precision)
share_of_gross = round(net_earnings / total_rewards * 100, 2)
total_fees = round(float(total_fees) / 1000000, precision)
total_staking_balance = round(float(total_staking_balance) / 1000000, precision)
baker_balance = round(float(baker_balance) / 1000000, precision)
baker_percentage = round(baker_balance / total_staking_balance * 100, 2)
# print out stats for this cycle's payouts
print ''
print '===================================================='
print 'Stats for cycle {}'.format(cycle)
print 'Total staked balance: {}'.format(total_staking_balance)
print 'Baker staked balance: {} ({}% of total)'.format(baker_balance, baker_percentage)
print 'Total (gross) earnings: {0:.6f}'.format(total_rewards)
if total_payouts > 0:
net_earnings_txt = '{0:.6f}'.format(net_earnings)
print 'Baker\'s (net) earnings: {} ({}% of gross) (that is, {} + {} as fees charged)'.format(net_earnings_txt, share_of_gross, net_earnings - total_fees, total_fees)
###############################################################################
# MIT License #
###############################################################################
# Copyright 2018 u/8x1_ and BakeTzForMe
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
###############################################################################
@baketzforme
Copy link
Author

baketzforme commented Mar 5, 2020

was testing to see if the pay.py was still working and something broke.... gave me this error...

python pay.py 206
Traceback (most recent call last):
File "pay.py", line 38, in
level = int(data['level'])
KeyError: 'level'

pay.py relies on the mystique.tzkt.io API to get information about the current state of the blockchain. Usually a call to https://mystique.tzkt.io/v3/head will return information about the current level (i.e., block number) for the blockchain. But right now it's returning only "Out of sync", which causes an error in pay.py because it doesn't expect to not get the information it is looking for.

As far as I'm aware, this problem is entirely in the hands of the folks at Baking Bad, who provide the Mystique API at tzkt.io, to fix. It's probably just a hiccup from the switch over to the Carthage upgrade which just went through.

I've contacted Baking Bad to make sure they're aware of the issue. Hopefully a fix will be coming shortly.

Alternatively, if you are aware of another working service which provides the same API, you can replace the API URLs in pay.py and it should calculate and process the payout information as usual.

EDIT: Seems to be working for me now. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment