import base64 import subprocess from algosdk.v2client import algod from pyteal import compileTeal, Mode from application_information.application import calc # This programs assumes Kubo IPFS version 0.17.0 # is installed, in the PATH, and initialized (ipfs init) # General parameters (assumes sandbox) algod_token = "a" * 64 algod_address = "http://localhost:4001" verbose = True def create_teal_bytecblock(bytes_slice: list[bytes]) -> str: """ Return a TEAL bytecblock with the byte strings given as inputs """ return f'bytecblock {" ".join(["0x" + b.hex() for b in bytes_slice])}' def append_bytes_information_to_teal(teal_script: str, information_bytes: bytes) -> str: """ Append a bytecblock containing the byte string `information_bytes` at the end of provided teal_script :param teal_script: string representing a TEAL program :param information_bytes: bytes to append as bytecblock at the end of the program :return: the new TEAL script """ bytecblock = create_teal_bytecblock([information_bytes]) return f'{teal_script}\n{bytecblock}\n' def compile_program(client: algod.AlgodClient, teal_script: str) -> bytes: compile_response = client.compile(teal_script) return base64.b64decode(compile_response["result"]) def compute_information_bytes(folder: str) -> bytes: """ Compute the (encoded) information byte string corresponding to all the files inside the folder `folder` """ # Use Kubo IPFS command line # We don't use --wrap-directory as we are already in a folder output = subprocess.run( ["ipfs", "add", "--cid-version=1", "--hash=sha2-256", "--recursive", "--quiet", "--only-hash", "--ignore=__pycache__", folder], capture_output=True ) # The CID is the last non-empty line text_cid = output.stdout.decode().strip().split("\n")[-1] assert text_cid == "bafybeiavazvdva6uyxqudfsh57jbithx7r7juzvxhrylnhg22aeqau6wte" # Check that the text CID is a base32 CID if text_cid[0] != "b": raise Exception("IPFS returned a non-base32 CID") # The CID is a base32 string starting with b, we need to remove this b to get the binary CID b32_string = text_cid[1:] assert b32_string == "afybeiavazvdva6uyxqudfsh57jbithx7r7juzvxhrylnhg22aeqau6wte" # We need to re-pad to make Python happy, pad_length = 6 pad_length = (8 - (len(b32_string) % 8)) % 8 binary_cid = base64.b32decode(b32_string.upper() + '=' * pad_length) assert binary_cid.hex() == "0170122015066a3a83d4c5e1419647efd2144cf7fc7e9a66b73c70b69cdad0090053d699" # Finally compute the bytes information information_bytes = b"arc23" + binary_cid assert information_bytes.hex() == \ "61726332330170122015066a3a83d4c5e1419647efd2144cf7fc7e9a66b73c70b69cdad0090053d699" return information_bytes def get_cid_from_compiled_program(compiled_program: bytes) -> bytes: """ Retrieve the binary CID from a compiled program. Return None if not found """ # Production code SHOULD check the TEAL version of the compiled_program first # and be updated to support different encoding for bytecblock for potential future TEAL versions # The binary CID consists in the 36 bytes after the following prefix prefix = bytes.fromhex("2601296172633233") # 0x26 is the bytecblock opcode, 0x01 is the number of []byte strings, # 0x29 is the length = 41 bytes, 0x6172633233 is hexadecimal of `arc23` count_query = compiled_program.count(prefix) if count_query > 1: # The program contains several potential CID # A production code would actually try each of them # but this code is more basic and will just fail return None if count_query == 0: # CID not present return None idx = compiled_program.index(prefix) + len(prefix) if len(compiled_program) < idx + 36: # This was a false positive: there are not enough bytes after the prefix return None binary_cid = compiled_program[idx:idx + 36] return binary_cid def main(): # Connect to an algod client algod_client = algod.AlgodClient(algod_token=algod_token, algod_address=algod_address) # Convert the PyTEAL approval program into a TEAL script teal_script = compileTeal(calc(), mode=Mode.Application, version=5) # Compile the program without information compiled_program_wo_information = compile_program(algod_client, teal_script) # Compute the information bytes information_bytes = compute_information_bytes("application_information") # Append it to the contract teal_script_with_information = append_bytes_information_to_teal(teal_script, information_bytes) if verbose: print("New TEAL script:") print(teal_script_with_information) print("----------------") # Compile the program compiled_program = compile_program(algod_client, teal_script_with_information) # Check that the new program is the old program concatenated with # 0x26012961726332330170122015066a3a83d4c5e1419647efd2144cf7fc7e9a66b73c70b69cdad0090053d699 # where 0x2601296172633233 is the prefix described above in `get_cid_from_compiled_program` # and 0x0170122015066a3a83d4c5e1419647efd2144cf7fc7e9a66b73c70b69cdad0090053d699 is the binary CID assert compiled_program == \ compiled_program_wo_information +\ bytes.fromhex("26012961726332330170122015066a3a83d4c5e1419647efd2144cf7fc7e9a66b73c70b69cdad0090053d699") assert len(compiled_program) == len(compiled_program_wo_information) + 44 # Now verify that we can read back binary_cid = get_cid_from_compiled_program(compiled_program) # Compute the associated text_cid # (remove padding, prefix by "b", put everything in lower case) text_cid = "b" + base64.b32encode(binary_cid).decode("ascii").lower().replace("=", "") assert text_cid == "bafybeiavazvdva6uyxqudfsh57jbithx7r7juzvxhrylnhg22aeqau6wte" print(f"Success: the CID is: {text_cid}") if __name__ == "__main__": main()