diff --git a/HeadlineHackathon/Dreamf1re_hack/README.md b/HeadlineHackathon/Dreamf1re_hack/README.md new file mode 100644 index 0000000..3ee3c8b --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/README.md @@ -0,0 +1,17 @@ +# Choice Texts +This is a web app, designed with PyWebIO, which aims to make text files permanent on the Algorand Blockchain. + +## Usage +1. Download this entire folder. +2. Install required packages using ```pip install -r requirements.txt```. +3. Edit Environment Variables -> add variable name "test_mnemonic" with variable value which is your test mnemonic.
Make sure to fund this account with TestNet ALGOs. +5. Run deploy.py through ```python deploy.py```. + +Running deploy.py will start a new tab in the user's default +web browser. All functionalities therein are intact and the user +can now start making text files permanent on the Algorand blockchain. + +## Notes +Only use text files that are allowed to be accessed publicly. Never use this web app with
+text files containing private information as they will be made permanent in the Algorand blockchain
+which is a ```public``` blockchain. diff --git a/HeadlineHackathon/Dreamf1re_hack/bridge.py b/HeadlineHackathon/Dreamf1re_hack/bridge.py new file mode 100644 index 0000000..f778b3f --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/bridge.py @@ -0,0 +1,31 @@ +from upload import upload, get_file_id +from download import download +from util import TEST_SENDER_ADDRESS, TEST_SENDER_PRIVATE_KEY, init_post_client + + +def upload_pdf(filename: str): + # Upload sample, returns a Transaction ID + # needed to retrieve the file from the blockchain + print("Procedure: Upload file to blockchain.") + txnids = upload( + filename=filename, + sender_address=TEST_SENDER_ADDRESS, + sender_private_key=TEST_SENDER_PRIVATE_KEY + ) + fid = get_file_id( + transaction_ids=txnids, + receiver_address=TEST_SENDER_ADDRESS, + sender_address=TEST_SENDER_ADDRESS, + sender_private_key=TEST_SENDER_PRIVATE_KEY, + post_client=init_post_client(), + filename=filename + ) + print(f"File ID: {fid}") + return fid + + +def download_pdf(file_id: str): + # Download sample, saves to current directory + print("Procedure: Download file from blockchain.") + filedld, filedldname = download(file_id=file_id) + return filedld, filedldname diff --git a/HeadlineHackathon/Dreamf1re_hack/checking.py b/HeadlineHackathon/Dreamf1re_hack/checking.py new file mode 100644 index 0000000..4e8d66d --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/checking.py @@ -0,0 +1,40 @@ +import hashlib + + +# Vital function to check if +# the uploaded file and downloaded +# file are exactly the same. This +# is achieved through checking if +# the hash of the uploaded data is +# equal to the hash of the +# downloaded data +def check_circular(original: str, stitched: str): + """ + Check if hashes of Original File and Downloaded File are + the same. This is to check if the uploaded file is complete + and is the same with the original file that has been uploaded. + + :param original: The original file. + :param stitched: The downloaded file. + :return: Returns True if original and downloaded hashes are the same. + """ + print(f'\nChecking circularity...') + stitched_hash = hashlib.md5(stitched.encode()).hexdigest() + original_hash = hashlib.md5(original.encode()).hexdigest() + if stitched_hash == original_hash: + print('Achieved circularity.') + return True + else: + print(f'Length of original: {len(original)} Hash: {original_hash}') + print(f'Length of stitched: {len(stitched)} Hash: {stitched_hash}') + print(f'Circularity not achieved. Trying again.') + + +# Check if the expected Transaction ID +# location in the note is all in uppercase +def check_if_connection_exists(note: str): + targ = note[(len(note)-1)-51:len(note)] + if targ.isupper(): + return True + else: + return False diff --git a/HeadlineHackathon/Dreamf1re_hack/choice_logo.jpg b/HeadlineHackathon/Dreamf1re_hack/choice_logo.jpg new file mode 100644 index 0000000..ea3bb67 Binary files /dev/null and b/HeadlineHackathon/Dreamf1re_hack/choice_logo.jpg differ diff --git a/HeadlineHackathon/Dreamf1re_hack/constants.py b/HeadlineHackathon/Dreamf1re_hack/constants.py new file mode 100644 index 0000000..03ab12e --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/constants.py @@ -0,0 +1,4 @@ +ALGONODE_NODE_ADDRESS = "http://testnet-api.algonode.network" +ALGONODE_INDX_ADDRESS = "http://testnet-idx.algonode.network" +ALGOEXPL_NODE_ADDRESS = "https://node.testnet.algoexplorerapi.io" +ALGOEXPL_INDX_ADDRESS = "https://algoindexer.testnet.algoexplorerapi.io" diff --git a/HeadlineHackathon/Dreamf1re_hack/deploy.py b/HeadlineHackathon/Dreamf1re_hack/deploy.py new file mode 100644 index 0000000..2fdf2c7 --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/deploy.py @@ -0,0 +1,531 @@ +# In production, an escrow account must be newly generated and +# funded to delegate uploading of .txt files. A user must pay +# the fees required to upload the file +# In this demo, a test account is set and is pre-funded +# to show functionality. + +from upload import * +from pywebio.input import * +from pywebio.output import * +from pywebio.session import * +from pywebio.platform.tornado import start_server +from qrcode import QRCode, constants +from cv2 import cv2 +import base64 +import os +from util import TEST_SENDER_PRIVATE_KEY, \ + TEST_SENDER_ADDRESS, search_note_by_txid, get_lines, init_get_client +from checking import check_if_connection_exists +from PIL import Image + + +class Veritas: + def __init__(self): + self.sender_address = None + self.original_file = None + self.alpha_fn = None + self.alpha = None + self.filename = None + self.transaction_ids = None + self.receiver_address = self.sender_address + self.sender_private_key = TEST_SENDER_PRIVATE_KEY + + @use_scope("upload") + def to_blockchain(self): + self.sender_address = TEST_SENDER_ADDRESS + self.receiver_address = self.sender_address + # Remove scopes + remove("download") + remove("selector") + remove("manage") + put_button( + label="Reload", + onclick=self.reload_page + ) + # Show file upload + file = file_upload( + label="Find your text file", + required=True, + accept=[".txt"] + ) + # Write + self.filename = file['filename'] + open(self.filename, 'wb').write(file['content']) + obtained_from_local = open(self.filename, 'rb').read() + while True: + if (self.filename and obtained_from_local) is not None: + break + # Await payment from user before uploading + # Compute cost - A payment of choice can also + # be included on top of the base cost + cost = self.compute_cost(self.filename) + put_text(f"Estimated Cost: {round(cost, 5)} ALGO") + # Start upload + file_id = self.custom_upload( + filename=self.filename, + sender_address=self.sender_address, + receiver_address=self.receiver_address, + sender_private_key=self.sender_private_key + ) + put_html( + f""" +
+ Successfully made {self.filename} permanent.
+ Scan this QR Code to get the file.
+ Transaction: {file_id} +
+ """ + ) + # Add data to QR Code and make + basewidth = 100 + choice_logo = Image.open("choice_logo.jpg") + + wpercent = (basewidth / float(choice_logo.size[0])) + hsize = int((float(choice_logo.size[1]) * float(wpercent))) + choice_logo = choice_logo.resize((basewidth, hsize)) + qrc = QRCode( + error_correction=constants.ERROR_CORRECT_H + ) + + qrc.add_data(file_id) + qrc.make() + qrimg = qrc.make_image( + fill_color="black", back_color="white").convert('RGB') + pos = ((qrimg.size[0] - choice_logo.size[0]) // 2, + (qrimg.size[1] - choice_logo.size[1]) // 2) + + qrimg.paste(choice_logo, pos) + + # Save QR Code + qr_code_saved_fname = f"{self.filename}-QR.png" + qrimg.save(qr_code_saved_fname) + + # Open QR Code and display + with open(qr_code_saved_fname, 'rb') as qrimg_: + __qr__ = qrimg_.read() + if __qr__ is not None: + put_image( + src=__qr__, + format="png", + title="QR Code", + width="185", + height="185", + ) + put_file( + name=qr_code_saved_fname, + content=__qr__, + label=f"Download QR" + ) + put_link( + name="See on Algoexplorer", + url=f"https://testnet.algoexplorer.io/tx/{file_id}", + new_window=True + ) + os.remove(self.filename) + os.remove(qr_code_saved_fname) + + @use_scope("download") + def from_blockchain(self): + # Remove scopes + remove("upload") + remove("selector") + remove("manage") + put_button( + label="Reload", + onclick=self.reload_page, + position=0 + ) + # Get output + qrcode = file_upload( + label="Locate QR Code.", + accept=".png", + required=True + ) + pb_download = "downloadprog" + put_processbar( + name=pb_download, + init=0, + label="Setting the truth free...", + auto_close=True + ) + filename = qrcode['filename'] + open(filename, 'wb').write(qrcode['content']) + obtained_from_local = open(filename, 'rb').read() + while True: + if (qrcode and obtained_from_local) is not None: + break + # Decode QR + qr_ = cv2.imread(filename) + qr__ = cv2.QRCodeDetector() + file_id, _, _ = qr__.detectAndDecode(qr_) + # Initiate download + # Initialize stuff to be used later + get_client = init_get_client() + remnant = [] + first = True + connection = None + fno = None + file_name_decoded = None + # Repeat until there is no Connection left. + # A Connection is the Transaction ID + # included at the end of a note to serve as + # a link to the preceding note. + set_processbar( + name=pb_download, + value=0.25, + label="Setting the truth free..." + ) + while True: + if first: + gotten = search_note_by_txid( + get_client=get_client, + txid=file_id + ) + first = False + else: + gotten = search_note_by_txid( + get_client=get_client, + txid=connection + ) + if gotten != "": + has_connection = check_if_connection_exists(gotten) + # Check if a Transaction ID is + # expected to be found in the note, + # thus hinting that there is a preceding + # note. if "" is found in the note, + # it means that the there are no more + # preceding notes. + if has_connection and not ("" in gotten): + connection = gotten[(len(gotten) - 1) - 51:] + actual = gotten[:(len(gotten) - 1) - 51] + remnant.append(actual) + else: + actual = gotten[:] + remnant.append(actual) + break + else: + break + # Arrange the reference line + # to link other Transaction IDs + set_processbar( + name=pb_download, + value=0.25, + label="Setting the truth free..." + ) + remnant.reverse() + omega = "" + for particle in remnant: + omega += particle + if "" in particle: + sidx = particle.index("") + idxstart_of_fn = sidx + 4 + idxend_of_fn = idxstart_of_fn + # Get index of end of filename + while particle[idxend_of_fn] != "<": + idxend_of_fn += 1 + # Get filename + fno = particle[idxstart_of_fn:idxend_of_fn] + file_name_decoded = base64.b64decode(fno.encode()).decode('iso-8859-1') + print(f"File name: {file_name_decoded} ") + print(f"File ID: {file_id}") + print(f"File description: ") + # An algorithm can be inserted here to get + # the file description if there is one included + set_processbar( + name=pb_download, + value=0.50, + label=f"Getting {file_name_decoded}..." + ) + filename_whole = f"{fno}" + if filename_whole in omega: + omega = omega.replace(filename_whole, "") + else: + print("Cannot edit omega") + transaction_ids = get_lines( + note=omega, + max_length=52 + ) + while True: + try: + # Get Transaction IDs from the + # Transaction IDs obtained from the File ID + txn_ids = get_txn_ids_from_txn_id( + __txids=transaction_ids, + client=get_client + ) + set_processbar( + name=pb_download, + value=0.75, + label=f"Getting {file_name_decoded}..." + ) + # Download the file from the blockchain + downloaded_file = stitch_records( + get_client=get_client, + txn_ids=txn_ids + ) + set_processbar( + name=pb_download, + value=0.85, + label=f"Getting {file_name_decoded}..." + ) + break + except Exception as err: + print(err.args) + set_processbar( + name=pb_download, + value=1, + label=f"Getting {file_name_decoded}..." + ) + filedld = downloaded_file + filedldname = file_name_decoded + # End download + put_text(f"Successfully obtained {filedldname}.") + put_file( + name=filedldname, + content=base64.b64decode(filedld).decode().encode("ISO-8859-1"), + label=f"Download file" + ) + put_text("Text:") + put_scrollable( + base64.b64decode(filedld).decode().encode("ISO-8859-1").decode(), + height=800 + ) + os.remove(filename) + + @use_scope("selector") + def selector(self): + remove("init") + remove("upload") + remove("download") + # Add buttons + put_button( + label="Make something permanent", + onclick=self.to_blockchain, + ) + put_button( + label="Retrieve document", + onclick=self.from_blockchain, + ) + + def root(self): + remove("main") + remove("connect") + remove("init") + put_scope("main") + put_scope("connect") + # Set title + set_env(title="Choice Texts") + # Introduction + choice_logo = open("choice_logo.jpg", "rb").read() + put_grid( + [ + [ + put_html( + """ + +

