diff --git a/README.md b/README.md index e44f0d0..129f102 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,6 @@ This will output two different kinds of files * `/tmp/dist__.json` which is the unsigned JSON representation of a batch transaction * `~/dist___signed.json` which represents the signed, but not yet broadcast batch transaaction -In addition to the original Lavender.Five nodes version of this script, there are two new command -line options, `--dry_run` and `-f`/`--refund_file`. Details below: - ```bash $ python3 src/slash_refund.py --help usage: slash_refund.py [-h] --denom DENOM --daemon DAEMON -c CHAIN_ID -e ENDPOINT -vc VALCONS_ADDRESS -v VALOPER_ADDRESS -s SEND_ADDRESS [-m MEMO] -k KEYNAME [--dry_run [DRY_RUN]] [-f REFUND_FILE] diff --git a/refunds.csv b/refunds.csv new file mode 100644 index 0000000..af708e3 --- /dev/null +++ b/refunds.csv @@ -0,0 +1,3 @@ +address,amount +decentr1qd5cvs4tel4a5zzgq2qgsmvyfzp7ytpxpu80vh,136813 +Total Refund Amount,136813 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/slash_refund.py b/src/slash_refund.py index 351d837..dbcfec0 100644 --- a/src/slash_refund.py +++ b/src/slash_refund.py @@ -5,6 +5,7 @@ from subprocess import run from time import sleep +from utils.csv_utils import writeRefundsCsv BIN_DIR = "" # if this isn't empty, make sure it ends with a slash @@ -14,6 +15,7 @@ logger.addHandler(stream_handler) logger.setLevel(logging.INFO) + def getResponse(end_point, query_field=None, query_msg=None): response = None @@ -29,11 +31,15 @@ def getResponse(end_point, query_field=None, query_msg=None): return json.loads(response.text) else: if response is not None: - logger.error('\n\t'.join(( - "Response Error", - str(response.status_code), - str(response.text), - ))) + logger.error( + "\n\t".join( + ( + "Response Error", + str(response.status_code), + str(response.text), + ) + ) + ) else: logger.error("Response is None") @@ -60,8 +66,8 @@ def getDelegationAmounts( while more_pages: endpoint_choice = (page % len(endpoints)) - 1 command = f"{BIN_DIR}{daemon} q staking delegations-to {valoper_address} --height {block_height} --page {page} --output json --limit {page_limit} --node {endpoints[endpoint_choice]} --chain-id {chain_id}" - logger.debug(f'Delegation amount command: {command}') - logger.info(f'Page: {page}') + logger.debug(f"Delegation amount command: {command}") + logger.info(f"Page: {page}") result = run( command, shell=True, @@ -69,7 +75,7 @@ def getDelegationAmounts( text=True, ) if result.returncode == 1: - logger.info(f'Failed endpoint: {endpoints[endpoint_choice]}') + logger.info(f"Failed endpoint: {endpoints[endpoint_choice]}") continue response = json.loads(result.stdout) @@ -96,11 +102,11 @@ def calculateRefundAmounts( pre_slash_delegations = getDelegationAmounts( daemon, endpoint, chain_id, pre_slack_block, valoper_address ) - logger.debug(f'Pre slash amounts: {pre_slash_delegations}') + logger.debug(f"Pre slash amounts: {pre_slash_delegations}") post_slash_delegations = getDelegationAmounts( daemon, endpoint, chain_id, slash_block, valoper_address ) - logger.debug(f'Post slash amounts: {post_slash_delegations}') + logger.debug(f"Post slash amounts: {post_slash_delegations}") if len(pre_slash_delegations) != len(post_slash_delegations): raise ("Something went awry on delegation calcs") @@ -111,7 +117,7 @@ def calculateRefundAmounts( if refund_amount > 10000: refund_amounts[delegation_address] = refund_amount - logger.info(f'Refund amounts: {len(refund_amounts)}') + logger.info(f"Refund amounts: {len(refund_amounts)}") return refund_amounts @@ -176,27 +182,47 @@ def buildRefundScript( def issue_refunds( - batch_count: int, daemon: str, chain_id: str, keyname: str, node: str + batch_count: int, + daemon: str, + chain_id: str, + keyname: str, + node: str, + broadcast: bool = True, ): i = 0 while i < batch_count: - command = f"{BIN_DIR}{daemon} tx sign /tmp/dist_{i}.json --from {keyname} -ojson --output-document ~/dist_signed.json --node {node} --chain-id {chain_id} --keyring-backend test", - logger.debug(f'command being run: {command}') - result = run( - f"{BIN_DIR}{daemon} tx sign /tmp/dist_{i}.json --from {keyname} -ojson --output-document ~/dist_signed.json --node {node} --chain-id {chain_id} --keyring-backend test", - shell=True, - capture_output=True, - text=True, + sign_cmd = ( + f"{BIN_DIR}{daemon} tx sign /tmp/dist_{i}.json --from {keyname} -ojson " + f"--output-document ~/dist_{i}_signed.json --node {node} --chain-id {chain_id} " + f"--keyring-backend test" ) - sleep(1) + broadcast_cmd = ( + f"{BIN_DIR}{daemon} tx broadcast ~/dist_{i}_signed.json --node {node} " + f"--chain-id {chain_id}" + ) + + # sign refund result = run( - f"{BIN_DIR}{daemon} tx broadcast ~/dist_signed.json --node {node} --chain-id {chain_id}", + sign_cmd, shell=True, capture_output=True, text=True, ) - i += 1 - sleep(15) + sleep(1) + + if broadcast: + # broadcast refund + result = run( + broadcast_cmd, + shell=True, + capture_output=True, + text=True, + ) + logger.info(f"Broadcasted refund: {result}") + + # if this is not the last batch, sleep + if i < batch_count: + sleep(16) def parseArgs(): @@ -266,9 +292,55 @@ def parseArgs(): required=True, help="Wallet to issue refunds from", ) + parser.add_argument( + "-f", + "--refund_file", + dest="refund_file", + required=False, + default=None, + type=open, + help=( + "CSV file that encodes the delegator addresses and refund amounts. Note: delegator " + "address is expected to be in the first column and the refund amount in [DENOM] is " + "expected to be in the fourth column." + ), + ) + parser.add_argument( + "--dry_run", + dest="dry_run", + action="store_const", + required=False, + default=False, + const=True, + help="Indicates whether this should actually broadcast transactions or not", + ) + parser.add_argument( + "--no_broadcast", + dest="no_broadcast", + action="store_const", + required=False, + default=False, + const=True, + help=( + "Similar to dry run, but in this case the tx JSON is output and signed, but not " + "broadcast. This is useful for testing." + ), + ) return parser.parse_args() +def get_daemon_path(daemon: str) -> str: + result = run( + f"which {daemon}", + shell=True, + capture_output=True, + text=True, + ) + binary_path = result.stdout.strip().removesuffix(daemon) + logger.info(f"Binary path: {binary_path}") + return binary_path + + def main(): global BIN_DIR args = parseArgs() @@ -281,28 +353,27 @@ def main(): send_address = args.send_address memo = args.memo keyname = args.keyname + refund_file = args.refund_file + dry_run = args.dry_run + should_broadcast = not args.no_broadcast + logger.debug(f"DEBUG: args: {args}") BIN_DIR = get_daemon_path(daemon) slash_block = getSlashBlock(endpoint, valcons_address) - logger.info(f'Slash block: {slash_block}') + logger.info(f"Slash block: {slash_block}") refund_amounts = calculateRefundAmounts( daemon, endpoint, chain_id, slash_block, valoper_address ) - batch_count = buildRefundScript(refund_amounts, send_address, denom, memo) - issue_refunds(batch_count, daemon, chain_id, keyname, endpoint) + writeRefundsCsv(refund_amounts) -def get_daemon_path(daemon: str) -> str: - result = run( - f"which {daemon}", - shell=True, - capture_output=True, - text=True, + batch_count = buildRefundScript(refund_amounts, send_address, denom, memo) + if not dry_run: + issue_refunds( + batch_count, daemon, chain_id, keyname, endpoint, should_broadcast ) - binary_path = result.stdout.strip().removesuffix(daemon) - logger.info(f'Binary path: {binary_path}') - return binary_path + if __name__ == "__main__": main() diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/csv_utils.py b/src/utils/csv_utils.py new file mode 100644 index 0000000..65ff638 --- /dev/null +++ b/src/utils/csv_utils.py @@ -0,0 +1,39 @@ +import csv +from decimal import Decimal + +DENOM_EXPONENTS = { + "ATOM": 0, + "uatom": 6, + "OSMO": 0, + "uosmo": 6, +} + + +def writeRefundsCsv(refund_amounts: dict): + header = ["address", "amount"] + refund_sum = 0 + with open("refunds.csv", "w") as f: + # create the csv writer + writer = csv.writer(f) + writer.writerow(header) + + for k in refund_amounts.items(): + _, refund_amount = k + writer.writerow(k) + refund_sum += refund_amount + + writer.writerow(["Total Refund Amount", refund_sum]) + + +def getRefundAmountsFromCSV(file_obj, denom): + refund_amounts = {} + refund_reader = csv.reader(file_obj, delimiter=",", quotechar="|") + denom_multiplier = 10 ** DENOM_EXPONENTS.get(denom, 1) + for row in refund_reader: + if "address" in row[0]: + continue + delegation_addr = row[0] + refund_amt = Decimal(row[3]) * denom_multiplier + refund_amounts[delegation_addr] = refund_amt + + return refund_amounts