diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1811f64 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# 0.1.0 +* First implementation of xch rpc wallet dealer diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..bf4d75d --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +requests = "*" +pyyaml = "*" + +[requires] +python_version = "3" + diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..e919342 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,99 @@ +{ + "_meta": { + "hash": { + "sha256": "5692032b7e232544624cc9a24e8f5d7e34e4c7b90021019c3c60808bcc4cd654" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", + "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" + ], + "markers": "python_version >= '3'", + "version": "==2.0.10" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "pyyaml": { + "hashes": [ + "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", + "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", + "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", + "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", + "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", + "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", + "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", + "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", + "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", + "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", + "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", + "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", + "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", + "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", + "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", + "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", + "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", + "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", + "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", + "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", + "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", + "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", + "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", + "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", + "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", + "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", + "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", + "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", + "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", + "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", + "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", + "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", + "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + ], + "index": "pypi", + "version": "==6.0" + }, + "requests": { + "hashes": [ + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + ], + "index": "pypi", + "version": "==2.27.1" + }, + "urllib3": { + "hashes": [ + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.8" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..75975ea --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Chia XCH Wallet Dealer + +![hello](chia-wallet-dealer.png) + +## Description +Welcome to Chia RPC XCH Wallet Dealer. The purpose of this project is fractionate and send XCH of an existing and synced chia wallet (using the Chia RPC Protocol) to multiple wallets destinations, using partitions rules on a config file. +Developed and tested only with the official [chia-blockchain](https://github.com/Chia-Network/chia-blockchain) project. + +If you found the project useful, please consider a donation to my chia wallet :) + +`xch1vt3g694eclvcjmrj8mq83vtgrva9sw0qdz34muxrqjh5y5fzq6vq89n605` + +## Requirements +Attending the official [chia RPC doc](https://docs.chia.net/docs/12rpcs/rpcs/) you need a wallet listening via RPC Protocol, built with a default installation. + +You may also need Python interpreter (3.x) installed on your system and it is recommended to use pipenv for installing all libraries dependencys. You can install it with + +```bash +pip3 install pipenv +``` + +## Usage +### The environment +* Install the environment with + +```bash +pipenv install +``` + +* Enter in the environment with + +```bash +pipenv shell +``` + +You can also execute xchdealer without entering in the environment, using `pipenv run python xchdealer.py [params]`, example: + +``` +pipenv run python xchdealer.py --help + +usage: xchdealer.py [-h] -f CONFIG_FILE [-m {simulate,execute}] +... + +``` +### Edit the config +* Edit config_example.yaml to your needs and rename it if you want + +## Config file +Example of config file: +```yaml +--- +rpc_connector: + host: "localhost" #Host of the wallet + port: 9256 #Port of the rpc wallet + private_wallet_cert_path: "~/.chia/mainnet/config/ssl/wallet/private_wallet.crt" #Private certificate to connect with RPC + private_wallet_key_path: "~/.chia/mainnet/config/ssl/wallet/private_wallet.key" #Key of the certificate to connect with RP +dealer: + source_wallet: + id: 1 #ID of source wallet, can be identified launching in simulate mode + distribute_percentage_of_total: 100.0 #Percentage of all amount will be distributed, the rest keep on the wallet + fee: 0 #Transaction fee, default 0 + destination_wallets: + - name: "iv-vz" #For each destination specify a name + address: "xch1vt3g694eclvcjmrj8mq83vtgrva9sw0qdz34muxrqjh5y5fzq6vq89n605" #Address of the destination wallet + distribution_percentage: 90.0 #Percentage of total amount assigned to this wallet + - name: "alber" + address: "xch1vt3g694eclvcjmrj8mq83vtgrva9sw0qdz34muxrqjh5y5fzq6vq89n605" + distribution_percentage: 10.0 + +#The sum of all distribution_percentage of destination_wallets must be equal to 100.0 +``` + +## Arguments + +### Simulate mode + +* You can execute xchdealer in simulate mode to show only the dealing results, check the config or identify the id of source wallet. Run: + +```bash +python xchdealer.py -f config_example.yaml -m simulate +``` + +### Execute mode + +* If you are sure of the result, you can execute the real transactions with: + +```bash +python xchdealer.py -f config_example.yaml -m execute +``` + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/chia-wallet-dealer.png b/chia-wallet-dealer.png new file mode 100644 index 0000000..8cf57ca Binary files /dev/null and b/chia-wallet-dealer.png differ diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..c56d337 --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,16 @@ +--- +rpc_connector: + host: "localhost" + port: 9256 + private_wallet_cert_path: "~/.chia/mainnet/config/ssl/wallet/private_wallet.crt" + private_wallet_key_path: "~/.chia/mainnet/config/ssl/wallet/private_wallet.key" + +dealer: + source_wallet: + id: 1 + distribute_percentage_of_total: 0.1 + fee: 0 + destination_wallets: + - name: "iv-vz" + address: "xch1vt3g694eclvcjmrj8mq83vtgrva9sw0qdz34muxrqjh5y5fzq6vq89n605" + distribution_percentage: 100.0 diff --git a/lib/dealermath/__init.py__ b/lib/dealermath/__init.py__ new file mode 100644 index 0000000..e69de29 diff --git a/lib/dealermath/dealermath.py b/lib/dealermath/dealermath.py new file mode 100644 index 0000000..abbec6a --- /dev/null +++ b/lib/dealermath/dealermath.py @@ -0,0 +1,25 @@ +class DealerMath(): + + @staticmethod + def mojo_to_xch_str(mojos=int): + max_send_amount_xch = (mojos / 1000000000000) + return(f'{max_send_amount_xch:.12f}') + + @staticmethod + def calculate_proportion(total_amount=int, percentage=float): + if DealerMath.is_between(0.0, percentage, 100.0): + return(int((percentage / 100) * total_amount)) + else: + raise Exception(f"Percentage values must be between 0.0 and 100.0! Specified: {str(percentage)}") + + @staticmethod + def check_sum(sum_elements=list, expected_total_value=float): + if all(isinstance(x, float) for x in sum_elements): + if sum(sum_elements) != expected_total_value: + raise Exception(f"The sum of {str(' + '.join(map(str, sum_elements)))} is not equal to {str(expected_total_value)}!") + else: + raise Exception(f"NOT all percentage values are written as floats!") + + @staticmethod + def is_between(a, x, b): + return min(a, b) <= x <= max(a, b) \ No newline at end of file diff --git a/lib/rpc/__init.py__ b/lib/rpc/__init.py__ new file mode 100644 index 0000000..e69de29 diff --git a/lib/rpc/xchrpc.py b/lib/rpc/xchrpc.py new file mode 100644 index 0000000..168f4d5 --- /dev/null +++ b/lib/rpc/xchrpc.py @@ -0,0 +1,92 @@ +import logging, requests, json +from os.path import expanduser +from requests.packages.urllib3.exceptions import InsecureRequestWarning +from lib.dealermath.dealermath import DealerMath + +class RemoteProcedureCall(): + + def __init__(self, host="localhost", port=9256, private_wallet_cert_path="~/.chia/mainnet/config/ssl/wallet/private_wallet.crt", private_wallet_key_path="~/.chia/mainnet/config/ssl/wallet/private_wallet.key"): + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + + self.default_rpc_headers = {'Content-Type': 'application/json'} + self.default_wallet_certs = (expanduser(private_wallet_cert_path), expanduser(private_wallet_key_path)) + self.host = host + self.port = port + + logging.info(f"RPC connector set to {self.host}:{str(self.port)} using certs {str(self.default_wallet_certs)}") + + + def check_available_wallets(self): + logging.info('Checking available RPC chia wallets') + + request_data = {"wallet_id": "*"} + try: + response = requests.post(f"https://{self.host}:{str(self.port)}/get_wallets", headers=self.default_rpc_headers, json=request_data, cert=self.default_wallet_certs, verify=False) + response.raise_for_status() + except Exception as e: + logging.error(f"Cannot get available RPC chia wallets {str(e)}") + return(False) + else: + available_wallets = json.loads(response.text)['wallets'] + logging.info(f"Connection with chia RPC protocol sucessfull") + logging.info(f"Available wallets: {available_wallets}") + return(available_wallets) + + def check_wallets_synced(self): + logging.info(f"Checking chia wallets synced") + + request_data = {} + try: + response = requests.post(f"https://{self.host}:{str(self.port)}/get_sync_status", headers=self.default_rpc_headers, json=request_data, cert=self.default_wallet_certs, verify=False) + response.raise_for_status() + except Exception as e: + logging.error(f"Cannot get RPC chia wallets sync status {str(e)}") + return(False) + else: + loaded_json = json.loads(response.text) + if loaded_json["syncing"]: + logging.info(f"Wallets are syncing with network") + else: + logging.info(f"Wallets are NOT syncing with network") + + if loaded_json["synced"]: + logging.info(f"Wallets are correctly synced with network") + else: + logging.warning(f"Wallets are NOT synced with network") + return(False) + + def check_wallet_balance(self, wallet_id=int): + logging.info(f"Checking XCH balance on wallet id {str(wallet_id)}") + + request_data = {"wallet_id": wallet_id} + try: + response = requests.post(f"https://{self.host}:{str(self.port)}/get_wallet_balance", headers=self.default_rpc_headers, json=request_data, cert=self.default_wallet_certs, verify=False) + response.raise_for_status() + except Exception as e: + logging.error(f"Cannot get available RPC chia wallets {str(e)}") + return(False) + else: + max_send_amount_mojo = int(json.loads(response.text)["wallet_balance"]["max_send_amount"]) + max_send_amount_xch_str = DealerMath.mojo_to_xch_str(max_send_amount_mojo) + logging.info(f"Available balance for sending (max_send_amount): {str(max_send_amount_mojo)} MOJOs == {str(max_send_amount_xch_str)} XCH") + + return(max_send_amount_mojo, max_send_amount_xch_str) + + def send_wallet_transaction(self, source_wallet_id=int, amount=int, destination_wallet_address=str, fee=0): + logging.info(f"Sending {str(amount)} MOJO to address {destination_wallet_address}") + + request_data = { + "wallet_id": source_wallet_id, + "amount": amount, + "address": destination_wallet_address, + "fee": fee + } + try: + response = requests.post(f"https://{self.host}:{str(self.port)}/send_transaction", headers=self.default_rpc_headers, json=request_data, cert=self.default_wallet_certs, verify=False) + response.raise_for_status() + except Exception as e: + logging.error(str(e)) + return(False) + + logging.info(response.text) + diff --git a/xchdealer.py b/xchdealer.py new file mode 100755 index 0000000..e1ea096 --- /dev/null +++ b/xchdealer.py @@ -0,0 +1,113 @@ +#!/bin/python +import argparse, requests, logging, sys, yaml +from lib.rpc.xchrpc import RemoteProcedureCall +from lib.dealermath.dealermath import DealerMath + +def set_app_logger(): + root = logging.getLogger() + root.setLevel(logging.INFO) + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.INFO) + formatter = logging.Formatter('[%(levelname)s]|%(asctime)s|%(filename)s|%(funcName)s|%(message)s') + handler.setFormatter(formatter) + root.addHandler(handler) + +def get_app_argparse(): + parser = argparse.ArgumentParser(description='Fractionate and send XCH of a chia wallet (using the Chia RPC Protocol) to multiple wallets destinations using partitions rules on a config file.') + + parser.add_argument('-f', '--config-file', type=str, help='YAML config file', required=True) + parser.add_argument('-m', '--mode', + type=str, + choices=['simulate', 'execute'], + default='simulate', required=False, + help='Simulate only or really execute the deal') + + global args + args = parser.parse_args() + +def load_config_file(): + global config + try: + with open(args.config_file, 'r') as file: + config = yaml.full_load(file) + except Exception as e: + logging.error(f"Cannot open config file {args.config_file}: {str(e)}") + exit(1) + else: + logging.info(f"Successfully loaded config file {args.config_file}") + +def calculate_deals(total_mojos, total_xch_str): + logging.info(f"Calculating proportions of deal") + dealing_total_mojo = DealerMath.calculate_proportion(total_mojos, float(config['dealer']['source_wallet']['distribute_percentage_of_total'])) + + logging.info(f"{str(config['dealer']['source_wallet']['distribute_percentage_of_total'])}% of the total amount will be dealed: {str(dealing_total_mojo)} MOJOs") + + deals = {} + + for count, dest in enumerate(config['dealer']['destination_wallets']): + will_receive_mojos = DealerMath.calculate_proportion(dealing_total_mojo, float(config['dealer']['destination_wallets'][count]['distribution_percentage'])) + deals[dest['name']] = {'address': dest['address'], 'mojo': will_receive_mojos, 'xch': DealerMath.mojo_to_xch_str(will_receive_mojos)} + logging.info(f"{dest['name']} will receive {will_receive_mojos} MOJOs == {DealerMath.mojo_to_xch_str(will_receive_mojos)} XCH") + + return(deals) + +def check_percentages(): + try: + DealerMath.check_sum([d['distribution_percentage'] for d in config['dealer']['destination_wallets']], 100.0) + except Exception as e: + logging.error(f"Incorrect configurated distribution percentages: {str(e)}") + exit(1) + else: + logging.info(f"All percentages in config are correctly set and pass checksum test") + +def default_routine(): + available_wallets = rpc.check_available_wallets() + if available_wallets == False: + exit(1) + + if any(d['id'] == config["dealer"]["source_wallet"]["id"] for d in available_wallets): + logging.info(f"Configured source wallet id: {str(config['dealer']['source_wallet']['id'])} is available, using it to operate") + else: + logging.error(f"Configured source wallet id: {str(config['dealer']['source_wallet']['id'])} not in available wallets") + exit(1) + + max_send_amount_mojo, max_send_amount_xch_str = rpc.check_wallet_balance(wallet_id=int(config['dealer']['source_wallet']['id'])) + check_percentages() + deals = calculate_deals(max_send_amount_mojo, max_send_amount_xch_str) + + for name, data in deals.items(): + logging.info(f"Preparing transaction to {name} with {data['xch']} XCH") + rpc.send_wallet_transaction(source_wallet_id=config['dealer']['source_wallet']['id'], amount=data['mojo'], destination_wallet_address=data['address']) + +def check_only_routine(): + rpc.check_available_wallets() + rpc.check_wallets_synced() + max_send_amount_mojo, max_send_amount_xch_str = rpc.check_wallet_balance(wallet_id=int(config['dealer']['source_wallet']['id'])) + check_percentages() + deals = calculate_deals(max_send_amount_mojo, max_send_amount_xch_str) + +if __name__ == "__main__": + get_app_argparse() + set_app_logger() + logging.info(f"Welcome to Chia XCH RPC Dealer") + load_config_file() + + rpc = RemoteProcedureCall( + host=config["rpc_connector"]["host"], + port=int(config["rpc_connector"]["port"]), + private_wallet_cert_path=config["rpc_connector"]["private_wallet_cert_path"], + private_wallet_key_path=config["rpc_connector"]["private_wallet_key_path"]) + + logging.info(f"XCH Dealer routine set to {args.mode} mode") + + if args.mode == "simulate": + check_only_routine() + logging.info(f"Simulate mode finished, if you want to apply and send XCH launch xchdealer in 'execute' mode") + elif args.mode == "execute": + default_routine() + else: + logging.error(f"Only simulate or execute modes can be set") + exit(1) + exit(0) + +