Last active
July 6, 2020 20:35
-
-
Save baketzforme/7a34f8061386698f7f2ed0c504080f29 to your computer and use it in GitHub Desktop.
Python script for Tezos payouts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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. | |
############################################################################### |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 currentlevel
(i.e., block number) for the blockchain. But right now it's returning only"Out of sync"
, which causes an error inpay.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. 👍