diff --git a/Makefile b/Makefile index 4dafb9f..32191c5 100644 --- a/Makefile +++ b/Makefile @@ -19,14 +19,16 @@ proto: @protoc --proto_path=model --go_out=model \ --go_opt=paths=source_relative model/model.proto -integration-test-cleanup: - rm -f flow.json - rm -f account-keys.csv - rm -rf data - rm -rf flow-go +cleanup-integration-tests: + rm -f tests/flow.json + rm -f tests/emulator-account.pkey + rm -f tests/.gitignore + rm -f tests/accounts-*.json + rm -rf ./data + rm -f ./server -integration-test: - python3 integration_test.py +testnet-integration-test: + python3 tests/integration_test.py --network testnet --init previewnet-integration-test: - python3 previewnet_integration_test.py \ No newline at end of file + python3 tests/integration_test.py --network previewnet --init diff --git a/mainnet-offline.json b/mainnet-offline.json deleted file mode 100644 index 76aed68..0000000 --- a/mainnet-offline.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "contracts": { - "flow_cold_storage_proxy": "0000000000000000", - "flow_fees": "f919ee77447b7497", - "flow_token": "1654653399040a61", - "fungible_token": "f233dcee88fe0abe" - }, - "network": "mainnet", - "mode": "offline", - "port": 8080 -} \ No newline at end of file diff --git a/previewnet_integration_test.py b/previewnet_integration_test.py deleted file mode 100644 index dcc6b80..0000000 --- a/previewnet_integration_test.py +++ /dev/null @@ -1,436 +0,0 @@ -import json -import subprocess -import requests - - -###################################################################################### -### Constants -###################################################################################### - -# Changing fcl versions seemed to have yielded varying flow.json outputs / default naming -# Specifying a constant init flow.json might be easier to maintain -previewnet_const = { - "networks": { - "previewnet": "access.previewnet.nodes.onflow.org:9000" - }, - "accounts": { - } -} - -network_flag = ['-n', 'previewnet'] -config_file_name = "previewnet.json" -rosetta_host_url = "http://127.0.0.1:8080" - -###################################################################################### -### Setup flow.json and a root originator with ColdStorageProxy contract -###################################################################################### - -def init_flow_json(): - with open('flow.json', 'w') as json_file: - json.dump(previewnet_const, json_file, indent=4) - -def create_originator(): - flow_key, rosetta_key, private_key = gen_account() - print("The originator's flow public key is: " + flow_key) - flow_address = input("What is the flow address generated from https://previewnet-faucet.onflow.org/ using ECDSA_secp256k1 (Include the 0x prefix)\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("originator," + flow_key + "," + rosetta_key + "," + private_key + "," + flow_address + "\n") - - contract_account_value = { - "address": flow_address, - "key": { - "type": "hex", - "index": 0, - "signatureAlgorithm": "ECDSA_secp256k1", - "hashAlgorithm": "SHA3_256", - "privateKey": private_key - } - } - - try: - with open('flow.json', "r+") as json_file: - data = json.load(json_file) - data["accounts"]["originator"] = contract_account_value - json_file.seek(0) - json.dump(data, json_file, indent=4) - json_file.truncate() - except IOError as e: - print(f"Error opening or writing to file: {e}") - - deploy_contracts("originator") - truncated_flow_address = flow_address[2:] - with open(config_file_name, "r+") as json_file: - data = json.load(json_file) - data["originators"] = [truncated_flow_address] - data["contracts"]["flow_cold_storage_proxy"] = truncated_flow_address - json_file.seek(0) - json.dump(data, json_file, indent=4) - json_file.truncate() - -def create_account(): - flow_key, rosetta_key, private_key = gen_account() - print("The flow-account's flow public key is: " + flow_key) - flow_address = input("What is the flow address generated from https://previewnet-faucet.onflow.org/ using ECDSA_secp256k1 (Include the 0x prefix)\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("flow-account," + flow_key + "," + rosetta_key + "," + private_key + "," + flow_address + "\n") - -def deploy_contracts(account_name): - ## Need to modify flow contract addresses - contract_path = "./script/cadence/contracts/FlowColdStorageProxy.cdc" - print("FlowToken from 0x4445e7ad11568276") - print("FungibleToken from 0xa0225e7000ac82a9") - print("Burner from 0xcf706c70db9dab9b") - _ = input("Modify the FlowColdStorageProxy contract at " + contract_path + " to use the correct contract addresses for previewnet.") - deploy_contract_cmd = f"flow-c1 accounts add-contract --signer {account_name} {contract_path}" - cmds = deploy_contract_cmd.split(" ") + network_flag - result = subprocess.run(cmds, stdout=subprocess.PIPE) - print(result.stdout.decode('utf-8')) - -def setup_rosetta(): - subprocess.run(["make"], stdout=subprocess.PIPE) - _ = input("Please start rosetta with the previewnet config file $./server " + config_file_name) - -###################################################################################### -### Helper Functions -###################################################################################### - - -def gen_account(): - gen_key_cmd = "go run ./cmd/genkey/genkey.go" - result = subprocess.run(gen_key_cmd.split(" "), stdout=subprocess.PIPE) - keys = result.stdout.decode('utf-8').split("\n") - public_flow_key = keys[0].split(" ")[-1] - public_rosetta_key = keys[1].split(" ")[-1] - private_key = keys[2].split(" ")[-1] - return (public_flow_key, public_rosetta_key, private_key) - -def get_account_keys(account): - with open("account-keys.csv") as search: - for line in search: - if account in line: - keys = line.split(",") - return (keys[1], keys[2], keys[3], keys[4][:-1]) - -def request_router(target_url, body): - headers = {'Content-type': 'application/json'} - try: - r = requests.post(target_url, data=json.dumps(body), headers=headers) - except requests.exceptions.RequestException as e: - print(f"Network request failed: {e}") - return r.json() - - -###################################################################################### -### Rosetta Construction Functions -###################################################################################### - - -def rosetta_create_account(root_originator, root_originator_name="originator", i=0): - public_flow_key, public_rosetta_key, new_private_key = gen_account() - transaction = "create_account" - metadata = {"public_key": public_rosetta_key} - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "metadata": metadata - } - ] - preprocess_response = preprocess_transaction(root_originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - flow_key, rosetta_key, private_key, _ = get_account_keys(root_originator_name) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - ## TODO: Figure out why flow cli gets stuck on "Waiting for transaction to be sealed..." despite explorer showing sealed - print("Look for the account that has Received 0.00100000 Flow") - flow_address = input("What is the flow address generated? (https://previewnet.flowdiver.io/tx/" + tx_hash + ")\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("create_account," + public_flow_key + "," + public_rosetta_key + "," + new_private_key + "," + flow_address + "\n") - -def rosetta_create_proxy_account(root_originator, root_originator_name="originator", i=0): - public_flow_key, public_rosetta_key, new_private_key = gen_account() - transaction = "create_proxy_account" - metadata = {"public_key": public_rosetta_key} - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "metadata": metadata - } - ] - preprocess_response = preprocess_transaction(root_originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - flow_key, rosetta_key, private_key, _ = get_account_keys(root_originator_name) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - ## TODO: Figure out why flow cli gets stuck on "Waiting for transaction to be sealed..." despite explorer showing sealed - print("Look for the account that has Received 0.00100000 Flow") - flow_address = input("What is the flow address generated? (https://previewnet.flowdiver.io/tx/" + tx_hash + ")\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("create_proxy_account," + public_flow_key + "," + public_rosetta_key + "," + new_private_key + "," + flow_address + "\n") - -def rosetta_transfer(originator, destination, amount, i=0): - transaction = "transfer" - operations = [{ - "type": transaction, - "operation_identifier": { - "index": i - }, - "account": { - "address": originator - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(-1 * amount * 10 ** 7) - } - }, - { - "type": transaction, - "operation_identifier": { - "index": i + 1 - }, - "related_operations": [ - { - "index": i - } - ], - "account": { - "address": destination - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(amount * 10 ** 7) - } - }] - - preprocess_response = preprocess_transaction(originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - _, rosetta_key, private_key, _ = get_account_keys(originator) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, originator, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - print("Transferring " + str(amount) + " from " + originator + " to " + destination) - print("Transaction submitted... https://previewnet.flowdiver.io/tx/" + tx_hash) - -def rosetta_proxy_transfer(originator, destination, originator_root, amount, i=0): - transaction = "proxy_transfer_inner" - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "account": { - "address": originator - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(-1 * amount * 10 ** 7) - } - }, - { - "type": transaction, - "operation_identifier": { - "index": i + 1 - }, - "related_operations": [ - { - "index": i - } - ], - "account": { - "address": destination - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(amount * 10 ** 7) - } - } - ] - preprocess_response = preprocess_transaction(originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - _, rosetta_key, private_key, _ = get_account_keys(originator) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, originator, hex_bytes, rosetta_key, signed_tx) - combined_signed_tx = combine_response["signed_transaction"] - - preprocess_response = preprocess_transaction(originator_root, operations, {"proxy_transfer_payload": combined_signed_tx}) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - _, rosetta_key, private_key, _ = get_account_keys(originator_root) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, originator_root, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - print("Proxy transferring " + str(amount) + " from " + originator + " to " + destination + " proxied through " + originator_root) - print("Transaction submitted... https://previewnet.flowdiver.io/tx/" + tx_hash) - -def preprocess_transaction(root_originator, operations, metadata=None): - endpoint = "/construction/preprocess" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "previewnet" - }, - "operations": operations, - "metadata": { - "payer": root_originator - } - } - if metadata: - for key in metadata: - data["metadata"][key] = metadata[key] - return request_router(target_url, data) - -def metadata_transaction(options): - endpoint = "/construction/metadata" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "previewnet" - }, - "options": options - } - return request_router(target_url, data) - -def payloads_transaction(operations, protobuf): - endpoint = "/construction/payloads" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "previewnet" - }, - "operations": operations, - "metadata": { - "protobuf": protobuf - } - } - return request_router(target_url, data) - -def combine_transaction(unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx): - endpoint = "/construction/combine" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "previewnet" - }, - "unsigned_transaction": unsigned_tx, - "signatures": [ - { - "signing_payload": { - "account_identifier": { - "address": root_originator - }, - "address": root_originator, - "hex_bytes": hex_bytes, - "signature_type": "ecdsa" - }, - "public_key": { - "hex_bytes": rosetta_key, - "curve_type": "secp256k1" - }, - "signature_type": "ecdsa", - "hex_bytes": signed_tx - } - ] - } - return request_router(target_url, data) - -def submit_transaction(signed_tx): - endpoint = "/construction/submit" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "previewnet" - }, - "signed_transaction": signed_tx - } - return request_router(target_url, data) - -###################################################################################### -### Main Script -###################################################################################### - -def main(): - init_setup = '' - while (init_setup != 'y') and (init_setup != 'n'): - print(init_setup) - init_setup = input("Do we need to instantiate a new root originator? (y/n)") - if init_setup == 'y': - init_flow_json() - create_originator() - create_account() - setup_rosetta() - - _, _, _, root_address = get_account_keys("originator") - rosetta_create_account(root_address, "originator", 0) - _, _, _, new_address = get_account_keys("create_account") - # TODO: uncomment when FlowColdStorageProxy.cdc updated to Cadence 1.0 - # rosetta_create_proxy_account(address, "originator", 0) - # _, _, _, new_proxy_address = get_account_keys("create_proxy_account") - - _, _, _, flow_account = get_account_keys("flow-account") - - print(f"addresses ", root_address, new_address, flow_account) - - rosetta_transfer(root_address, new_address, 50) - # TODO: uncomment when FlowColdStorageProxy.cdc updated to Cadence 1.0 - # time.sleep(30) ## Hacky fix to not check nonce - # rosetta_transfer(address, new_proxy_address, 50) - # time.sleep(30) - - # _, _, _, flow_account_address = get_account_keys("flow-account") - # rosetta_proxy_transfer(new_proxy_address, flow_account_address, address, 10) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/testnet-clone.json b/testnet-clone.json deleted file mode 100644 index c5e5968..0000000 --- a/testnet-clone.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "cache": true, - "construction_access_nodes": [ - { - "address": "access.devnet.nodes.onflow.org:9000" - } - ], - "contracts": { - "flow_cold_storage_proxy": "0000000000000000", - "flow_fees": "912d5440f7e3769e", - "flow_token": "7e60df042a9c0868", - "fungible_token": "9a0766d93b6608b7" - }, - "data_dir": "data", - "disable_consensus_follower": true, - "network": "testnet", - "originators": [], - "port": 8080, - "spork_seal_tolerance": 600, - "sporks": { - "50": { - "access_nodes": [ - { - "address": "access.devnet.nodes.onflow.org:9000" - } - ], - "root_block": 185185854, - "version": 6 - } - } -} \ No newline at end of file diff --git a/testnet-offline.json b/testnet-offline.json deleted file mode 100644 index 720297e..0000000 --- a/testnet-offline.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "contracts": { - "flow_cold_storage_proxy": "0000000000000000", - "flow_fees": "912d5440f7e3769e", - "flow_token": "7e60df042a9c0868", - "fungible_token": "9a0766d93b6608b7" - }, - "network": "testnet", - "mode": "offline", - "port": 8080 -} \ No newline at end of file diff --git a/testnet_integration_test.py b/testnet_integration_test.py deleted file mode 100644 index 5868960..0000000 --- a/testnet_integration_test.py +++ /dev/null @@ -1,430 +0,0 @@ -import json -import subprocess -from threading import Thread -import os -import csv -import requests -import time - - -###################################################################################### -### Constants -###################################################################################### - -# Changing fcl versions seemed to have yielded varying flow.json outputs / default naming -# Specifying a constant init flow.json might be easier to maintain -testnet_const = { - "networks": { - "testnet": "access.devnet.nodes.onflow.org:9000" - }, - "accounts": { - } -} - -network_flag = ['-n', 'testnet'] -config_file_name = "testnet-clone.json" -rosetta_host_url = "http://127.0.0.1:8080" - - -###################################################################################### -### Setup flow.json and a root originator with ColdStorageProxy contract -###################################################################################### - - -def init_flow_json(): - with open('flow.json', 'w') as json_file: - json.dump(testnet_const, json_file, indent=4) - -def create_originator(): - flow_key, rosetta_key, private_key = gen_account() - print("The originator's flow public key is: " + flow_key) - flow_address = input("What is the flow address generated from https://testnet-faucet.onflow.org/ using ECDSA_secp256k1 (Include the 0x prefix)\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("originator," + flow_key + "," + rosetta_key + "," + private_key + "," + flow_address + "\n") - - contract_account_value = { - "address": flow_address, - "key": { - "type": "hex", - "index": 0, - "signatureAlgorithm": "ECDSA_secp256k1", - "hashAlgorithm": "SHA3_256", - "privateKey": private_key - } - } - - with open('flow.json', "r+") as json_file: - data = json.load(json_file) - data["accounts"]["originator"] = contract_account_value - json_file.seek(0) - json.dump(data, json_file, indent=4) - json_file.truncate() - - deploy_contracts("originator") - truncated_flow_address = flow_address[2:] - with open(config_file_name, "r+") as json_file: - data = json.load(json_file) - data["originators"] = [truncated_flow_address] - data["contracts"]["flow_cold_storage_proxy"] = truncated_flow_address - json_file.seek(0) - json.dump(data, json_file, indent=4) - json_file.truncate() - -def create_account(): - flow_key, rosetta_key, private_key = gen_account() - print("The originator's flow public key is: " + flow_key) - flow_address = input("What is the flow address generated from https://testnet-faucet.onflow.org/ using ECDSA_secp256k1 (Include the 0x prefix)\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("flow-account," + flow_key + "," + rosetta_key + "," + private_key + "," + flow_address + "\n") - -def deploy_contracts(account_name): - ## Need to modify flow contract addresses - contract_path = "./script/cadence/contracts/FlowColdStorageProxy.cdc" - print("FlowToken from 0x7e60df042a9c0868") - print("FungibleToken from 0x9a0766d93b6608b7") - _ = input("Modify the FlowColdStorageProxy contract at " + contract_path + " to use the correct contract addresses for testnet.") - deploy_contract_cmd = "flow accounts add-contract --signer " + account_name + " FlowColdStorageProxy " + contract_path - cmds = deploy_contract_cmd.split(" ") + network_flag - result = subprocess.run(cmds, stdout=subprocess.PIPE) - print(result.stdout.decode('utf-8')) - -def setup_rosetta(): - subprocess.run(["make"], stdout=subprocess.PIPE) - _ = input("Please start rosetta with the localnet config file $./server " + config_file_name) - -###################################################################################### -### Helper Functions -###################################################################################### - - -def gen_account(): - gen_key_cmd = "go run ./cmd/genkey/genkey.go" - result = subprocess.run(gen_key_cmd.split(" "), stdout=subprocess.PIPE) - keys = result.stdout.decode('utf-8').split("\n") - public_flow_key = keys[0].split(" ")[-1] - public_rosetta_key = keys[1].split(" ")[-1] - private_key = keys[2].split(" ")[-1] - return (public_flow_key, public_rosetta_key, private_key) - -def get_account_keys(account): - with open("account-keys.csv") as search: - for line in search: - if account in line: - keys = line.split(",") - return (keys[1], keys[2], keys[3], keys[4][:-1]) - -def request_router(target_url, body): - headers = {'Content-type': 'application/json'} - r = requests.post(target_url, data=json.dumps(body), headers=headers) - return r.json() - - -###################################################################################### -### Rosetta Construction Functions -###################################################################################### - - -def rosetta_create_account(root_originator, root_originator_name="originator", i=0): - public_flow_key, public_rosetta_key, new_private_key = gen_account() - transaction = "create_account" - metadata = {"public_key": public_rosetta_key} - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "metadata": metadata - } - ] - preprocess_response = preprocess_transaction(root_originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - flow_key, rosetta_key, private_key, _ = get_account_keys(root_originator_name) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - ## TODO: Figure out why flow cli gets stuck on "Waiting for transaction to be sealed..." despite explorer showing sealed - print("Look for the account that has Received 0.00100000 Flow") - flow_address = input("What is the flow address generated? (https://testnet.flowscan.org/transaction/" + tx_hash + ")\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("create_account," + public_flow_key + "," + public_rosetta_key + "," + new_private_key + "," + flow_address + "\n") - -def rosetta_create_proxy_account(root_originator, root_originator_name="originator", i=0): - public_flow_key, public_rosetta_key, new_private_key = gen_account() - transaction = "create_proxy_account" - metadata = {"public_key": public_rosetta_key} - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "metadata": metadata - } - ] - preprocess_response = preprocess_transaction(root_originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - flow_key, rosetta_key, private_key, _ = get_account_keys(root_originator_name) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - ## TODO: Figure out why flow cli gets stuck on "Waiting for transaction to be sealed..." despite explorer showing sealed - print("Look for the account that has Received 0.00100000 Flow") - flow_address = input("What is the flow address generated? (https://testnet.flowscan.org/transaction/" + tx_hash + ")\n") - with open('account-keys.csv', "a+") as file_object: - file_object.write("create_proxy_account," + public_flow_key + "," + public_rosetta_key + "," + new_private_key + "," + flow_address + "\n") - -def rosetta_transfer(originator, destination, amount, i=0): - transaction = "transfer" - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "account": { - "address": originator - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(-1 * amount * 10 ** 7) - } - }, - { - "type": transaction, - "operation_identifier": { - "index": i + 1 - }, - "related_operations": [ - { - "index": i - } - ], - "account": { - "address": destination - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(amount * 10 ** 7) - } - } - ] - preprocess_response = preprocess_transaction(originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - _, rosetta_key, private_key, _ = get_account_keys(originator) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, originator, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - print("Transferring " + str(amount) + " from " + originator + " to " + destination) - print("Transaction submitted... https://testnet.flowscan.org/transaction/" + tx_hash) - -def rosetta_proxy_transfer(originator, destination, originator_root, amount, i=0): - transaction = "proxy_transfer_inner" - operations = [ - { - "type": transaction, - "operation_identifier": { - "index": i - }, - "account": { - "address": originator - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(-1 * amount * 10 ** 7) - } - }, - { - "type": transaction, - "operation_identifier": { - "index": i + 1 - }, - "related_operations": [ - { - "index": i - } - ], - "account": { - "address": destination - }, - "amount": { - "currency": { - "decimals": 8, - "symbol": "FLOW" - }, - "value": str(amount * 10 ** 7) - } - } - ] - preprocess_response = preprocess_transaction(originator, operations) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - _, rosetta_key, private_key, _ = get_account_keys(originator) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, originator, hex_bytes, rosetta_key, signed_tx) - combined_signed_tx = combine_response["signed_transaction"] - - preprocess_response = preprocess_transaction(originator_root, operations, {"proxy_transfer_payload": combined_signed_tx}) - metadata_response = metadata_transaction(preprocess_response["options"]) - payloads_response = payloads_transaction(operations, metadata_response["metadata"]["protobuf"]) - _, rosetta_key, private_key, _ = get_account_keys(originator_root) - hex_bytes = payloads_response["payloads"][0]["hex_bytes"] - unsigned_tx = payloads_response["unsigned_transaction"] - sign_tx_cmd = "go run cmd/sign/sign.go " + private_key + " " + hex_bytes - result = subprocess.run(sign_tx_cmd.split(" "), stdout=subprocess.PIPE) - signed_tx = result.stdout.decode('utf-8')[:-1] - combine_response = combine_transaction(unsigned_tx, originator_root, hex_bytes, rosetta_key, signed_tx) - submit_transaction_response = submit_transaction(combine_response["signed_transaction"]) - tx_hash = submit_transaction_response["transaction_identifier"]["hash"] - print("Proxy transferring " + str(amount) + " from " + originator + " to " + destination + " proxied through " + originator_root) - print("Transaction submitted... https://testnet.flowscan.org/transaction/" + tx_hash) - -def preprocess_transaction(root_originator, operations, metadata=None): - endpoint = "/construction/preprocess" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "testnet" - }, - "operations": operations, - "metadata": { - "payer": root_originator - } - } - if metadata: - for key in metadata: - data["metadata"][key] = metadata[key] - return request_router(target_url, data) - -def metadata_transaction(options): - endpoint = "/construction/metadata" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "testnet" - }, - "options": options - } - return request_router(target_url, data) - -def payloads_transaction(operations, protobuf): - endpoint = "/construction/payloads" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "testnet" - }, - "operations": operations, - "metadata": { - "protobuf": protobuf - } - } - return request_router(target_url, data) - -def combine_transaction(unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx): - endpoint = "/construction/combine" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "testnet" - }, - "unsigned_transaction": unsigned_tx, - "signatures": [ - { - "signing_payload": { - "account_identifier": { - "address": root_originator - }, - "address": root_originator, - "hex_bytes": hex_bytes, - "signature_type": "ecdsa" - }, - "public_key": { - "hex_bytes": rosetta_key, - "curve_type": "secp256k1" - }, - "signature_type": "ecdsa", - "hex_bytes": signed_tx - } - ] - } - return request_router(target_url, data) - -def submit_transaction(signed_tx): - endpoint = "/construction/submit" - target_url = rosetta_host_url + endpoint - data = { - "network_identifier": { - "blockchain": "flow", - "network": "testnet" - }, - "signed_transaction": signed_tx - } - return request_router(target_url, data) - -###################################################################################### -### Main Script -###################################################################################### - -def main(): - init_setup = '' - while (init_setup != 'y') and (init_setup != 'n'): - print(init_setup) - init_setup = input("Do we need to instantiate a new root originator? (y/n)") - if init_setup == 'y': - init_flow_json() - create_originator() - create_account() - setup_rosetta() - - _, _, _, address = get_account_keys("originator") - rosetta_create_account(address, "originator", 0) - _, _, _, new_address = get_account_keys("create_account") - rosetta_create_proxy_account(address, "originator", 0) - _, _, _, new_proxy_address = get_account_keys("create_proxy_account") - - rosetta_transfer(address, new_address, 50) - time.sleep(30) ## Hacky fix to not check nonce - rosetta_transfer(address, new_proxy_address, 50) - time.sleep(30) - - _, _, _, flow_account_address = get_account_keys("flow-account") - rosetta_proxy_transfer(new_proxy_address, flow_account_address, address, 10) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..2f43ec6 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,259 @@ +import fileinput +import subprocess +import re +import json +import sys +import os + + +# Reads the credentials of the account that is used to sign other accounts' +# transactions including their creation. +# +# The function takes a file_name as input, which should be a JSON file +# containing the address and private key of a funded account on the network. +# The expected format of the JSON file is: +# { +# "address": "16114ab6a0672112", +# "key": "98bf75312a38ff6c4052faf4484b95f0bfa795531e8d50da0442c75746b49a4c" +# } +# +# If the file is empty or does not contain the required keys, the function raises an exception +# +# Returns: +# dict: A dictionary containing the address and private key of the account. +# +def read_account_signer(file_name): + with open(file_name, 'r') as file: + account_signer = json.load(file) + + if len(json.dumps(account_signer)) == 0: + raise Exception(f"{file_name} should contain funded account on network") + + return account_signer + + +# Saves an account's data to the accounts-{network}.json file. +# +# This function is used to store account data in a JSON file. +# The accounts file is used by the Rosetta's functions to manage account information. +# +# Args: +# account_name (str): The name of the account to be saved. +# account_data (dict): A dictionary containing the account's data (e.g., address, keys). +# +def save_account(network, account_name, account_data): + filename = f'accounts-{network}.json' + filepath = get_file_path(filename) + + with open(filepath, 'r+') as account_file: + accounts = json.load(account_file) + accounts[account_name] = account_data + + data = json.dumps(accounts, indent=4) + account_file.seek(0) + account_file.write(data) + + +# Saves an account's data to the flow.json file. +# +# This function is used to store account data in the flow.json file, which is used +# by the Flow CLI (FCL) to sign transactions. +# +# Args: +# account_name (str): The name of the account to be saved. +# account_data (dict): A dictionary containing the account's data (e.g., address, keys). +# +def save_account_to_flow_json(account_name, account_data): + filepath = get_file_path('flow.json') + with open(filepath, "r+") as file: + accounts = json.load(file) + accounts["accounts"][account_name] = account_data + + data = json.dumps(accounts, indent=4) + file.seek(0) + file.write(data) + + +# Returns the directory where the script is located +def get_script_directory(): + return os.path.dirname(os.path.abspath(__file__)) + + +# Returns the absolute path of a file relative to the script's directory +def get_file_path(filename): + script_dir = get_script_directory() + return os.path.join(script_dir, filename) + + +# Creates a new Flow account on the network. +# +# This function creates a new Flow account on the network by generating a new +# key pair and calling the Flow CLI to create the account. It returns a dictionary +# containing the account's address, public key, private key, and Rosetta key. +# +# Returns: +# dict: A dictionary containing the account's address, public key, private key, +# and Rosetta key. +# +def create_flow_account(network: str, account_signer: str): + public_key, rosetta_key, private_key = generate_keys() + + cmd = f"flow-c1 accounts create --sig-algo ECDSA_secp256k1 --network {network}" \ + f" --signer {account_signer} --key {public_key}" + cmd_result = subprocess.run(cmd.split(), stdout=subprocess.PIPE, cwd=get_script_directory()) + if cmd_result.returncode != 0: + raise Exception(f"Couldn't create account. {cmd} finished with non-zero code") + + cmd_output = cmd_result.stdout.strip().decode() + regex = "(Address)([\t ]+)([0-9a-z]+)" # parsing string like 'Address 0x123456789' + address_regex_group = re.search(regex, cmd_output) + address = address_regex_group[3] + + return { + "address": address, + "public_key": public_key, + "private_key": private_key, + "rosetta_key": rosetta_key, + } + + +# Deploys a Cadence contract to a Flow account on the network. +# +# This function funds a Flow account on the network, replaces the addresses in a +# Cadence contract file, and then deploys the contract to the account using the +# Flow CLI. +# +# Args: +# account_name (str): The name of the account to deploy the contract to. +# account_address (str): The address of the account to deploy the contract to. +# +def deploy_contract(network: str, account_name, account_address): + if network == "testnet": + flow_token_address = "0x7e60df042a9c0868" + fungible_token_address = "0x9a0766d93b6608b7" + elif network == "previewnet": + flow_token_address = "0x4445e7ad11568276" + fungible_token_address = "0xa0225e7000ac82a9" + + contract_path = os.path.join(get_script_directory(), "..", "script", "cadence", "contracts", "FlowColdStorageProxy.cdc") + replace_address_in_contract(contract_path, "FlowToken", flow_token_address) + replace_address_in_contract(contract_path, "FungibleToken", fungible_token_address) + + fund_account(network, account_address) + + deploy_contract_cmd = f"flow-c1 accounts add-contract {contract_path} --signer {account_name} --network {network}" + result = subprocess.run(deploy_contract_cmd.split(), stdout=subprocess.PIPE, cwd=get_script_directory()) + if result.returncode != 0: + raise Exception(f"Couldn't deploy contract to {network}. `{deploy_contract_cmd}` cmd finished with non-zero code.\n" + f"Is account funded?") + print(result.stdout.decode('utf-8')) + + +# Funds a Flow account on the network. +# +# This function prompts the user to open a link and fund a Flow account on the network. +# There's no way to do it without user for now. Human-interaction is required by Flow faucet. +# +# Args: +# account_address (str): The address of the account to be funded. +# +def fund_account(network: str, account_address): + # TODO: fund cmd doesn't work for some reason + # fund_account_cmd = f"flow-c1 accounts fund {account_address} --network {network}" + _ = input( + f"Open a link and fund account https://{network}-faucet.onflow.org/fund-account?address={account_address}\n" + f"Press enter when finished...") + + # result = subprocess.run(fund_account_cmd.split(), stdout=subprocess.PIPE, cwd=get_script_directory()) + # if result.returncode != 0: + # raise Exception( + # f"Couldn't fund account {account_address} on {network}. {fund_account_cmd} finished with non-zero code") + + +# Replaces the address in a Cadence contract file. +# +# This function replaces the address in a Cadence contract file with a new address. +# It uses regular expressions to find and replace the address in the contract file. +# +# Args: +# contract_path (str): The path to the Cadence contract file. +# contract_name (str): The name of the contract being replaced. +# address (str): The new address to replace in the contract file. +# +def replace_address_in_contract(contract_path, contract_name, address): + pattern = f"(import {contract_name} from )([a-z0-9]+)" + for line in fileinput.input(contract_path, inplace=True): + if re.match(pattern, line): + line = re.sub(pattern, f"\\g<1>{address}", line) + sys.stdout.write(line) + + +# Generates a new set of Flow keys. +# +# This function generates a new set of Flow keys by running a Go script. +# It returns the public Flow key, public Rosetta key, and private key. +# +# Returns: +# tuple: A tuple containing the public Flow key, public Rosetta key, and private key. +# +def generate_keys(): + gen_key_cmd = "go run ../cmd/genkey/genkey.go" + result = subprocess.run(gen_key_cmd.split(), stdout=subprocess.PIPE, cwd=get_script_directory()) + if result.returncode != 0: + raise Exception(f"Couldn't parse output of `{gen_key_cmd}`. Process finished with non-zero code") + + keys = result.stdout.decode('utf-8').split("\n") + public_flow_key = keys[0].split(" ")[-1] + public_rosetta_key = keys[1].split(" ")[-1] + private_key = keys[2].split(" ")[-1] + return public_flow_key, public_rosetta_key, private_key + + +# Reads an account's data from the accounts file. +# +# This function reads an account's data (address, public key, private key, and Rosetta key) +# from the accounts file. +# +# Args: +# account_name (str): The name of the account to read. +# +# Returns: +# dict: A dictionary containing the account's data. +# +def read_account(network: str, account_name: str) -> dict: + filename = f"accounts-{network}.json" + filepath = get_file_path(filename) + + with open(filepath) as keys_file_json: + accounts = json.load(keys_file_json) + return accounts[account_name] + + +# Reads an account's keys from the accounts file. +# +# This function reads an account's address, public key, private key, and Rosetta key +# from the accounts file. +# +# Args: +# account_name (str): The name of the account to read. +# +# Returns: +# tuple: A tuple containing the account's address, public key, private key, and Rosetta key. +# +def read_account_keys(network, account_name): + account = read_account(network, account_name) + return account["address"], account["public_key"], account["private_key"], account["rosetta_key"] + + +def add_hex_prefix(address): + if address.startswith("0x"): + return address + + return "0x" + address + + +def remove_hex_prefix(address): + if address.startswith("0x"): + return address.replace("0x", "") + + return address diff --git a/tests/integration_test.py b/tests/integration_test.py new file mode 100644 index 0000000..4978ba9 --- /dev/null +++ b/tests/integration_test.py @@ -0,0 +1,186 @@ +import time + +from rosetta import * +from helpers import * + +def main(): + network, account_signer = parse_network_and_signer_args() + + if "--init" in sys.argv: + init_flow_json(network, account_signer) + create_originator(network, account_signer) + create_proxy_account(network, account_signer) + + setup_rosetta(network) + + flow_originator_account = read_account(network, "originator") + + alice_account_rosetta = rosetta_create_account_transaction(network=network, + transaction_type="create_account", + root_originator=flow_originator_account, + account_name="alice_rosetta", + operation_id=0) + + bob_proxy_account_rosetta = rosetta_create_account_transaction(network=network, + transaction_type="create_proxy_account", + root_originator=flow_originator_account, + account_name="bob_proxy_account_rosetta", + operation_id=0) + + rosetta_transfer(network=network, + sender_name="originator", + sender_address=flow_originator_account["address"], + receiver_name="alice_rosetta", + receiver_address=alice_account_rosetta["address"], + amount=50, + i=0) + time.sleep(20) # Hacky fix to not check nonce + + rosetta_transfer(network=network, + sender_name="originator", + sender_address=flow_originator_account["address"], + receiver_name="bob_proxy_account_rosetta", + receiver_address=bob_proxy_account_rosetta["address"], + amount=50, + i=0) + time.sleep(20) + + # TODO: Proxy transfer doesn't work for now. Make it work + flow_proxy_account = read_account(network, "proxy_account") + rosetta_proxy_transfer(network=network, + sender_name="bob_proxy_account_rosetta", + sender_address=bob_proxy_account_rosetta["address"], + receiver_name="proxy_account", + receiver_address=flow_proxy_account["address"], + originator_root_name="originator", + originator_root_address=flow_originator_account["address"], + amount=10, + i=0) + + print("Test script is finished successfully") + + +def parse_network_and_signer_args(): + network = parse_network() + signer = set_account_signer(network) + return network, signer + + +def parse_network(): + if "--network" in sys.argv: + try: + network = sys.argv[sys.argv.index("--network") + 1] + except IndexError: + raise Exception("Error: --network flag provided without a value") + + if network != "testnet" and network != "previewnet": + raise Exception("Error: only 2 networks are supported: testnet or previewnet") + + return network + + +def set_account_signer(network: str): + if network == "testnet": + account_signer = "testnet_account_signer" + elif network == "previewnet": + account_signer = "previewnet_account_signer" + else: + raise Exception(f"Error: unexpected {network} argument") + + return account_signer + + +# Initializes the Flow CLI configuration for the network. +# +# This function initializes the Flow CLI configuration for the network. It also loads the network account signer from +# the `{network}_account_signer.json` file and saves it to the `flow.json` configuration file. +# +def init_flow_json(network: str, account_signer: str): + cmd = f"flow-c1 init --config-only --network {network}" + result = subprocess.run(cmd.split(), stdout=subprocess.PIPE, cwd=get_script_directory()) + if result.returncode != 0: + raise Exception("Couldn't init directory with `flow` CLI. Is it installed?") + + # We use signer account to create other accounts and sign transactions + account_signer_file_name = f"{account_signer}.json" + signer_account = read_account_signer(get_file_path(account_signer_file_name)) + save_account_to_flow_json(f"{account_signer}", signer_account) + + +# Creates an originator account and sets up the necessary configuration. +# +# This function creates a new Flow account to be used as the originator account. It saves the account details +# to the `flow.json` configuration file and deploys a contract to the account. It also updates the `{network}.json` +# configuration file with the originator's address and the deployed contract address. Finally, it saves the +# originator account details to the `accounts.json` file for use with the Rosetta API. +# +def create_originator(network: str, account_signer: str): + originator = create_flow_account(network, account_signer) + + # Save originator to flow.json + originator_config_data = { + "address": originator["address"], + "key": { + "type": "hex", + "index": 0, + "signatureAlgorithm": "ECDSA_secp256k1", + "hashAlgorithm": "SHA3_256", + "privateKey": originator["private_key"] + } + } + save_account_to_flow_json("originator", originator_config_data) + + deploy_contract(network, "originator", originator["address"]) + + # Add originator to network config file and update flow_cold_storage_proxy contract address + filename = f"{network}.json" + with open(get_file_path(filename), "r+") as json_file: + data = json.load(json_file) + data["originators"] = [remove_hex_prefix(originator["address"])] + data["contracts"]["flow_cold_storage_proxy"] = originator["address"] + json_file.seek(0) + json.dump(data, json_file, indent=4) + json_file.truncate() + + # Functions related to rosetta look for created accounts in accounts json file + filename = f"accounts-{network}.json" + originator["address"] = add_hex_prefix(originator["address"]) + + with open(get_file_path(filename), 'w+') as account_file: + accounts = dict() + accounts["originator"] = originator + data = json.dumps(accounts, indent=4) + account_file.seek(0) + account_file.write(data) + + +# Creates a proxy account and saves its details to the accounts.json file. +# +# This function creates a new Flow account to be used as a proxy account. It converts the account address +# to the Rosetta format and saves the account details to the `accounts.json` file. +# +def create_proxy_account(network: str, account_signer: str): + proxy_account = create_flow_account(network, account_signer) + proxy_account["address"] = add_hex_prefix( + proxy_account["address"]) + save_account(network, "proxy_account", proxy_account) + + +# Sets up the Rosetta server for the network. +# +# This function builds the Rosetta server using the `make` command. It then prompts the user to start the +# Rosetta server with the `{network}.json` configuration file in a separate terminal session. +# +def setup_rosetta(network: str): + cwd = os.path.join(get_script_directory(), "..") + result = subprocess.run(["make"], stdout=subprocess.PIPE, cwd=cwd) + if result.returncode != 0: + raise Exception("Couldn't build rosetta. `make` command failed") + + # TODO: we can do it ourselves without user interaction + _ = input(f"Please start rosetta by running ./server [path to {network}.json] in different terminal.\n" + f"Press enter in this terminal when you've finished...") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/localnet.json b/tests/localnet.json similarity index 100% rename from localnet.json rename to tests/localnet.json diff --git a/integration_test.py b/tests/localnet_integration_test.py similarity index 94% rename from integration_test.py rename to tests/localnet_integration_test.py index c773148..ad098e8 100644 --- a/integration_test.py +++ b/tests/localnet_integration_test.py @@ -39,7 +39,7 @@ ###################################################################################### def clone_flowgo_cmd(): - searchfile = open("go.mod", "r") + searchfile = open("../go.mod", "r") cmd = "" for line in searchfile: if "/onflow/flow-go " in line: @@ -69,7 +69,7 @@ def init_localnet(): subprocess.run(start_localnet_cmd.split(" "), stdout=subprocess.PIPE) def init_flow_json(): - with open('flow.json', 'w') as json_file: + with open('../flow.json', 'w') as json_file: json.dump(localnet_const, json_file, indent=4) @@ -112,7 +112,7 @@ def gen_contract_account(account_name): # Open flow.json in read and write mode and load its content as a JSON object. # Add the new account to the JSON object under the key specified by account_name. # Write back the modified JSON object to the file and truncate any remaining content. - with open('flow.json', "r+") as json_file: + with open('../flow.json', "r+") as json_file: data = json.load(json_file) data["accounts"][account_name] = contract_account_value json_file.seek(0) @@ -122,12 +122,12 @@ def gen_contract_account(account_name): # Open account-keys.csv in append mode and write a line that contains the account name, public flow key, # public rosetta key, private key, and address of the new account, separated by commas. # This file is probably used to store and manage the keys for different accounts. - with open('account-keys.csv', "a+") as file_object: + with open('../account-keys.csv', "a+") as file_object: file_object.write(account_name + "," + public_flow_key + "," + public_rosetta_key + "," + private_key + ",0x" + address + "\n") def deploy_contracts(account_name): - contract_path = "./script/cadence/contracts/FlowColdStorageProxy.cdc" + contract_path = "../script/cadence/contracts/FlowColdStorageProxy.cdc" deploy_contract_cmd = ("flow accounts add-contract " + contract_path + " --signer " + account_name + " FlowColdStorageProxy -f flow.json") cmds = deploy_contract_cmd.split(" ") + localnet_flags @@ -140,7 +140,7 @@ def setup_rosetta(): _ = input("Please start rosetta with the localnet config file $./server localnet.json\n") def seed_contract_accounts(): - with open('account-keys.csv', "r+") as file_object: + with open('../account-keys.csv', "r+") as file_object: reader = csv.reader(file_object) for row in reader: address = row[-1] @@ -164,7 +164,7 @@ def gen_account_keys(): return (public_flow_key, public_rosetta_key, private_key) def get_account_keys(account): - with open("account-keys.csv") as search: + with open("../account-keys.csv") as search: for line in search: if account in line: keys = line.split(",") @@ -208,7 +208,7 @@ def rosetta_create_account(root_originator, root_originator_name="root-originato tx_hash = submit_transaction_response["transaction_identifier"]["hash"] ## TODO: Convert to take input from flow cli flow_address = input("What is the flow address generated? flow transactions get " + tx_hash + " + -n localnet)\n") - with open('account-keys.csv', "a+") as file_object: + with open('../account-keys.csv', "a+") as file_object: account_name = root_originator_name + "-create_account," row_data = account_name + public_flow_key + "," + public_rosetta_key + "," + new_private_key + "," + flow_address + "\n" file_object.write(row_data) @@ -240,7 +240,7 @@ def rosetta_create_proxy_account(root_originator, root_originator_name="root-ori tx_hash = submit_transaction_response["transaction_identifier"]["hash"] ## TODO: Convert to take input from flow cli flow_address = input("What is the flow address generated? flow transactions get " + tx_hash + " + -n localnet)\n") - with open('account-keys.csv', "a+") as file_object: + with open('../account-keys.csv', "a+") as file_object: account_name = root_originator_name + "-create_proxy_account," row_data = account_name + public_flow_key + "," + public_rosetta_key + "," + new_private_key + "," + flow_address + "\n" file_object.write(row_data) @@ -476,15 +476,15 @@ def main(): _, _, _, root_address = get_account_keys("root-originator-account-1") print("root_address" + root_address) - # rosetta_create_account(root_address, "root-originator-account-1") - # rosetta_create_proxy_account(root_address, "root-originator-account-1") - # _, _, _, new_address = get_account_keys("root-originator-account-1-create_account") - - # rosetta_transfer(root_address, new_address, 50) - # _, _, _, new_proxy_address = get_account_keys("root-originator-account-1-create_proxy_account") - # rosetta_transfer(root_address, new_proxy_address, 50) - # _, _, _, flow_account_address = get_account_keys("flow-account") - # rosetta_proxy_transfer(new_proxy_address, flow_account_address, root_address, 10) + rosetta_create_account(root_address, "root-originator-account-1") + rosetta_create_proxy_account(root_address, "root-originator-account-1") + _, _, _, new_address = get_account_keys("root-originator-account-1-create_account") + + rosetta_transfer(root_address, new_address, 50) + _, _, _, new_proxy_address = get_account_keys("root-originator-account-1-create_proxy_account") + rosetta_transfer(root_address, new_proxy_address, 50) + _, _, _, flow_account_address = get_account_keys("flow-account") + rosetta_proxy_transfer(new_proxy_address, flow_account_address, root_address, 10) if __name__ == "__main__": diff --git a/mainnet.json b/tests/mainnet.json similarity index 100% rename from mainnet.json rename to tests/mainnet.json diff --git a/previewnet.json b/tests/previewnet.json similarity index 86% rename from previewnet.json rename to tests/previewnet.json index 69cd0b6..a933793 100644 --- a/previewnet.json +++ b/tests/previewnet.json @@ -6,7 +6,7 @@ } ], "contracts": { - "flow_cold_storage_proxy": "cf706c70db9dab9b", + "flow_cold_storage_proxy": "0x05f842cd1b178690", "flow_fees": "ab086ce9cc29fc80", "flow_token": "4445e7ad11568276", "fungible_token": "a0225e7000ac82a9", @@ -16,7 +16,7 @@ "disable_consensus_follower": true, "network": "previewnet", "originators": [ - "cf706c70db9dab9b" + "0x05f842cd1b178690" ], "port": 8080, "spork_seal_tolerance": 10000, @@ -28,7 +28,7 @@ "address": "access.previewnet.nodes.onflow.org:9000" } ], - "root_block": 14700328, + "root_block": 29859120, "version": 7 } } diff --git a/tests/previewnet_account_signer.json b/tests/previewnet_account_signer.json new file mode 100644 index 0000000..59bcb97 --- /dev/null +++ b/tests/previewnet_account_signer.json @@ -0,0 +1,10 @@ +{ + "address": "941840a945dddfd0", + "key": { + "type": "hex", + "index": 0, + "signatureAlgorithm": "ECDSA_secp256k1", + "hashAlgorithm": "SHA3_256", + "privateKey": "573e4f583fa08997e3ca91a4adcbcd5831ccb0b4d8e47476bec0b305c3e5b79a" + } +} \ No newline at end of file diff --git a/tests/rosetta.py b/tests/rosetta.py new file mode 100644 index 0000000..ad3d096 --- /dev/null +++ b/tests/rosetta.py @@ -0,0 +1,471 @@ +import json +import subprocess + +import requests + +import helpers + +rosetta_host_url = "http://127.0.0.1:8080" + + +# Creates a new Flow account using the Rosetta API. +# +# This function creates a new Flow account using the Rosetta API by following these steps: +# 1. Generates a new set of Flow keys (public Flow key, public Rosetta key, private key). +# 2. Constructs the necessary operations and metadata for the account creation transaction. +# 3. Preprocesses the transaction using the Rosetta API. +# 4. Retrieves the transaction metadata and payloads using the Rosetta API. +# 5. Signs the transaction payload using a Go script and the root originator's private key. +# 6. Combines the signed transaction payload with the unsigned transaction using the Rosetta API. +# 7. Submits the signed transaction using the Rosetta API. +# 8. Prompts the user to enter the generated Flow address from the transaction explorer. +# 9. Saves the new account's details (address, public key, private key, Rosetta key) to the accounts file. +# +# Args: +# transaction_type (str): The type of the transaction (e.g., "create_account", "create_proxy_account"). +# root_originator (dict): A dictionary containing the address, private key, and Rosetta key of originator account. +# account_name (str): The name to assign to the new account in the accounts file. +# operation_id (int): The index of the operation in the transaction. +# +# Returns: +# dict: A dictionary containing the new account's details (address, public key, private key, Rosetta key). +# +def rosetta_create_account_transaction(network, transaction_type, root_originator, account_name, operation_id): + public_flow_key, public_rosetta_key, flow_private_key = helpers.generate_keys() + metadata = { + "public_key": public_rosetta_key + } + operations = [ + { + "type": transaction_type, + "operation_identifier": { + "index": operation_id + }, + "metadata": metadata + } + ] + + preprocess_response = preprocess_transaction(network, + root_originator["address"], + operations) + if "options" not in preprocess_response: + raise Exception("Preprocess transaction returned unexpected response") + metadata_response = metadata_transaction(network, + preprocess_response["options"]) + if "metadata" not in metadata_response: + raise Exception("Metadata transaction finished returned unexpected response") + payloads_response = payloads_transaction(network, + operations, + metadata_response["metadata"]["protobuf"]) + if "payloads" not in payloads_response: + raise Exception("Payloads transaction returned unexpected response") + hex_bytes = payloads_response["payloads"][0]["hex_bytes"] + + sign_tx_cmd = "go run ../cmd/sign/sign.go " + \ + root_originator["private_key"] + " " + hex_bytes + result = subprocess.run(sign_tx_cmd.split(), stdout=subprocess.PIPE, cwd=helpers.get_script_directory()) + if result.returncode != 0: + raise Exception(f"Couldn't sign the tx. {sign_tx_cmd} finished with non-zero code") + signed_tx = result.stdout.decode('utf-8')[:-1] + + unsigned_tx = payloads_response["unsigned_transaction"] + + combine_tx_response = combine_transaction(network, + unsigned_tx, + root_originator["address"], + hex_bytes, + root_originator["rosetta_key"], + signed_tx) + + submit_transaction_response = submit_transaction(network, + combine_tx_response["signed_transaction"]) + tx_hash = submit_transaction_response["transaction_identifier"]["hash"] + print("Look for the account that has Received 0.00100000 Flow.") + generated_address = input(f"Enter generated Flow address at https://{network}.flowdiver.io/tx/{tx_hash}\n" + "(wait for a second if tx is not processed yet): ") + + created_account = { + "address": generated_address, + "public_key": public_flow_key, + "private_key": flow_private_key, + "rosetta_key": public_rosetta_key + } + helpers.save_account(network, account_name, created_account) + + return created_account + + +def rosetta_transfer(network: str, sender_name, sender_address, receiver_name, receiver_address, amount, i): + transaction = "transfer" + operations = [ + { + "type": transaction, + "operation_identifier": { + "index": i + }, + "account": { + "address": sender_address + }, + "amount": { + "currency": { + "decimals": 8, + "symbol": "FLOW" + }, + "value": str(-1 * amount * 10 ** 7) + } + }, + { + "type": transaction, + "operation_identifier": { + "index": i + 1 + }, + "related_operations": [ + { + "index": i + } + ], + "account": { + "address": receiver_address + }, + "amount": { + "currency": { + "decimals": 8, + "symbol": "FLOW" + }, + "value": str(amount * 10 ** 7) + } + } + ] + + preprocess_response = preprocess_transaction(network, sender_address, operations) + metadata_response = metadata_transaction(network, preprocess_response["options"]) + payloads_response = payloads_transaction(network, + operations, + metadata_response["metadata"]["protobuf"]) + + _, _, private_key, rosetta_key = helpers.read_account_keys(network, sender_name) + hex_bytes = payloads_response["payloads"][0]["hex_bytes"] + + unsigned_tx = payloads_response["unsigned_transaction"] + + sign_tx_cmd = "go run ../cmd/sign/sign.go " + private_key + " " + hex_bytes + result = subprocess.run(sign_tx_cmd.split(), stdout=subprocess.PIPE, cwd=helpers.get_script_directory()) + if result.returncode != 0: + raise Exception(f"Couldn't sign the tx. {sign_tx_cmd} finished with non-zero code") + signed_tx = result.stdout.decode('utf-8')[:-1] + + combine_response = combine_transaction(network, unsigned_tx, sender_address, hex_bytes, rosetta_key, signed_tx) + + submit_transaction_response = submit_transaction(network, combine_response["signed_transaction"]) + tx_hash = submit_transaction_response["transaction_identifier"]["hash"] + + print(f"Transferring {str(amount)} Flow from {sender_name} ({sender_address})" + f" to {receiver_name} ({receiver_address})") + print(f"Transaction submitted... https://{network}.flowdiver.io/tx/{tx_hash}") + + +# Transfers Flow tokens between two accounts using the Rosetta API. +# +# This function transfers Flow tokens from one account to another using the Rosetta API. +# It constructs the necessary operations for the transfer transaction, signs the transaction +# payload, and submits the signed transaction to the Flow network. +# +# Args: +# originator (str): The address of the account sending the tokens. +# destination (str): The address of the account receiving the tokens. +# amount (float): The amount of Flow tokens to transfer. +# i (int, optional): The index of the first operation in the transaction. Defaults to 0. +# +# Returns: +# None +# +# Raises: +# subprocess.CalledProcessError: If the transaction signing process fails. +# +def rosetta_proxy_transfer(network, + sender_name, + sender_address, + receiver_name, + receiver_address, + originator_root_name, + originator_root_address, + amount, + i): + transaction = "proxy_transfer_inner" + operations = [ + { + "type": transaction, + "operation_identifier": { + "index": i + }, + "account": { + "address": sender_address + }, + "amount": { + "currency": { + "decimals": 8, + "symbol": "FLOW" + }, + "value": str(-1 * amount * 10 ** 7) + } + }, + { + "type": transaction, + "operation_identifier": { + "index": i + 1 + }, + "related_operations": [ + { + "index": i + } + ], + "account": { + "address": receiver_address + }, + "amount": { + "currency": { + "decimals": 8, + "symbol": "FLOW" + }, + "value": str(amount * 10 ** 7) + } + } + ] + + # sender signs the transaction + preprocess_response = preprocess_transaction(network, sender_address, operations) + metadata_response = metadata_transaction(network, preprocess_response["options"]) + payloads_response = payloads_transaction(network, + operations, + metadata_response["metadata"]["protobuf"]) + hex_bytes = payloads_response["payloads"][0]["hex_bytes"] + unsigned_tx = payloads_response["unsigned_transaction"] + + # sign tx + sender_account = helpers.read_account(network, sender_name) + sender_private_key = sender_account["private_key"] + sign_tx_cmd = f"go run ../cmd/sign/sign.go {sender_private_key} {hex_bytes}" + result = subprocess.run(sign_tx_cmd.split(), stdout=subprocess.PIPE, cwd=helpers.get_script_directory()) + if result.returncode != 0: + raise Exception(f"Couldn't sign the tx. {sign_tx_cmd} finished with non-zero code") + signed_tx = result.stdout.decode('utf-8')[:-1] + + combine_response = combine_transaction(network, + unsigned_tx, + sender_address, + hex_bytes, + sender_account["rosetta_key"], + signed_tx) + combined_signed_tx = combine_response["signed_transaction"] + + # transaction from originator root + preprocess_response = preprocess_transaction(network, + originator_root_address, + operations, + metadata={"proxy_transfer_payload": combined_signed_tx}) + metadata_response = metadata_transaction(network, preprocess_response["options"]) + payloads_response = payloads_transaction(network, operations, metadata_response["metadata"]["protobuf"]) + hex_bytes = payloads_response["payloads"][0]["hex_bytes"] + unsigned_tx = payloads_response["unsigned_transaction"] + + # sign tx + originator_root_account = helpers.read_account(network, originator_root_name) + originator_root_private_key = originator_root_account["private_key"] + sign_tx_cmd = f"go run ../cmd/sign/sign.go {originator_root_private_key} {hex_bytes}" + result = subprocess.run(sign_tx_cmd.split(), stdout=subprocess.PIPE, cwd=helpers.get_script_directory()) + if result.returncode != 0: + raise Exception(f"Couldn't sign the tx. {sign_tx_cmd} finished with non-zero code") + signed_tx = result.stdout.decode('utf-8')[:-1] + + combine_response = combine_transaction(network, + unsigned_tx, + originator_root_address, + hex_bytes, + originator_root_account["rosetta_key"], + signed_tx) + + submit_transaction_response = submit_transaction(network, combine_response["signed_transaction"]) + tx_hash = submit_transaction_response["transaction_identifier"]["hash"] + + print( + f"Proxy transferring {amount} Flow from {sender_name} ({sender_address}) to {receiver_name} ({receiver_address})." + f" Proxied through {originator_root_name} ({originator_root_address})") + print(f"Transaction submitted... https://{network}.flowdiver.io/tx/{tx_hash}") + + +# Preprocesses a transaction using the Rosetta API. +# +# This function sends a request to the Rosetta API to preprocess a transaction. It constructs +# the necessary data payload with the provided operations and metadata, and sends a POST request +# to the "/construction/preprocess" endpoint. The response from the API is returned. +# +# Args: +# root_originator (str): The address of the root originator account. +# operations (list): A list of operation dictionaries representing the transaction operations. +# metadata (dict, optional): Additional metadata to include in the request payload. +# +# Returns: +# dict: The response from the Rosetta API's "/construction/preprocess" endpoint. +# +def preprocess_transaction(network, root_originator, operations, metadata=None): + endpoint = "/construction/preprocess" + target_url = rosetta_host_url + endpoint + data = { + "network_identifier": { + "blockchain": "flow", + "network": network + }, + "operations": operations, + "metadata": { + "payer": root_originator + } + } + if metadata: + for key in metadata: + data["metadata"][key] = metadata[key] + return request_router(target_url, data) + + +# Retrieves transaction metadata using the Rosetta API. +# +# This function sends a request to the Rosetta API to retrieve metadata for a transaction. It constructs +# the necessary data payload with the provided options, and sends a POST request to the "/construction/metadata" +# endpoint. The response from the API is returned. +# +# Args: +# options (dict): The options dictionary returned from the "/construction/preprocess" endpoint. +# +# Returns: +# dict: The response from the Rosetta API's "/construction/metadata" endpoint. +# +def metadata_transaction(network, options): + endpoint = "/construction/metadata" + target_url = rosetta_host_url + endpoint + data = { + "network_identifier": { + "blockchain": "flow", + "network": network + }, + "options": options + } + return request_router(target_url, data) + + +# Retrieves transaction payloads using the Rosetta API. +# +# This function sends a request to the Rosetta API to retrieve payloads for a transaction. It constructs +# the necessary data payload with the provided operations and metadata, and sends a POST request to the +# "/construction/payloads" endpoint. The response from the API is returned. +# +# Args: +# operations (list): A list of operation dictionaries representing the transaction operations. +# protobuf (str): The protobuf metadata returned from the "/construction/metadata" endpoint. +# +# Returns: +# dict: The response from the Rosetta API's "/construction/payloads" endpoint. +# +def payloads_transaction(network, operations, protobuf): + endpoint = "/construction/payloads" + target_url = rosetta_host_url + endpoint + data = { + "network_identifier": { + "blockchain": "flow", + "network": network + }, + "operations": operations, + "metadata": { + "protobuf": protobuf + } + } + return request_router(target_url, data) + + +# Combines an unsigned transaction with a signed payload using the Rosetta API. +# +# This function sends a request to the Rosetta API to combine an unsigned transaction with a signed payload. +# It constructs the necessary data payload with the unsigned transaction, root originator details, signed payload, +# and public key, and sends a POST request to the "/construction/combine" endpoint. The response from the API +# is returned. +# +# Args: +# unsigned_tx (str): The unsigned transaction. +# root_originator (str): The address of the root originator account. +# hex_bytes (str): The hexadecimal bytes of the signed payload. +# rosetta_key (str): The public key of the root originator account in hexadecimal format. +# signed_tx (str): The signed transaction payload. +# +# Returns: +# dict: The response from the Rosetta API's "/construction/combine" endpoint. +# +def combine_transaction(network, unsigned_tx, root_originator, hex_bytes, rosetta_key, signed_tx): + endpoint = "/construction/combine" + target_url = rosetta_host_url + endpoint + data = { + "network_identifier": { + "blockchain": "flow", + "network": network + }, + "unsigned_transaction": unsigned_tx, + "signatures": [ + { + "signing_payload": { + "account_identifier": { + "address": root_originator + }, + "address": root_originator, + "hex_bytes": hex_bytes, + "signature_type": "ecdsa" + }, + "public_key": { + "hex_bytes": rosetta_key, + "curve_type": "secp256k1" + }, + "signature_type": "ecdsa", + "hex_bytes": signed_tx + } + ] + } + return request_router(target_url, data) + + +# Submits a signed transaction using the Rosetta API. +# +# This function sends a request to the Rosetta API to submit a signed transaction. It constructs the +# necessary data payload with the signed transaction, and sends a POST request to the "/construction/submit" +# endpoint. The response from the API is returned. +# +# Args: +# signed_tx (str): The signed transaction. +# +# Returns: +# dict: The response from the Rosetta API's "/construction/submit" endpoint. +# +def submit_transaction(network, signed_tx): + endpoint = "/construction/submit" + target_url = rosetta_host_url + endpoint + data = { + "network_identifier": { + "blockchain": "flow", + "network": network + }, + "signed_transaction": signed_tx + } + return request_router(target_url, data) + + +# Sends a POST request to a target URL with a JSON body. +# +# This function sends a POST request to a target URL with a JSON body. +# It returns the response as a JSON object. +# +# Args: +# target_url (str): The target URL to send the request to. +# body (dict): The JSON body to send in the request. +# +# Returns: +# dict: The response from the server as a JSON object. +# +def request_router(target_url, body): + headers = {'Content-type': 'application/json'} + r = requests.post(target_url, data=json.dumps(body), headers=headers) + return r.json() \ No newline at end of file diff --git a/testnet.json b/tests/testnet.json similarity index 93% rename from testnet.json rename to tests/testnet.json index ec62d19..d129dea 100644 --- a/testnet.json +++ b/tests/testnet.json @@ -20,13 +20,13 @@ "port": 8080, "spork_seal_tolerance": 600, "sporks": { - "50": { + "51": { "access_nodes": [ { "address": "access.testnet.nodes.onflow.org:9000" } ], - "root_block": 185185854, + "root_block": 211176670, "version": 6 } } diff --git a/tests/testnet_account_signer.json b/tests/testnet_account_signer.json new file mode 100644 index 0000000..0811c97 --- /dev/null +++ b/tests/testnet_account_signer.json @@ -0,0 +1,10 @@ +{ + "address": "129b3055d5c17977", + "key": { + "type": "hex", + "index": 0, + "signatureAlgorithm": "ECDSA_secp256k1", + "hashAlgorithm": "SHA3_256", + "privateKey": "8ad6b3c4ab1cb753139285870c9361590269ed633356f7349067c74b8080e834" + } +} \ No newline at end of file