From db40fa35ca4c95229593f72e5fd215d553bf4976 Mon Sep 17 00:00:00 2001 From: cxloe <77425619+cxloe@users.noreply.github.com> Date: Mon, 5 Dec 2022 17:48:57 -0500 Subject: [PATCH 1/4] build / submit transaction test --- python-library/test-suite/build-submit.py | 185 ++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 python-library/test-suite/build-submit.py diff --git a/python-library/test-suite/build-submit.py b/python-library/test-suite/build-submit.py new file mode 100644 index 000000000..cf367e985 --- /dev/null +++ b/python-library/test-suite/build-submit.py @@ -0,0 +1,185 @@ +import argparse +import asyncio +import json +import subprocess +import sys +import os +#repo_root_dir = subprocess.check_output("git rev-parse --show-toplevel", shell=True).decode("utf8").strip() +repo_root_dir = os.environ.get("PWD") +sys.path.append("{}/python-library".format(repo_root_dir)) + +from fullservice import FullServiceAPIv2 as v2 +from FSDataObjects import Response, Account + +sleepy_time = 15 +default_config_path = "./test_config.json" +config = json.loads(open(default_config_path).read()) +account_ids = [] + +fs = v2() + +# TODO: this is very uggly and needs to be refactored +async def wait_for_account_to_sync(id): + account_status = await fs.get_account_status(id) + while ((account_status.get("result").get("account").get("next_block_index") + != account_status.get("result").get("local_block_height")) or (account_status.get("result").get("balance_per_token").get("0").get("pending") != "0")): + await asyncio.sleep(sleepy_time) + account_status = await fs.get_account_status(id) + + +async def test_cleanup(): + global account_ids + for id in account_ids: + await wait_for_account_to_sync(id) + await fs.remove_account(id) + accounts = await fs.get_accounts() + for id in account_ids: + assert id not in accounts.get('result').get('account_ids'),"Failed to clear out accounts" + account_ids = [] + + +# If this test fails before reaching the last cleanup step, we have leftover +# artifacts in the FS instance. We clean up those residual artifacts here. +# Note: Using a testing framework like pytest would allow us to bundle this in +# a test fixture +async def preclean_this_test(): + await get_account(0, "alice", True) + await get_account(1, "bob", True) + await test_cleanup() + + +def get_mnemonics(n=2): + if n > len(config["Account Mnemonics"]): + raise ValueError("Not enough account available in config") + return config["Account Mnemonics"][:n] + + +async def get_account(i, name="", okay_if_already_imported=False): + global account_ids + print(config) + print(i) + + mnemonic = config["Account Mnemonics"][i]["mnemonic"] + account = await fs.import_account( + mnemonic, + "2", # This parameter indicates that we are using the 2nd key derivations method (mnemonics) + name=name + ) + + if not okay_if_already_imported: + assert "error" not in account.keys(), "Failed to import account" + + # Newly imported + if "error" not in account.keys(): + result = Account(account["result"]["account"]) + account_ids.append(result.id) + # Previously imported + else: + error_msg = account.get("error").get("data").get("details") + assert error_msg.startswith("Error interacting& with the database: Account already exists:"), "Unknown import failure" + id = error_msg.split()[-1] + result = Response(await fs.get_account_status(id)).account + account_ids.append(result.id) + return result + + +async def wait_for_network_sync(): + network_status = await fs.get_network_status() + while network_status.get('result').get('network_block_height') != network_status.get('result').get('local_block_height'): + print("Sleep") + await asyncio.sleep(sleepy_time) + network_status = await fs.get_network_status() + + +async def main(): + while (await fs.get_wallet_status())['result']['wallet_status']['is_synced_all'] != True: + await asyncio.sleep(sleepy_time) + await does_it_go() + + +async def does_it_go(amount_pmob: int = 600000000) -> bool: + network_status = await fs.get_network_status() + assert "error" not in network_status.keys(), "Failed to get network status" + fee = int(network_status.get("result") + .get("network_status") + .get("fees") + .get("0") # zero is the fee key for mob + ) + + """Test Setup """ + pmob_to_send = amount_pmob + + # await preclean_this_test() + + alice = await get_account(0, "alice", True) + bob = await get_account(1, "bob", True) + + await wait_for_account_to_sync(alice.id) + await wait_for_account_to_sync(bob.id) + + alice_balance_0 = int( + (await fs.get_account_status(alice.id)) + .get("result") + .get("balance_per_token") + .get("0") + .get("unspent") + ) + + assert alice_balance_0 >= pmob_to_send + fee, "Insufficient funds in first account." + + bob_balance_0 = int( + (await fs.get_account_status(bob.id)) + .get("result") + .get("balance_per_token") + .get("0") + .get("unspent") + ) + built_transaction = await fs.build_transaction( + alice.id, + recipient_public_address=bob.main_address, + amount={"value": str(pmob_to_send), "token_id": str(0)}, + ) + + first_transaction = await fs.submit_transaction( + tx_proposal=built_transaction.get("result").get("tx_proposal"), + ) + + # TODO: replace this with a poll loop that waits a block or two + await wait_for_network_sync() + await wait_for_account_to_sync(alice.id) + await wait_for_account_to_sync(bob.id) + + """ Check Results """ + alice_balance_1 = int( + (await fs.get_account_status(alice.id)) + .get("result") + .get("balance_per_token") + .get("0") + .get("unspent") + ) + + bob_balance_1 = int( + (await fs.get_account_status(bob.id)) + .get("result") + .get("balance_per_token") + .get("0") + .get("unspent") + ) + + assert alice_balance_0 == alice_balance_1 + fee + pmob_to_send, "Alice doesn't end with the expected amount" + assert bob_balance_1 == bob_balance_0 + pmob_to_send, "Bob doesn't end with the expected amount" + + # await test_cleanup() + +def not_main(): + parser = argparse.ArgumentParser(description="Basic test") + parser.add_argument("config_path", nargs='?', type=str, default=default_config_path) + args = parser.parse_args() + + with open(args.config_path) as json_file: + config = json.load(json_file) + + asyncio.run(main()) + +if __name__ == "__main__": + not_main() From 55f17517eff87f1cfd2151d1c9ebffb00e0272fe Mon Sep 17 00:00:00 2001 From: cxloe <77425619+cxloe@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:07:53 -0500 Subject: [PATCH 2/4] partial typing for fullservice.py --- python-library/fullservice.py | 56 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/python-library/fullservice.py b/python-library/fullservice.py index c1cea8e4c..3691ac6a2 100644 --- a/python-library/fullservice.py +++ b/python-library/fullservice.py @@ -5,7 +5,7 @@ import json import logging import base64 -from typing import Optional +from typing import Any, Dict, List, Union, Optional import ssl import forest_utils as utils from rich import print_json @@ -28,7 +28,7 @@ class Request: - def __init__(self, logLevel = logging.ERROR): + def __init__(self, logLevel: int = logging.ERROR) -> None: self.logger = utils.logger url = utils.get_secret('URL') @@ -47,7 +47,7 @@ async def req(self, request_data: dict, ) -> dict: self.logger.info(response_data) return response_data - async def request(self, request_data: dict): + async def request(self, request_data: dict) -> Dict[str, Any]: request_data = {"jsonrpc": "2.0", "id": "1", **request_data} # TODO: come back and sanitize # self.logger.debug(f"request data: {request_data}") @@ -133,16 +133,16 @@ async def build_burn_transaction( async def build_transaction( self, - account_id, - addresses_and_amounts="", - recipient_public_address="", - amount="", - input_txo_ids="", - fee_value="", - fee_token_id="", - tombstone_block="", - max_spendable_value="", - ): + account_id: str, + addresses_and_amounts: str="", + recipient_public_address: str="", + amount: Dict[str, str]="", + input_txo_ids: str="", + fee_value: str="", + fee_token_id: str="", + tombstone_block: str="", + max_spendable_value: str="", + ) -> Dict[str, Union[str, Dict[str, Union[Dict[str, Union[List[Dict[str, Union[str, Dict[str, str]]]], Dict[str, str], str]], str]]]]: return await self.req( { "method": "build_transaction", @@ -276,8 +276,8 @@ async def export_account_secrets( async def get_account_status( self, - account_id, - ): + account_id: str, + ) -> Dict[str, Union[str, Dict[str, Union[Dict[str, Union[str, bool]], str, Dict[str, Dict[str, str]]]]]]: return await self.req( { "method": "get_account_status", @@ -393,10 +393,10 @@ async def validate_confirmation(self, account_id, txo_id="", confirmation=""): } ) - async def get_network_status(self): + async def get_network_status(self) -> Dict[str, Union[str, Dict[str, Dict[str, Union[str, Dict[str, str]]]]]]: return await self.req({"method": "get_network_status", "params": {}}) - async def get_wallet_status(self): + async def get_wallet_status(self) -> Dict[str, Union[str, Dict[str, Dict[str, Union[str, bool, Dict[str, Dict[str, str]]]]]]]: return await self.req({"method": "get_wallet_status", "params": {}}) async def version(self): @@ -519,13 +519,13 @@ async def get_txo_membership_proofs(self, outputs=""): async def import_account( self, - mnemonic, - key_derivation_version, - name="", - first_block_index="", - next_subaddress_index="", - fog_info="", - ): + mnemonic: str, + key_derivation_version: str, + name: str="", + first_block_index: str="", + next_subaddress_index: str="", + fog_info: str="", + ) -> Dict[str, Union[str, Dict[str, Union[int, str, Dict[str, str]]]]]: return await self.req( { "method": "import_account", @@ -595,7 +595,7 @@ async def sample_mixins(self, num_mixins="", excluded_outputs=""): async def submit_transaction( self, - tx_proposal={ + tx_proposal: Dict[str, Union[List[Dict[str, Union[str, Dict[str, str]]]], Dict[str, str], str]]={ "input_txos": [], "payload_txos": [], "change_txos": [], @@ -603,9 +603,9 @@ async def submit_transaction( "tombstone_block_index": "", "tx_proto": "", }, - comment="", - account_id="", - ): + comment: str="", + account_id: str="", + ) -> Dict[str, Union[str, Dict[str, None]]]: return await self.req( { "method": "submit_transaction", From 5b7664ee4ddd74ddf1e7a54be3b81194cd3ede3e Mon Sep 17 00:00:00 2001 From: cxloe <77425619+cxloe@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:08:17 -0500 Subject: [PATCH 3/4] format fullservice.py --- python-library/fullservice.py | 83 +++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/python-library/fullservice.py b/python-library/fullservice.py index 3691ac6a2..32d4a563b 100644 --- a/python-library/fullservice.py +++ b/python-library/fullservice.py @@ -30,9 +30,13 @@ class Request: def __init__(self, logLevel: int = logging.ERROR) -> None: self.logger = utils.logger - url = utils.get_secret('URL') - async def req(self, request_data: dict, ) -> dict: + url = utils.get_secret("URL") + + async def req( + self, + request_data: dict, + ) -> dict: logging.info("request: %s", request_data.get("method")) if len(request_data["params"]) > 0: request_data["params"] = { @@ -134,15 +138,34 @@ async def build_burn_transaction( async def build_transaction( self, account_id: str, - addresses_and_amounts: str="", - recipient_public_address: str="", - amount: Dict[str, str]="", - input_txo_ids: str="", - fee_value: str="", - fee_token_id: str="", - tombstone_block: str="", - max_spendable_value: str="", - ) -> Dict[str, Union[str, Dict[str, Union[Dict[str, Union[List[Dict[str, Union[str, Dict[str, str]]]], Dict[str, str], str]], str]]]]: + addresses_and_amounts: str = "", + recipient_public_address: str = "", + amount: Dict[str, str] = "", + input_txo_ids: str = "", + fee_value: str = "", + fee_token_id: str = "", + tombstone_block: str = "", + max_spendable_value: str = "", + ) -> Dict[ + str, + Union[ + str, + Dict[ + str, + Union[ + Dict[ + str, + Union[ + List[Dict[str, Union[str, Dict[str, str]]]], + Dict[str, str], + str, + ], + ], + str, + ], + ], + ], + ]: return await self.req( { "method": "build_transaction", @@ -277,7 +300,15 @@ async def export_account_secrets( async def get_account_status( self, account_id: str, - ) -> Dict[str, Union[str, Dict[str, Union[Dict[str, Union[str, bool]], str, Dict[str, Dict[str, str]]]]]]: + ) -> Dict[ + str, + Union[ + str, + Dict[ + str, Union[Dict[str, Union[str, bool]], str, Dict[str, Dict[str, str]]] + ], + ], + ]: return await self.req( { "method": "get_account_status", @@ -393,10 +424,17 @@ async def validate_confirmation(self, account_id, txo_id="", confirmation=""): } ) - async def get_network_status(self) -> Dict[str, Union[str, Dict[str, Dict[str, Union[str, Dict[str, str]]]]]]: + async def get_network_status( + self, + ) -> Dict[str, Union[str, Dict[str, Dict[str, Union[str, Dict[str, str]]]]]]: return await self.req({"method": "get_network_status", "params": {}}) - async def get_wallet_status(self) -> Dict[str, Union[str, Dict[str, Dict[str, Union[str, bool, Dict[str, Dict[str, str]]]]]]]: + async def get_wallet_status( + self, + ) -> Dict[ + str, + Union[str, Dict[str, Dict[str, Union[str, bool, Dict[str, Dict[str, str]]]]]], + ]: return await self.req({"method": "get_wallet_status", "params": {}}) async def version(self): @@ -521,10 +559,10 @@ async def import_account( self, mnemonic: str, key_derivation_version: str, - name: str="", - first_block_index: str="", - next_subaddress_index: str="", - fog_info: str="", + name: str = "", + first_block_index: str = "", + next_subaddress_index: str = "", + fog_info: str = "", ) -> Dict[str, Union[str, Dict[str, Union[int, str, Dict[str, str]]]]]: return await self.req( { @@ -595,7 +633,9 @@ async def sample_mixins(self, num_mixins="", excluded_outputs=""): async def submit_transaction( self, - tx_proposal: Dict[str, Union[List[Dict[str, Union[str, Dict[str, str]]]], Dict[str, str], str]]={ + tx_proposal: Dict[ + str, Union[List[Dict[str, Union[str, Dict[str, str]]]], Dict[str, str], str] + ] = { "input_txos": [], "payload_txos": [], "change_txos": [], @@ -603,8 +643,8 @@ async def submit_transaction( "tombstone_block_index": "", "tx_proto": "", }, - comment: str="", - account_id: str="", + comment: str = "", + account_id: str = "", ) -> Dict[str, Union[str, Dict[str, None]]]: return await self.req( { @@ -1180,4 +1220,3 @@ async def verify_address(self, address=""): return await self.req( {"method": "verify_address", "params": {"address": address}} ) - From 89f82d5e0012f71ec8b99e52ebd22d18c0909432 Mon Sep 17 00:00:00 2001 From: cxloe <77425619+cxloe@users.noreply.github.com> Date: Mon, 5 Dec 2022 18:13:34 -0500 Subject: [PATCH 4/4] quick fix for wait for account to sync, rename not_main --- python-library/{test-suite => }/build-submit.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) rename python-library/{test-suite => }/build-submit.py (95%) diff --git a/python-library/test-suite/build-submit.py b/python-library/build-submit.py similarity index 95% rename from python-library/test-suite/build-submit.py rename to python-library/build-submit.py index cf367e985..4116740d1 100644 --- a/python-library/test-suite/build-submit.py +++ b/python-library/build-submit.py @@ -4,8 +4,10 @@ import subprocess import sys import os -#repo_root_dir = subprocess.check_output("git rev-parse --show-toplevel", shell=True).decode("utf8").strip() -repo_root_dir = os.environ.get("PWD") +repo_root_dir = subprocess.check_output("git rev-parse --show-toplevel", shell=True).decode("utf8").strip() + +# uncomment to happily run locally +#repo_root_dir = os.environ.get("PWD") sys.path.append("{}/python-library".format(repo_root_dir)) from fullservice import FullServiceAPIv2 as v2 @@ -25,6 +27,7 @@ async def wait_for_account_to_sync(id): != account_status.get("result").get("local_block_height")) or (account_status.get("result").get("balance_per_token").get("0").get("pending") != "0")): await asyncio.sleep(sleepy_time) account_status = await fs.get_account_status(id) + await asyncio.sleep(sleepy_time) async def test_cleanup(): @@ -165,13 +168,13 @@ async def does_it_go(amount_pmob: int = 600000000) -> bool: .get("0") .get("unspent") ) - + assert alice_balance_0 == alice_balance_1 + fee + pmob_to_send, "Alice doesn't end with the expected amount" assert bob_balance_1 == bob_balance_0 + pmob_to_send, "Bob doesn't end with the expected amount" # await test_cleanup() -def not_main(): +def run_test(): parser = argparse.ArgumentParser(description="Basic test") parser.add_argument("config_path", nargs='?', type=str, default=default_config_path) args = parser.parse_args() @@ -182,4 +185,4 @@ def not_main(): asyncio.run(main()) if __name__ == "__main__": - not_main() + run_test()