+ Immutable, transparent
+ records on the blockchain
+

+

+ Choice Texts allows you
+ to make text files permanent
+ on the Algorand public blockchain.
+
+ Now there is a way to preserve
+ our stories for generations
+ and generations to come. +

+ + """, + ), + put_image( + src=choice_logo, + format=".jpg", + width="250", + height="250", + title="Choice Logo" + ) + ] + ], + scope="main" + ) + + with use_scope("init") as init: + put_button( + label="Begin", + onclick=self.selector, + scope=init + ) + + def custom_upload( + self, + filename: str, + sender_address: str, + receiver_address: str, + sender_private_key: str + ): + # Init POST Client + post_client = init_post_client() + # Init process bar + process_bar_name = "uploadprog" + pbvi = 1 / 7 + put_processbar( + name=process_bar_name, + init=0, + label="Making sure things will never change...", + auto_close=True + ) + # Open file + # ( Progress - Task #1 ) + set_processbar(name=process_bar_name, value=pbvi) + with open(filename, 'rb') as o: + self.original_file = o.read().decode('ISO-8859-1') + self.original_file = base64.b64encode(self.original_file.encode()).decode() + if self.original_file is not None: + + # Get Transaction IDs submitted to the blockchain + # ( Progress - Task #2 ) + pbvi += pbvi + set_processbar( + name=process_bar_name, value=pbvi, + label="Making sure things will never change..." + ) + self.transaction_ids = process_publishing( + feed=self.original_file, + receiver_address=receiver_address, + sender_address=sender_address, + sender_private_key=self.sender_private_key + ) + # Initialize GET Client + get_client = init_get_client() + # Loop until downloaded data is exactly + # the same with the uploaded data + # ( Progress - Task #3 ) + pbvi += pbvi + set_processbar( + name=process_bar_name, value=pbvi, + label="Making sure things will never change..." + ) + while True: + # Get Transaction IDs from Transaction IDs + txn_ids = get_txn_ids_from_txn_id( + __txids=self.transaction_ids, + client=get_client + ) + # Download the uploaded file from the blockchain + downloaded_file = stitch_records( + get_client=get_client, + txn_ids=txn_ids + ) + # Check if the uploaded file + # and downloaded file are the same + # and return Transaction IDs from + # upload procedure if so + circular = check_circular( + original=self.original_file, + stitched=downloaded_file + ) + if circular: + print('File successfully uploaded to blockchain.') + break + # ( Progress - Task #4 ) + # Get File ID and second cost + pbvi += pbvi + set_processbar( + name=process_bar_name, value=pbvi, + label="Making sure things will never change..." + ) + print(f"Assigning File ID...Please Wait.") + alpha_fn = base64.b64encode(filename.encode()).decode() + alpha = f"{alpha_fn}" + for txid in self.transaction_ids: + alpha += txid + # ( Progress - Task #5 ) + pbvi += pbvi + set_processbar( + name=process_bar_name, value=pbvi, + label="Making sure things will never change..." + ) + feed = get_lines( + note=alpha, + max_length=947 + ) + txid = None + # ( Progress - Task #6 ) + pbvi += pbvi + set_processbar( + name=process_bar_name, value=pbvi, + label="Making sure things will never change..." + ) + for each in feed: + if len(each) != 0: + if len(feed) > 1: + if txid is None: + txn = create_transaction( + post_client, + receiver_address, + sender_address, + message=each + ) + else: + txn = create_transaction( + post_client, + receiver_address, + sender_address, + message=each + txid + ) + sgd = txn.sign(sender_private_key) + txid = post_client.send_transaction(sgd) + transaction.wait_for_confirmation( + algod_client=post_client, + txid=txid + ) + else: + txn = create_transaction( + post_client, + receiver_address, + sender_address, + message=each + ) + sgd = txn.sign(sender_private_key) + txid = post_client.send_transaction(sgd) + transaction.wait_for_confirmation( + algod_client=post_client, + txid=txid + ) + # ( Progress - Task #7 ) + pbvi += pbvi + set_processbar( + name=process_bar_name, value=1, + label="Making sure things will never change..." + ) + return txid + else: + raise Exception("Error: original file is {None}") + + @staticmethod + def reload_page(): + run_js("window.location.reload();") + + def compute_cost(self, filename): + with open(filename, 'rb') as o: + self.original_file = o.read().decode('ISO-8859-1') + self.original_file = base64.b64encode(self.original_file.encode()).decode() + lines = get_lines(self.original_file, max_length=947) + algocost1 = len(lines) * 0.001 + groups = len(lines) / 16 + algocost2 = groups * 0.001 + total_cost = algocost2 + algocost1 + return total_cost + + +if __name__ == "__main__": + v = Veritas() + start_server( + applications=v.root, + port=0, + host="", + debug=True, + auto_open_webbrowser=True + ) diff --git a/HeadlineHackathon/Dreamf1re_hack/download.py b/HeadlineHackathon/Dreamf1re_hack/download.py new file mode 100644 index 0000000..bed3128 --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/download.py @@ -0,0 +1,155 @@ +from util import init_get_client, search_note_by_txid, get_lines, get_txn_ids_from_txn_id +import base64 +from checking import check_if_connection_exists +from stitching import stitch_records +from pywebio.output import set_processbar, put_processbar + + +# Main download procedure +def download(file_id: str): + """ + Download file from blockchain using the + File ID generated from upload procedure + + :param file_id: The link which is a File ID generated from upload. + :return: None, after downloading, the downloaded file will be in the same directory. + """ + # Initialize stuff to be used later + process_bar_name = "downloadprog" + put_processbar( + name=process_bar_name, + init=0, + label="Setting the truth free...", + auto_close=True + ) + pbd = 1/4 + set_processbar( + name=process_bar_name, + value=pbd, + label="Setting the truth free..." + ) + get_client = init_get_client() + remnant = [] + first = True + connection = None + fno = None + file_name_decoded = None + # Repeat until there is no Connection left. + # A Connection is the Transaction ID + # included at the end of a note to serve as + # a link to the preceding note. + pbd += pbd + set_processbar( + name=process_bar_name, + value=pbd, + label="Setting the truth free..." + ) + while True: + if first: + gotten = search_note_by_txid( + get_client=get_client, + txid=file_id + ) + first = False + else: + gotten = search_note_by_txid( + get_client=get_client, + txid=connection + ) + if gotten != "": + has_connection = check_if_connection_exists(gotten) + # Check if a Transaction ID is + # expected to be found in the note, + # thus hinting that there is a preceding + # note. if "" is found in the note, + # it means that the there are no more + # preceding notes. + if has_connection and not ("" in gotten): + connection = gotten[(len(gotten)-1)-51:] + actual = gotten[:(len(gotten)-1)-51] + remnant.append(actual) + else: + actual = gotten[:] + remnant.append(actual) + break + else: + break + # Arrange the reference line + # to link other Transaction IDs + pbd += pbd + set_processbar( + name=process_bar_name, + value=pbd, + label="Setting the truth free..." + ) + remnant.reverse() + omega = "" + for particle in remnant: + omega += particle + if "" in particle: + sidx = particle.index("") + idxstart_of_fn = sidx + 4 + idxend_of_fn = idxstart_of_fn + # Get index of end of filename + while particle[idxend_of_fn] != "<": + idxend_of_fn += 1 + # Get filename + fno = particle[idxstart_of_fn:idxend_of_fn] + file_name_decoded = base64.b64decode(fno.encode()).decode('iso-8859-1') + print(f"File name: {file_name_decoded} ") + print(f"File ID: {file_id}") + print(f"File description: ") + # An algorithm can be inserted here to get + # the file description if there is one included + pbd += pbd + set_processbar( + name=process_bar_name, + value=pbd, + label="Setting the truth free..." + ) + filename_whole = f"{fno}" + if filename_whole in omega: + omega = omega.replace(filename_whole, "") + else: + print("Cannot edit omega") + transaction_ids = get_lines( + note=omega, + max_length=52 + ) + pbd += pbd + set_processbar( + name=process_bar_name, + value=pbd, + label="Setting the truth free..." + ) + while True: + try: + # Get Transaction IDs from the + # Transaction IDs obtained from the File ID + txn_ids = get_txn_ids_from_txn_id( + __txids=transaction_ids, + client=get_client + ) + # Download the file from the blockchain + downloaded_file = stitch_records( + get_client=get_client, + txn_ids=txn_ids + ) + # Return if finished + return downloaded_file, file_name_decoded + except Exception as err: + print(err.args) + + +# Writes data to disk +def write_to_file(input_data: str, file_name_out: str): + """ + Write the downloaded file from blockchain to disk. + + :param input_data: The downloaded data from blockchain + :param file_name_out: Filename of output file + """ + with open(file_name_out, 'wb') as f: + to_be = base64.b64decode(input_data).decode() + f.write(to_be.encode("ISO-8859-1")) + print(f'\nDownloaded {file_name_out} to current directory') diff --git a/HeadlineHackathon/Dreamf1re_hack/requirements.txt b/HeadlineHackathon/Dreamf1re_hack/requirements.txt new file mode 100644 index 0000000..dbe3af1 Binary files /dev/null and b/HeadlineHackathon/Dreamf1re_hack/requirements.txt differ diff --git a/HeadlineHackathon/Dreamf1re_hack/stitching.py b/HeadlineHackathon/Dreamf1re_hack/stitching.py new file mode 100644 index 0000000..2080440 --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/stitching.py @@ -0,0 +1,44 @@ +from util import search_note_by_txid +from algosdk.v2client.algod import AlgodClient + + +# This function is to assemble +# the notes from each transaction +# in the upload procedure to +# produce the same uploaded file +def stitch_records( + get_client: AlgodClient, + txn_ids: list, +) -> str: + """ + Stitches notes from raw Transaction IDs + obtained from the upload procedure + + :param get_client: AlgodClient (GET) + :param txn_ids: Transaction IDs from upload procedure + :return: stitched - the Stitched Records (string) + """ + stitched = "" + stitched_initial_list = [] + for specific_txid in txn_ids: + while True: + # Search note based on given Transaction ID + obtained_note = search_note_by_txid( + get_client=get_client, + txid=specific_txid + ) + if obtained_note is not None: + # Append to list if note is found + stitched_initial_list.append(obtained_note) + now = txn_ids.index(specific_txid)+1 + mot = len(txn_ids) + num = round((now/mot)*100, 3) + print(f"\r({num}%) Stitching... ", end="") + + break + # Once all the notes are obtained, + # place them all in one single + # string and return + for sil in stitched_initial_list: + stitched += sil + return stitched diff --git a/HeadlineHackathon/Dreamf1re_hack/upload.py b/HeadlineHackathon/Dreamf1re_hack/upload.py new file mode 100644 index 0000000..85bc547 --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/upload.py @@ -0,0 +1,242 @@ +from algosdk.v2client import algod +from algosdk.future import transaction +from util import init_post_client, init_get_client, \ + get_lines, create_transaction, \ + get_txn_ids_from_txn_id +from stitching import stitch_records +from checking import check_circular +import base64 + + +# Core upload function +def process_publishing( + feed: str, + receiver_address: str, + sender_address: str, + sender_private_key: str +) -> list: + """ + This is the core upload procedure using the feed which is the string of + encoded bytes from the file to be uploaded. This returns the + Transaction IDs from submitted group transactions. + + :param feed: The raw string that is base64 encoded with encoding ISO-8859-1 + :param receiver_address: Algorand address of receiver + :param sender_address: Algorand address of sender + :param sender_private_key: Algorand private key of sender + :return: txids - the Transaction IDs of submitted atomic transactions + """ + # Initiate Algorand Client + post_client = init_post_client() + # Get lines from the feed + lines = get_lines(note=feed, max_length=947) + # Create transactions and append + transactions = [] + # The maximum number of transactions + # in a group transaction is 16. + if len(lines) > 16: + # If number of lines exceeds 16, even + # (empty) lines are included in transaction + # creation. Transactions are then appended + # to the transactions list + for line in lines: + created_txn = create_transaction( + post_client=post_client, + receiver_address=receiver_address, + sender_address=sender_address, + message=line + ) + transactions.append(created_txn) + progs = round((len(transactions)/len(lines))*100, ndigits=3) + print(f"\rPreparing..{progs}%", end="") + else: + # If number of lines did not exceed 16, + # each line is included in transaction + # creation if the length of line is not 0 + transactions = [ + create_transaction( + post_client=post_client, + receiver_address=receiver_address, + sender_address=sender_address, + message=line + ) for line in lines if line != "" + ] + # Group created transactions by 16 to comply + # with the group transaction limit of 16 + # individual transactions per group + init_lines = [] + actual_lines = [] + if len(lines) > 16: + for eachtxn in transactions: + init_lines.append(eachtxn) + if len(init_lines) == 16: + actual_lines.append(init_lines) + init_lines = [] + progs = round(((transactions.index(eachtxn)+1)/len(transactions)*100), ndigits=3) + print(f"\rAppending..{progs}%", end="") + # If the last batch of transactions are out and + # their number did not reach 16, append to + # actual lines nonetheless. + if len(init_lines) != 0: + actual_lines.append(init_lines) + else: + # If the length of main line is less than 16, + # bypass the sorting so that the actual lines + # becomes the list of transactions + actual_lines = [transactions] + # Calculate Group IDs for + # each subgroup in transactions list + # and append to a signed transactions list + signed_transactions = [] + signed_transactions_process = [] + print(f'\nCalculating GIDs..') + for group in actual_lines: + # Calculate Group ID for each batch of transactions + cgid = transaction.calculate_group_id(txns=group) + for inner_item in group: + # Assign calculated Group ID + # to each transaction + inner_item.group = cgid + # Sign transaction + signed_txn = inner_item.sign(sender_private_key) + # Add to processing list + signed_transactions_process.append(signed_txn) + # Append to final signed_transactions list + signed_transactions.append(signed_transactions_process) + # Reset helper list + signed_transactions_process = [] + # Send signed group transactions to the Algorand blockchain + txids = [] + for signed_group in signed_transactions: + txid = post_client.send_transactions(signed_group) + txids.append(txid) + # Do not wait for confirmation + progs = round(((signed_transactions.index(signed_group)+1)/len(signed_transactions)) * 100, 3) + print(f"\rSubmitted transactions..{progs}% ", end="") + return txids + + +# Main upload function +def upload( + filename: str, + sender_address: str, + sender_private_key: str +): + """ + Uploads certain local file to blockchain and returns only if + the uploaded file is found to be the same with the downloaded file. + + :param filename: The filename of the file to be uploaded + :param sender_address: Algorand address of sender + :param sender_private_key: Algorand private key of sender + :return: downloaded_file: The stitched records from Algorand blockchain + :return: transaction_ids: Transaction IDs submitted to the blockchain + """ + # Base64 encode the bytes and decode to get the string + with open(filename, 'rb') as o: + original_file = o.read().decode('ISO-8859-1') + original_file = base64.b64encode(original_file.encode()).decode() + print(f'Uploading {filename} to Algorand blockchain..') + # Get Transaction IDs submitted to the blockchain + transaction_ids = process_publishing( + feed=original_file, + receiver_address=sender_address, + sender_address=sender_address, + sender_private_key=sender_private_key + ) + # Initialize GET Client + get_client = init_get_client() + # Loop until downloaded data is exactly + # the same with the uploaded data + while True: + # Get Transaction IDs from Transaction IDs + txn_ids = get_txn_ids_from_txn_id( + __txids=transaction_ids, + client=get_client + ) + # Download the uploaded file from the blockchain + downloaded_file = stitch_records( + get_client=get_client, + txn_ids=txn_ids + ) + # Check if the uploaded file + # and downloaded file are the same + # and return Transaction IDs from + # upload procedure if so + circular = check_circular( + original=original_file, + stitched=downloaded_file + ) + if circular: + print('File successfully uploaded to blockchain.') + return transaction_ids + + +# Link getter +def get_file_id( + transaction_ids: list, + receiver_address: str, + sender_address: str, + sender_private_key: str, + post_client: algod.AlgodClient, + filename: str, +) -> str: + """ + Returns a link which is essentially a Transaction ID + that can be used to download the uploaded file. + + :param transaction_ids: Transaction IDs from upload + :param receiver_address: Algorand address of receiver + :param sender_address: Algorand address of sender + :param sender_private_key: Private key of sender + :param post_client: AlgodClient to node (not indexer) + :param filename: filename of the uploaded file + :return: File ID (a Transaction ID) used for stitching + """ + print(f"Assigning File ID...Please Wait.") + alpha_fn = base64.b64encode(filename.encode()).decode() + alpha = f"{alpha_fn}" + for txid in transaction_ids: + alpha += txid + feed = get_lines( + note=alpha, + max_length=947 + ) + txid = None + for each in feed: + if len(each) != 0: + if len(feed) > 1: + if txid is None: + txn = create_transaction( + post_client, + receiver_address, + sender_address, + message=each + ) + else: + txn = create_transaction( + post_client, + receiver_address, + sender_address, + message=each+txid + ) + sgd = txn.sign(sender_private_key) + txid = post_client.send_transaction(sgd) + transaction.wait_for_confirmation( + algod_client=post_client, + txid=txid + ) + else: + txn = create_transaction( + post_client, + receiver_address, + sender_address, + message=each + ) + sgd = txn.sign(sender_private_key) + txid = post_client.send_transaction(sgd) + transaction.wait_for_confirmation( + algod_client=post_client, + txid=txid + ) + return txid diff --git a/HeadlineHackathon/Dreamf1re_hack/util.py b/HeadlineHackathon/Dreamf1re_hack/util.py new file mode 100644 index 0000000..125461e --- /dev/null +++ b/HeadlineHackathon/Dreamf1re_hack/util.py @@ -0,0 +1,317 @@ +from algosdk.v2client import algod +from algosdk.future import transaction +from algosdk.mnemonic import to_private_key +from algosdk.account import address_from_private_key +from urllib.request import Request, urlopen +import json +import base64 + +from constants import * +import os + + +TEST_SENDER_MNEMONIC = os.environ["test_mnemonic"] +TEST_SENDER_PRIVATE_KEY = to_private_key(TEST_SENDER_MNEMONIC) +TEST_SENDER_ADDRESS = address_from_private_key(TEST_SENDER_PRIVATE_KEY) + + +def init_post_client(): + """ + Initializes an Algorand Client for posting data + + :return: algod_client - algod.AlgodClient (POST) + """ + algod_address = ALGONODE_NODE_ADDRESS + algod_token = '' + headers = {'User-Agent': 'algosdk'} + algod_client = algod.AlgodClient(algod_token, algod_address, headers) + return algod_client + + +def init_get_client(): + """ + Initializes an Algorand Client for getting data + + :return: algod_client - algod.AlgodClient (GET) + """ + algod_address = ALGONODE_INDX_ADDRESS + algod_token = '' + headers = {'User-Agent': 'algosdk'} + algod_client = algod.AlgodClient(algod_token, algod_address, headers) + return algod_client + + +def get_account_info( + get_client: algod.AlgodClient, + account_address: str +): + """ + Gets account information from the given account address. + + :param get_client: algod.AlgodClient (GET) + :param account_address: Algorand public address of target account + :return: info - Account information + """ + info = None + while True: + try: + info = get_client.account_info(address=account_address) + if info is not None: + return info + except Exception as err: + print(err.args) + finally: + if info is None: + info = get_client.account_info(address=account_address) + if info is not None: + return info + + +# Notes in payment transactions are +# utilized to store data in the blockchain +def create_transaction( + post_client: algod.AlgodClient, + receiver_address: str, + sender_address: str, + message +): + """ + Creates a payment transaction for a given message. + + :param post_client: algod.AlgodClient (POST) + :param receiver_address: Algorand receiver address + :param sender_address: Algorand sender address + :param message: The note + :return: A payment transaction (PaymentTxn) + """ + return transaction.PaymentTxn( + sender=sender_address, + sp=post_client.suggested_params(), + receiver=receiver_address, + amt=0, + note=message + ) + + +# Because data is stored in notes, +# the following function gets the note +# of a given transaction. +def search_note_by_txid( + get_client: algod.AlgodClient, + txid: str +): + """ + Gets note based on the specified Transaction ID + + :param get_client: algod.AlgodClient (GET) + :param txid: Transaction ID from which to get the note + :return: note - the note of a given transaction + """ + try: + while True: + req = f'/v2/transactions/{txid}' + url = get_client.algod_address + req + request = Request(url=url, headers=get_client.headers) + resp = urlopen(request) + json_loaded = json.load(resp) + note = str(json_loaded['transaction']['note']) + # Notes are base64 decoded as it is + # base64 encoded before uploading to the + # blockchain. This is to add a layer of + # obfuscation to the contents of the actual + # note. This can be edited so as not to + # do base64 encoding before uploading thereby + # not needing to decode when it is fetched from + # the blockchain. + note = base64.b64decode(note).decode() + if len(note) != 0: + return note + except Exception as e: + print(e.args) + + +# Since there is limited length of bytes per note in the +# transaction which is 1024 bytes or 1 kilobyte, the main feed +# of bytes obtained from encoding of the file-to-be-uploaded is +# divided into separate lines +def get_lines( + note: str, + max_length: int +) -> list: + """ + Get lines for each transaction. Each line, by design, is 947 bytes in length, + max length is 1024 bytes for the Algorand note field. + + :param note: The main feed which is base64 encoded with ISO-8859-1 encoding + :param max_length: The intended line length + :return: list_of_notes - A list of notes + """ + # Do first append + list_of_notes = [note[0:max_length]] + new_note = note[max_length:] + # Repeat succeeding appends + while True: + list_of_notes.append(new_note[0:max_length]) + new_note = new_note[max_length:] + # Do append if final line is reached + if len(new_note) < max_length: + list_of_notes.append(new_note[0:]) + break + return list_of_notes + + +def get_transaction_info( + txids: list, + client: algod.AlgodClient +): + """ + Gets transaction infos from transaction IDs + + :param txids: Transaction IDs + :param client: algod.AlgodClient (GET -> directed to indexer) + :return: jsons: Transaction Infos + """ + jsons = [] + for txid in txids: + try: + req = f'/v2/transactions/{txid}' + url = client.algod_address + req + request = Request(url, headers=client.headers) + while True: + resp = urlopen(request) + json_loaded = json.load(resp) + if len(str(json_loaded)) > 0 and str(json_loaded) != "()": + jsons.append(json_loaded) + print(f"\rFetched infos...", end="") + break + except Exception as e: + print(e.args) + return jsons + + +def get_confirmed_rounds_from_txid( + txids: list, + client: algod.AlgodClient +): + confirmed_rounds = [] + # Get confirmed round + try: + while True: + tx_infos = get_transaction_info(txids=txids, client=client) + if len(tx_infos) != 0: + break + for txinfo in tx_infos: + while True: + conrnd = str(txinfo['transaction']['confirmed-round']) + if len(conrnd) > 0: + confirmed_rounds.append(conrnd) + break + except Exception as e: + print(e.args) + return confirmed_rounds + + +def get_txn_ids_from_txn_id( + __txids: list, + client: algod.AlgodClient +): + """ + Gets Transaction IDs from a Transaction ID, + leverages Confirmed Rounds and Group IDs. + + :param __txids: Transaction IDs + :param client: algod.AlgodClient (GET -> directed to indexer) + :return: txids: Transaction IDs + """ + txids = [] + initial = [] + bridge_for_reverse = [] + block_infos = [] + while True: + try: + # Get confirmed rounds + while True: + confirmed_rounds = get_confirmed_rounds_from_txid( + txids=__txids, + client=client + ) + if len(confirmed_rounds) > 0: + break + print("\rGoing through blocks... ", end="") + while True: + # Get block infos + for cround in confirmed_rounds: + req = f'/v2/transactions?round={cround}' + url = client.algod_address + req + request = Request(url, headers=client.headers) + while True: + resp = urlopen(request) + json_loaded = str(json.load(resp)) + if len(json_loaded) > 0: + block_infos.append(json_loaded) + break + # Get Group ID list + gids = get_group_id(client=client, txids=__txids) + # Get Transaction IDs + for index, block_info in enumerate(block_infos, start=0): + while gids[index] in block_info: + gid_index = block_info.find(gids[index]) + full_gid_index = gid_index + len(gids[index]) + gid_ = block_info[gid_index:full_gid_index] + gid_and_ = gid_ + '","id":"' + txid_start_index = (gid_index + 1) + (len(gid_and_) + 1) + txid_end_index = txid_start_index + 52 + txid_extract = block_info[txid_start_index:txid_end_index] + initial.append(txid_extract) + block_info = block_info[txid_end_index+1:] + txids.append(initial) + initial = [] + break + break + except Exception as e: + print(e.args) + txids.reverse() + for sublist in txids: + bridge_for_reverse.append(sublist) + txids = [] + bridge_for_reverse.reverse() + for superior in bridge_for_reverse: + for inferior in superior: + txids.append(inferior) + return txids + + +# Group IDs are used to find +# other Transaction IDs from +# the given Transaction IDs +# obtained from the upload +# procedure +def get_group_id( + client: algod.AlgodClient, + txids: list +) -> list: + """ + Gets Group IDs from Transaction IDs + + :param client: an algod.AlgodClient (GET) + :param txids: Transaction IDs + :return: gids - Group IDs + """ + # Get Group IDs + gids = [] + print("Getting gids...") + try: + while True: + txn_infos = get_transaction_info( + txids=txids, + client=client + ) + if len(txn_infos) != 0: + for txn_info in txn_infos: + gid = txn_info['transaction']['group'] + if len(gid) > 0: + gids.append(gid) + break + except Exception as e: + print(e.args) + return gids