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