From c161401f93a3fab583ceaa973d322a92710e9ff7 Mon Sep 17 00:00:00 2001 From: Daniel Briskin Date: Fri, 25 Oct 2024 15:58:08 +0900 Subject: [PATCH] Initial commit --- .github/workflows/test.yml | 42 +++ .gitignore | 14 + .gitmodules | 3 + .vscode/extensions.json | 9 + .vscode/settings.json | 6 + README.md | 131 +++++++++ foundry.toml | 7 + lib/forge-std | 1 + main.py | 573 +++++++++++++++++++++++++++++++++++++ src/MultiBaas.sol | 75 +++++ 10 files changed, 861 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 100644 main.py create mode 100644 src/MultiBaas.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3edf3ec --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + # - name: Run Forge tests + # run: | + # forge test -vvv + # id: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..8ecb6ec --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + // Solidity + "nomicfoundation.hardhat-solidity", + // Python + "ms-python.python", + "ms-python.black-formatter" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..789c680 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "solidity.formatter": "forge", + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3a660d --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# Forge MultiBaas Library + +The `forge-multibaas` library allows developers to easily upload and link their Solidity contracts to a [MultiBaas](https://www.curvegrid.com/multibaas/) deployment as they develop and deploy smart contracts using [Foundry Forge](https://book.getfoundry.sh/). + +Forge is a high-performance, portable, and modular Ethereum development toolchain that enables developers to compile, test, deploy, and interact with Solidity smart contracts. It is a part of the broader Foundry project, which aims to provide an easy-to-use and fast toolkit for blockchain development on Ethereum-compatible networks. + +By integrating the `forge-multibaas` library, you can streamline your development workflow by automatically uploading and linking your contracts to a MultiBaas deployment directly from your Forge scripts. + +For more information on how to use MultiBaas, please see our [docs](https://docs.curvegrid.com/multibaas/). + +## Installation + +If you have not already setup Forge, follow the [Foundry installation guide](https://book.getfoundry.sh/getting-started/installation.html). + +Then, install the library into your project by running: + +```bash +forge install curvegrid/forge-multibaas +``` + +## System Requirements + +The Forge MultiBaas library relies on Python 3 to run its backend operations. Ensure that Python 3 is installed and can be run in your terminal via the `python3` command. + +### Foundry Setup Requirements + +Make sure your `foundry.toml` file includes the following: + +```toml +libs = ["lib"] +``` + +This is necessary due to the way the library is added as a submodule to your Foundry project. + +## Environment Setup + +The library requires certain environment variables to be defined: + +- `MULTIBAAS_URL`: The MultiBaas deployment URL, including the protocol (e.g., `https://`) +- `MULTIBAAS_API_KEY`: An API key created on the MultiBaas deployment with admin group permissions. + +Optionally, the following variables can be set to bypass safeguards: + +- `MULTIBAAS_ALLOW_UPDATE_CONTRACT=true`: Allows contracts to be reuploaded and labelled as an already existing version in MultiBaas, even if the bytecode changes. +- `MULTIBAAS_ALLOW_UPDATE_ADDRESS=true`: Allows addresses to be updated in MultiBaas if the address label conflicts. + +If you choose to set these variables using an `.env` file, since the `MULTIBAAS_API_KEY` grants full admin access to the MultiBaas deployment, we strongly advise against checking it into source control. + +## Usage + +### Deploying Contracts with MultiBaas + +The library provides a simple interface for linking contracts to MultiBaas. Building on top of the [`hello_foundry` tutorial](https://book.getfoundry.sh/projects/creating-a-new-project), here’s an example Counter deployment script (`script/Counter.s.sol`) that uses the library. + +Please note that in order for this example deployment script to run, the `PRIVATE_KEY` environment variable should be set to your `0x`-prefixed deployer private key. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {MultiBaas} from "forge-multibaas/MultiBaas.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterScript is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Deploy the first NFT contract + Counter counter = new Counter(); + + // Upload and link the contract in MultiBaas with default options + MultiBaas.linkContract("Counter", address(counter)); + + // Deploy a second Counter contract + Counter secondCounter = new Counter(); + + // Upload and link the second contract in MultiBaas with custom options + bytes memory encodedOptions = MultiBaas.withOptions( + "counter", // Arbitrary MultiBaas label for the uploaded contract code and associated ABI + "counter", // Arbitrary MultiBaas label for the address at which the contract is deployed + "1.5", // Arbitrary MultiBaas version label for the uploaded contract code and associated ABI + "-50" // Start syncing events from 50 blocks prior to the current network block + ); + MultiBaas.linkContractWithOptions("Counter", address(secondCounter), encodedOptions); + + vm.stopBroadcast(); + } +} +``` + +For details on the encoded options, please see the section on [Options Encoding](#options-encoding). + +### Running the Script + +To run the deployment script and broadcast the transactions, use the following command. Please note that this command requires the `NETWORK_RPC_URL` environment variable to have been set to an HTTP or WS RPC endpoint of the same network that the MultiBaas deployment is connected to. + +```bash +forge script script/Counter.s.sol:CounterScript --rpc-url $NETWORK_RPC_URL --broadcast --ffi +``` + +**Note:** The script must be run with the [`--ffi` flag](https://book.getfoundry.sh/cheatcodes/ffi) to allow the Python script for MultiBaas integration to be executed. Alternatively, the `ffi = true` flag can be set in the `foundry.toml` file. Note that FFI allows for arbitrary code to be executed by any Forge library, and for now it is a workaround as configuring MultiBaas requires making HTTP calls which are not yet natively supported in Forge. + +### Forge MultiBaas Library Functions + +The `forge-multibaas` library provides the following key functions: + +- `linkContract`: Links a contract to MultiBaas with default options. +- `linkContractWithOptions`: Allows you to link a contract to MultiBaas with custom options, such as custom labels, versioning, and event syncing blocks. + +#### Example Usage in Solidity: + +```solidity +bytes memory encodedOptions = MultiBaas.withOptions( + "custom_contract_label", // Arbitrary MultiBaas label for the uploaded contract code and associated ABI + "custom_address_label", // Arbitrary MultiBaas label for the address at which the contract is deployed + "1.0", // Arbitrary MultiBaas version label for the uploaded contract code and associated ABI + "-100" // Start syncing events from 100 blocks prior to the current network block +); +MultiBaas.linkContractWithOptions("MyContract", address(myContract), encodedOptions); +``` + +### Options Encoding + +You can customize the following options when linking contracts: + +- **`contractLabel`**: The label for the contract. Defaults to the contract name in lowercase. +- **`addressLabel`**: The label for the contract address. Defaults to the contract name in lowercase. +- **`contractVersion`**: The version label for the contract. Defaults to `1.0` and if left empty it auto-increments as the contract bytecode changes. If it is set to a version string that is already uploaded to MultiBaas, but the contract bytecode has changed, then the `MULTIBAAS_ALLOW_UPDATE_CONTRACT` environment variable must be set to `true` to allow for the version label to be reused on the new contract bytecode. +- **`startingBlock`**: The block number from which to start syncing events in MultiBaas. Negative values represent blocks before the current block; `0` represents the current block. Positive values represent absolute block numbers on the blockchain. diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..cbd8bf8 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +ffi = true + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..1eea5ba --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/main.py b/main.py new file mode 100644 index 0000000..51a326f --- /dev/null +++ b/main.py @@ -0,0 +1,573 @@ +import sys +import json +import urllib.request +import os +import re +import traceback +import subprocess +from typing import Optional, Dict, Any, Tuple + + +# Custom Exception for MultiBaas API errors +class MultiBaasAPIError(Exception): + """ + Exception raised for errors in the MultiBaas API interactions. + """ + + def __init__(self, path: str, status_code: Any, message: str): + self.path = path + self.status_code = status_code + self.message = message + super().__init__( + f"MultiBaas API Error [{status_code}] while calling {path}: {message}" + ) + + +def filter_empty_values(options: Dict[str, str]) -> Dict[str, str]: + """ + Filters out key-value pairs from a dictionary where the value is an empty string. + + Args: + options (dict): The dictionary to filter. + + Returns: + dict: A new dictionary with empty string values removed. + """ + return {k: v for k, v in options.items() if v} + + +def mb_request( + mb_url: str, + mb_api_key: str, + path: str, + method: str = "GET", + data: Optional[Dict] = None, +) -> Any: + """ + Helper function for making requests to MultiBaas API. + + Args: + mb_url (str): The base URL for MultiBaas. + mb_api_key (str): The API key for authentication. + path (str): The API endpoint path. + method (str): HTTP method ('GET', 'POST', 'DELETE'). + data (dict, optional): Data to send in the request body. + + Returns: + Any: The result from the API call. + + Raises: + MultiBaasAPIError: If the API call fails. + """ + url = f"{mb_url}/api/v0/{path.lstrip('/')}" + headers = { + "Authorization": f"Bearer {mb_api_key}", + "Content-Type": "application/json", + } + + req = urllib.request.Request(url, method=method) + for key, value in headers.items(): + req.add_header(key, value) + + if data is not None: + json_data = json.dumps(data).encode("utf-8") + req.data = json_data + + try: + with urllib.request.urlopen(req) as response: + res_body = response.read().decode("utf-8") + res_json = json.loads(res_body) + + # Check for 404 status code and raise a specific error + if response.status == 404: + raise MultiBaasAPIError(path, 404, "Not found") + + # Check if the response has "message": "success" + if res_json.get("message") != "success": + raise MultiBaasAPIError( + path, response.status, res_json.get("message", "Unknown error") + ) + + # Unwrap and return the result + return res_json.get("result") + + except urllib.error.HTTPError as e: + raise MultiBaasAPIError(path, e.code, e.reason) + except urllib.error.URLError as e: + raise MultiBaasAPIError(path, "URL Error", e.reason) + + +def get_artifact_dir() -> Optional[str]: + """ + Retrieve the artifact directory from Forge config. + + Returns: + Optional[str]: The artifact directory path or None if not found. + """ + try: + output = subprocess.run( + ["forge", "config", "--json"], capture_output=True, text=True + ) + config = json.loads(output.stdout) + return config.get("out", "out") # Fallback to 'out' if not found + except (subprocess.CalledProcessError, json.JSONDecodeError) as e: + print(f"Error retrieving artifact directory: {str(e)}") + return None + + +def get_multibaas_credentials() -> Tuple[str, str]: + """ + Validate and retrieve MultiBaas credentials from environment variables. + + Returns: + (str, str): A tuple containing the MultiBaas URL and API key. + + Exits: + Exits the program if the environment variables are not set. + """ + mb_url = os.getenv("MULTIBAAS_URL", "").rstrip("/") # Strip trailing slashes + mb_api_key = os.getenv("MULTIBAAS_API_KEY", "") + + if not mb_url or not mb_api_key: + print( + "Error: MULTIBAAS_URL and/or MULTIBAAS_API_KEY environment variables are not set." + ) + sys.exit(1) + + return mb_url, mb_api_key + + +def validate_api_key(mb_url: str, mb_api_key: str) -> bool: + """ + Validates the API key by making a request to MultiBaas. + + Args: + mb_url (str): The MultiBaas URL. + mb_api_key (str): The API key for authentication. + + Returns: + bool: True if validation is successful, False otherwise. + """ + try: + # Make the request to validate the API key + result = mb_request(mb_url, mb_api_key, "currentuser") + + if result: + print("API key validation successful.") + return True + else: + print("API key validation failed.") + return False + except MultiBaasAPIError as e: + print(f"Error during validation: {e}") + return False + + +def create_address( + mb_url: str, mb_api_key: str, address: str, contract_label: str, address_label: str +) -> Optional[Dict]: + """ + Creates an address in MultiBaas, handling label conflicts and updates. + + Args: + mb_url (str): The MultiBaas URL. + mb_api_key (str): The API key for authentication. + address (str): The Ethereum address to create. + contract_label (str): The label for the contract. + address_label (str): The label for the address. + + Returns: + Optional[Dict]: The created address information or None if failed. + """ + # Fetch allow_update_address from environment, default to False + allow_update_address = os.getenv( + "MULTIBAAS_ALLOW_UPDATE_ADDRESS", "false" + ).lower() in ["true", "1"] + + try: + # Check if the address already exists + result = mb_request(mb_url, mb_api_key, f"/chains/ethereum/addresses/{address}") + + # If the address exists, handle any conflicting labels + if result and result.get("label", "") != "": + existing_label = result["label"] + + if existing_label != address_label: + raise MultiBaasAPIError( + f"/chains/ethereum/addresses/{address}", + 409, + f"The address {address} has already been created under a different label '{existing_label}'", + ) + + print(f"Address {address} already created as '{existing_label}'") + return result + + except MultiBaasAPIError as e: + if e.status_code != 404: + raise # Re-raise exception if it's not a 404 Not Found error + # If the address is not found (404), proceed to create the address + + # Generate a unique address label if the default is conflicting + if address_label == contract_label: + # Check for similar labels and generate a new one if necessary + similar_labels = mb_request( + mb_url, + mb_api_key, + f"/chains/ethereum/addresses/similarlabels/{contract_label}", + ) + similar_set = set(label_info["label"] for label_info in similar_labels) + if contract_label in similar_set: + # Add a numeric suffix to create a unique label + num = 2 + while f"{contract_label}{num}" in similar_set: + num += 1 + address_label = f"{contract_label}{num}" + else: + try: + # Check if the label already exists + result = mb_request( + mb_url, mb_api_key, f"/chains/ethereum/addresses/{address_label}" + ) + + if not allow_update_address: + raise MultiBaasAPIError( + f"/chains/ethereum/addresses/{address_label}", + 409, + f"Another address has already been created under the label '{address_label}'", + ) + + # If allow_update_address is True, delete the conflicting address + if result and result.get("address", "") != "": + old_address = result["address"] + print( + f"Deleting old address {old_address} with label '{address_label}'" + ) + mb_request( + mb_url, + mb_api_key, + f"/chains/ethereum/addresses/{address_label}", + method="DELETE", + ) + + except MultiBaasAPIError as e: + if e.status_code != 404: + raise # Re-raise exception if it's not a 404 Not Found error + # If the label is not found (404), proceed to create the address + + # Create the new address + print(f"Creating address {address} with label '{address_label}'") + data = { + "address": address, + "label": address_label, + } + try: + created_address = mb_request( + mb_url, mb_api_key, "/chains/ethereum/addresses", method="POST", data=data + ) + return created_address + + except MultiBaasAPIError as e: + print(f"Failed to create address: {e}") + return None + + +def create_contract( + mb_url: str, + mb_api_key: str, + artifact_dir: str, + contract_name: str, + contract_label: str, + contract_version: Optional[str], +) -> Optional[Dict]: + """ + Creates or updates a contract in MultiBaas, handling versioning and conflicts. + + Args: + mb_url (str): The MultiBaas URL. + mb_api_key (str): The API key for authentication. + artifact_dir (str): The directory where artifacts are stored. + contract_name (str): The name of the contract. + contract_label (str): The label for the contract. + contract_version (str, optional): The version label for the contract. + + Returns: + Optional[Dict]: The created or existing contract information or None if failed. + """ + try: + # Load the artifact + artifact_path = os.path.join( + artifact_dir, f"{contract_name}.sol", f"{contract_name}.json" + ) + with open(artifact_path, "r") as artifact_file: + artifact_data = json.load(artifact_file) + + # Extract ABI, bytecode, devdoc, and userdoc + abi = json.dumps(artifact_data["abi"]) + bytecode = artifact_data["bytecode"] + if isinstance(bytecode, dict) and "object" in bytecode: + bytecode = bytecode["object"] + + devdoc = json.dumps( + artifact_data.get("metadata", {}).get("output", {}).get("devdoc", "{}") + ) + userdoc = json.dumps( + artifact_data.get("metadata", {}).get("output", {}).get("userdoc", "{}") + ) + + # Prepare payload with basic contract information + payload = { + "label": contract_label, + "language": "solidity", + "bin": bytecode, + "rawAbi": abi, + "contractName": contract_name, + "developerDoc": devdoc, + "userdoc": userdoc, + } + + # Check if the exact contract version exists + if contract_version is not None: + try: + mb_contract = mb_request( + mb_url, + mb_api_key, + f"/contracts/{contract_label}/{contract_version}", + ) + + # If contracts share the same bytecode, skip creation + if mb_contract["bin"] == payload["bin"]: + print( + f"Contract '{mb_contract['contractName']} {mb_contract['version']}' already exists. Skipping creation." + ) + return mb_contract + + # If the bytecode differs, check if updates are allowed + allow_update_contract = os.getenv( + "MULTIBAAS_ALLOW_UPDATE_CONTRACT", "false" + ).lower() in ["true", "1"] + if not allow_update_contract: + raise MultiBaasAPIError( + f"/contracts/{contract_label}/{contract_version}", + 409, + f"A different '{mb_contract['contractName']} {mb_contract['version']}' has already been deployed.", + ) + + # Delete the old contract if updates are allowed + print( + f"Deleting old contract with label='{contract_label}' and version='{contract_version}' to deploy a new one." + ) + mb_request( + mb_url, + mb_api_key, + f"/contracts/{contract_label}/{contract_version}", + method="DELETE", + ) + except MultiBaasAPIError as e: + if e.status_code != 404: + raise # Re-raise exception if it's not a 404 Not Found error + # If the contract version is not found (404), proceed to create the contract + + # If the version is not provided or needs to be incremented + if contract_version is None: + try: + mb_contract = mb_request( + mb_url, mb_api_key, f"/contracts/{contract_label}" + ) + + # Check if contracts share the same bytecode + if mb_contract["bin"] == payload["bin"]: + print( + f"Contract '{mb_contract['contractName']} {mb_contract['version']}' already exists. Skipping creation." + ) + return mb_contract + + version = mb_contract["version"] + # Increment version number if found + if re.search(r"\d+$", version): + version = re.sub( + r"\d+$", lambda m: str(int(m.group()) + 1), version + ) + else: + version += "2" + + contract_version = version + except MultiBaasAPIError as e: + if e.status_code != 404: + raise # Re-raise exception if it's not a 404 Not Found error + contract_version = "1.0" + + # Now that contract_version is finalized, add it to the payload + payload["version"] = contract_version + + # Proceed to create the contract + print(f"Creating contract '{contract_label} {contract_version}'") + mb_contract = mb_request( + mb_url, + mb_api_key, + f"/contracts/{contract_label}", + method="POST", + data=payload, + ) + + return mb_contract + + except FileNotFoundError: + print(f"Error: Artifact JSON file not found at {artifact_path}") + return None + except MultiBaasAPIError as e: + print(f"Failed to create contract: {e}") + return None + + +def link_contract_to_address( + mb_url: str, + mb_api_key: str, + contract_label: str, + contract_version: Optional[str], + address_label: str, + starting_block: str, +) -> None: + """ + Links a contract to an address in MultiBaas. + + Args: + mb_url (str): The MultiBaas URL. + mb_api_key (str): The API key for authentication. + contract_label (str): The label of the contract. + contract_version (str, optional): The version of the contract. + address_label (str): The label of the address. + starting_block (str): The starting block for event syncing. + """ + data = { + "label": contract_label, + "version": contract_version, + "startingBlock": starting_block, + } + try: + mb_request( + mb_url, + mb_api_key, + f"/chains/ethereum/addresses/{address_label}/contracts", + method="POST", + data=data, + ) + except MultiBaasAPIError as e: + print(f"Failed to link contract to address: {e}") + raise + + +def upload_and_link_contract( + mb_url: str, + mb_api_key: str, + artifact_dir: str, + contract_name: str, + contract_address: str, + options: Dict[str, str], +) -> None: + """ + Uploads a contract to MultiBaas and links it to an address. + + Args: + mb_url (str): The MultiBaas URL. + mb_api_key (str): The API key for authentication. + artifact_dir (str): The directory where artifacts are stored. + contract_name (str): The name of the contract. + contract_address (str): The Ethereum address of the contract. + options (dict): Additional options for contract and address labeling. + """ + contract_label = options.get("contractLabel", contract_name.lower()) + contract_version = options.get("contractVersion", None) + address_label = options.get("addressLabel", contract_label) + starting_block = options.get("startingBlock", "-100") + + # Create the contract + mb_contract = create_contract( + mb_url, + mb_api_key, + artifact_dir, + contract_name, + contract_label, + contract_version, + ) + if not mb_contract: + print("Error: Failed to create contract. Stopping execution.") + return + + # Create the address + mb_address = create_address( + mb_url, mb_api_key, contract_address, contract_label, address_label + ) + if not mb_address: + print("Error: Failed to create address. Stopping execution.") + return + + # Link the contract to the address + try: + link_contract_to_address( + mb_url, + mb_api_key, + contract_label, + mb_contract.get("version"), + address_label, + starting_block, + ) + print("Contract linked successfully.") + except MultiBaasAPIError as e: + print(f"Error: Failed to link contract to address: {e}") + + +def main() -> None: + """ + Main function to handle commands and orchestrate the MultiBaas interactions. + """ + try: + # Step 1: Retrieve MultiBaas credentials from environment variables + mb_url, mb_api_key = get_multibaas_credentials() + + # Step 2: Validate the API key + if not validate_api_key(mb_url, mb_api_key): + return + + # Step 3: Retrieve the artifact directory from forge config + artifact_dir = get_artifact_dir() + if not artifact_dir: + print("Error: Unable to retrieve the artifact directory.") + return + + # Step 4: Parse command and arguments + if len(sys.argv) < 4: + print( + "Usage: python3 main.py COMMAND CONTRACT_NAME CONTRACT_ADDRESS OPTIONS" + ) + return + + command = sys.argv[1] + contract_name = sys.argv[2] + contract_address = sys.argv[3] + options_str = sys.argv[4] + + # Parse options as a dict directly + options = filter_empty_values(json.loads(options_str)) + + # Step 5: Handle the command + if command == "linkContract": + upload_and_link_contract( + mb_url, + mb_api_key, + artifact_dir, + contract_name, + contract_address, + options, + ) + else: + print(f"Unknown command: {command}") + + except MultiBaasAPIError as e: + print(f"MultiBaas API error occurred: {e}") + except Exception as e: + print(f"An unexpected error occurred: {str(e)}") + traceback.print_exc() + + +if __name__ == "__main__": + main() diff --git a/src/MultiBaas.sol b/src/MultiBaas.sol new file mode 100644 index 0000000..bfab706 --- /dev/null +++ b/src/MultiBaas.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; + +library MultiBaas { + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + // Encode options for uploading and linking a contract MultiBaas + // @param contractLabel: Label for the contract. Defaults to contract name if empty + // @param addressLabel: Label for the contract address. Defaults to contract name if empty + // @param contractVersion: Version label for the contract. Defaults to 1.0 and auto increments if empty + // @param startingBlock: From which block to start syncing events. Defaults to -100. If a negative value, it represents blocks before the current block. If a positive value, it represents the absolute block number. If 0, it represents the latest block. + function withOptions( + string memory contractLabel, + string memory addressLabel, + string memory contractVersion, + string memory startingBlock + ) internal pure returns (bytes memory encodedOptions) { + encodedOptions = abi.encodePacked( + '{"contractLabel":"', + contractLabel, + '","addressLabel":"', + addressLabel, + '","contractVersion":"', + contractVersion, + '","startingBlock":"', + startingBlock, + '"}' + ); + } + + // Link a contract to MultiBaas with options + function linkContractWithOptions(string memory contractName, address contractAddress, bytes memory encodedOptions) + internal + { + // Construct the command to call the Python script + string[] memory inputs = new string[](6); + string memory scriptPath = string.concat(vm.projectRoot(), "/lib/forge-multibaas/main.py"); + inputs[0] = "python3"; + inputs[1] = scriptPath; + inputs[2] = "linkContract"; + inputs[3] = contractName; + inputs[4] = toString(contractAddress); + inputs[5] = string(encodedOptions); + + // Call the Python script using ffi + bytes memory res = vm.ffi(inputs); + + // Log the response from MultiBaas + console.log("Link Contract: %s", string(res)); + } + + // Link a contract to MultiBaas with default options + function linkContract(string memory contractName, address contractAddress) internal { + bytes memory defaultEncodedOptions = withOptions("", "", "", ""); + return linkContractWithOptions(contractName, contractAddress, defaultEncodedOptions); + } + + // Utility function to convert an address to a string for Python + function toString(address _address) internal pure returns (string memory) { + bytes32 value = bytes32(uint256(uint160(_address))); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(42); + str[0] = "0"; + str[1] = "x"; + for (uint256 i = 0; i < 20; i++) { + str[2 + i * 2] = alphabet[uint8(value[i + 12] >> 4)]; + str[3 + i * 2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + return string(str); + } +}