├── conftest.py ├── examples ├── agents │ ├── seven.png │ ├── img │ │ └── account.png │ ├── action_agent.py │ ├── read_contracts.py │ ├── using_polygon.ipynb │ ├── using_arbitrum.ipynb │ └── mnist_agent_tutorial.ipynb ├── verifiable_mnist │ ├── imgs │ │ ├── zero.png │ │ ├── action.png │ │ ├── action_runs.png │ │ ├── deployments.png │ │ ├── giza_stack.png │ │ └── mnist_dataset_illustration.png │ └── deployments │ │ └── pytorch_mnist_deployment.py ├── imagenet │ ├── README.md │ └── inference.py ├── ezkl │ └── linear_regression │ │ ├── train_linear_regression.py │ │ ├── predict_action.py │ │ └── README.md ├── uni_v3_lp │ ├── addresses.py │ ├── action_agent.py │ ├── lp_tools.py │ └── mint_position.py └── traditional-ml │ └── decision_tree.ipynb ├── ape-config.yaml ├── giza └── agents │ ├── integrations │ ├── __init__.py │ └── uniswap │ │ ├── constants.py │ │ ├── utils.py │ │ ├── pool_factory.py │ │ ├── assets │ │ ├── quoter.json │ │ ├── pool_factory.json │ │ └── erc20.json │ │ ├── quoter.py │ │ ├── pool.py │ │ ├── router.py │ │ ├── nft_manager.py │ │ └── uniswap.py │ ├── exceptions.py │ ├── logger.py │ ├── integration.py │ ├── task.py │ ├── deployments.py │ ├── __init__.py │ ├── logging.yaml │ ├── utils.py │ └── action.py ├── .github └── workflows │ ├── onpush.yml │ └── onrelease.yml ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml ├── tests ├── test_utils.py ├── test_model.py └── test_agent.py ├── .gitignore └── README.md /conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/agents/seven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/agents/seven.png -------------------------------------------------------------------------------- /examples/agents/img/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/agents/img/account.png -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | mnemonic: test test test test test test test test test test test junk 3 | number_of_accounts: 5 4 | -------------------------------------------------------------------------------- /examples/verifiable_mnist/imgs/zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/verifiable_mnist/imgs/zero.png -------------------------------------------------------------------------------- /giza/agents/integrations/__init__.py: -------------------------------------------------------------------------------- 1 | from giza.agents.integrations.uniswap.uniswap import Uniswap 2 | 3 | __all__ = ["Uniswap"] 4 | -------------------------------------------------------------------------------- /examples/verifiable_mnist/imgs/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/verifiable_mnist/imgs/action.png -------------------------------------------------------------------------------- /examples/verifiable_mnist/imgs/action_runs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/verifiable_mnist/imgs/action_runs.png -------------------------------------------------------------------------------- /examples/verifiable_mnist/imgs/deployments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/verifiable_mnist/imgs/deployments.png -------------------------------------------------------------------------------- /examples/verifiable_mnist/imgs/giza_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/verifiable_mnist/imgs/giza_stack.png -------------------------------------------------------------------------------- /giza/agents/exceptions.py: -------------------------------------------------------------------------------- 1 | class DuplicateIntegrationError(Exception): 2 | """Exception raised when there is a duplicate in integration names.""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /examples/verifiable_mnist/imgs/mnist_dataset_illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShumCr/giza-agents/HEAD/examples/verifiable_mnist/imgs/mnist_dataset_illustration.png -------------------------------------------------------------------------------- /giza/agents/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class WordReplacerFilter(logging.Filter): 5 | def filter(self, record: logging.LogRecord) -> bool: 6 | record.msg = record.msg.replace("flow", "action") 7 | record.msg = record.msg.replace("Flow", "Action") 8 | return True 9 | -------------------------------------------------------------------------------- /giza/agents/integration.py: -------------------------------------------------------------------------------- 1 | from ape.api import AccountAPI 2 | 3 | from giza.agents.integrations import Uniswap 4 | 5 | 6 | class IntegrationFactory: 7 | @staticmethod 8 | def from_name(name: str, sender: AccountAPI) -> Uniswap: 9 | match name: 10 | case "UniswapV3": 11 | return Uniswap(sender, version=3) 12 | case _: 13 | raise ValueError(f"Integration {name} not found") 14 | -------------------------------------------------------------------------------- /giza/agents/task.py: -------------------------------------------------------------------------------- 1 | from functools import partial, wraps 2 | from typing import Any 3 | 4 | from prefect import task as prefect_task 5 | 6 | 7 | def task(func: Any, *task_init_args: Any, **task_init_kwargs: Any) -> Any: 8 | if func is None: 9 | return partial(task, *task_init_args, **task_init_kwargs) 10 | 11 | @wraps(func) 12 | def safe_func(*args: Any, **kwargs: Any) -> Any: 13 | try: 14 | res = func(*args, **kwargs) 15 | return res 16 | except Exception as e: 17 | raise e 18 | 19 | safe_func.__name__ = func.__name__ 20 | return prefect_task(safe_func, *task_init_args, **task_init_kwargs) 21 | -------------------------------------------------------------------------------- /giza/agents/deployments.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os # noqa: E402 3 | from typing import Any 4 | 5 | from giza.agents.utils import get_workspace_uri # noqa: E402 6 | 7 | os.environ["PREFECT_API_URL"] = f"{get_workspace_uri()}/api" 8 | os.environ["PREFECT_UI_URL"] = get_workspace_uri() 9 | 10 | from prefect.deployments import run_deployment # noqa: E402 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def run_action_deployment(name: str, parameters: dict = None) -> Any: 16 | deployment_run = run_deployment(name=name, parameters=parameters) 17 | logger.info( 18 | f"Deployment run name: {deployment_run.name} exited with state: {deployment_run.state_name}" 19 | ) 20 | return deployment_run 21 | -------------------------------------------------------------------------------- /examples/imagenet/README.md: -------------------------------------------------------------------------------- 1 | # ImageNet Example 2 | 3 | This example demonstrates how to use the Giza SDK to perform image classification using a pre-trained ResNet-50 model from the ONNX model zoo. 4 | 5 | ## Steps 6 | 7 | 1. Download the model, image, and labels using the `download_model()`, `download_image()`, and `download_labels()` tasks respectively. 8 | 2. Read the labels using the `read_labels()` task. 9 | 3. Load the model using `GizaModel(model_path=model_path)`. 10 | 4. Preprocess the image using the `preprocess(img)` function. 11 | 5. Predict the class of the image using the `predict(model.session, labels, img)` function. 12 | 13 | ## Running the example 14 | 15 | To run the example, execute the `execution()` action. This action will perform all the steps mentioned above in sequence. 16 | 17 | ``` 18 | python example.py 19 | ``` 20 | 21 | Then, you can execute the Action using the Prefect UI: 22 | 23 | ``` 24 | prefect server start 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/onpush.yml: -------------------------------------------------------------------------------- 1 | name: Giza CI 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | push: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | python -m pip install poetry 26 | poetry config virtualenvs.create false 27 | poetry install --all-extras 28 | - name: Lint with ruff 29 | run: | 30 | poetry run ruff giza 31 | - name: Pre-commit check 32 | run: | 33 | poetry run pre-commit run --all-files 34 | - name: Testing 35 | run: | 36 | poetry run pytest --cov=giza.agents --cov-report term-missing 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Auditless Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/onrelease.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | max-parallel: 1 13 | matrix: 14 | python-version: ["3.11"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install poetry 29 | poetry config virtualenvs.create false 30 | poetry install 31 | - name: Lint with ruff 32 | run: | 33 | poetry run ruff giza 34 | - name: Build dist 35 | run: poetry build 36 | - name: Publish a Python distribution to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.GIZA_ACTIONS_PYPI_TOKEN }} 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | exclude: docs/.*|examples/.* 8 | - id: trailing-whitespace 9 | exclude: docs/.*|examples/.* 10 | - id: check-added-large-files 11 | exclude: examples/.* 12 | - id: check-merge-conflict 13 | exclude: examples/.* 14 | - id: no-commit-to-branch 15 | args: [-b main] 16 | exclude: examples/.* 17 | - repo: https://github.com/asottile/pyupgrade 18 | rev: v3.4.0 19 | hooks: 20 | - id: pyupgrade 21 | files: "py$" 22 | - repo: local 23 | hooks: 24 | - id: isort 25 | name: isort 26 | entry: isort 27 | language: system 28 | files: "py$" 29 | exclude: examples/.* 30 | args: ["--profile", "black"] 31 | - id: black 32 | name: black 33 | entry: black 34 | language: system 35 | files: "py$" 36 | exclude: examples/.* 37 | - id: ruff 38 | name: ruff 39 | entry: ruff 40 | language: system 41 | files: "py$" 42 | exclude: examples/.* 43 | -------------------------------------------------------------------------------- /giza/agents/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | 4 | from giza.agents.agent import AgentResult, Contract, ContractHandler, GizaAgent 5 | 6 | # The absolute path to this module 7 | __module_path__ = pathlib.Path(__file__).parent 8 | # The absolute path to the root of the repository, only valid for use during development 9 | __development_base_path__ = __module_path__.parents[1] 10 | 11 | 12 | def set_logging_level(level: int) -> None: 13 | """ 14 | Set the logging level for the Giza package. 15 | 16 | Args: 17 | level (int): The logging level to set. 18 | """ 19 | numeric_level = getattr(logging, level.upper(), None) 20 | if not isinstance(numeric_level, int): 21 | raise ValueError(f"Invalid log level: {level}") 22 | 23 | # Set the log level for the root logger of the package 24 | logger = logging.getLogger("giza.agents") 25 | logger.setLevel(numeric_level) 26 | 27 | # Configure the logging handler 28 | handler = logging.StreamHandler() 29 | handler.setLevel(numeric_level) 30 | formatter = logging.Formatter( 31 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 32 | ) 33 | handler.setFormatter(formatter) 34 | 35 | # Remove any existing handlers 36 | if logger.hasHandlers(): 37 | logger.handlers.clear() 38 | 39 | logger.addHandler(handler) 40 | 41 | 42 | __all__ = ["GizaAgent", "AgentResult", "ContractHandler", "Contract"] 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "giza-agents" 3 | version = "0.5.0" 4 | 5 | description = "A Python SDK for Giza platform" 6 | authors = [ 7 | "Francisco Algaba ", 8 | "Raphael Doukhan ", 9 | "Gonzalo Mellizo-Soto "] 10 | readme = "README.md" 11 | license = "MIT" 12 | packages = [{include = "giza"}] 13 | include = ["giza/agents/integrations/uniswap/assets"] 14 | 15 | [tool.poetry.dependencies] 16 | python = ">=3.11,<4.0" 17 | numpy = "^1.26.2" 18 | prefect = "2.14.6" 19 | onnx = "^1.15.0" 20 | httpx = "^0.25.1" 21 | onnxruntime = "^1.16.3" 22 | prefect-gcp = "^0.5.4" 23 | pyyaml = "^6.0.1" 24 | prefect-docker = "^0.4.1" 25 | distlib = "^0.3.8" 26 | giza-cli = ">=0.17.0" 27 | giza-osiris = ">=0.2.8,<1.0.0" 28 | loguru = "^0.7.2" 29 | eth-ape = "^0.7.10" 30 | ape-etherscan = "^0.7.2" 31 | diskcache = "^5.6.3" 32 | 33 | [tool.poetry.dev-dependencies] 34 | pytest = "^6.2.5" 35 | 36 | [tool.poetry.group.dev.dependencies] 37 | pillow = "^10.1.0" 38 | opencv-python = "^4.8.1.78" 39 | black = "^23.11.0" 40 | isort = {extras = ["pyproject"], version = "^5.12.0"} 41 | pytest-cov = "^4.1.0" 42 | mypy = "^1.7.1" 43 | ruff = "^0.1.6" 44 | lazydocs = "^0.4.8" 45 | pre-commit = "^3.5.0" 46 | torch = "^2.1.2" 47 | torchvision = "^0.16.2" 48 | matplotlib = "^3.8.2" 49 | ipykernel = "^6.29.4" 50 | 51 | [build-system] 52 | requires = ["poetry-core>=1.0.0"] 53 | build-backend = "poetry.core.masonry.api" 54 | 55 | [tool.isort] 56 | profile = "black" 57 | -------------------------------------------------------------------------------- /examples/ezkl/linear_regression/train_linear_regression.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import numpy as np 4 | import torch 5 | from hummingbird.ml import convert 6 | from sklearn.linear_model import LinearRegression 7 | 8 | 9 | def train(): 10 | X = np.array([[1, 1], [1, 2], [2, 2], [2, 3]]) 11 | 12 | y = np.dot(X, np.array([1, 2])) + 3 13 | reg = LinearRegression().fit(X, y) 14 | 15 | return reg 16 | 17 | 18 | def convert_to_torch(linear_regression, sample): 19 | return convert(linear_regression, "torch", sample).model 20 | 21 | 22 | def convert_to_onnx(model, sample): 23 | # Input to the model 24 | shape = sample.shape 25 | x = torch.rand(1, *shape, requires_grad=True) 26 | 27 | # Export the model 28 | torch.onnx.export( 29 | model, 30 | x, 31 | "network.onnx", 32 | export_params=True, 33 | opset_version=10, 34 | do_constant_folding=True, 35 | input_names=["input"], 36 | output_names=["output"], 37 | dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, 38 | ) 39 | 40 | 41 | def create_input_file(sample: np.ndarray): 42 | with open("input.json", "w") as f: 43 | f.write( 44 | json.dumps( 45 | { 46 | "input_shapes": [sample.shape], 47 | "input_data": [sample.tolist()], 48 | } 49 | ) 50 | ) 51 | 52 | 53 | def model_to_onnx(): 54 | lr = train() 55 | sample = np.array([7, 2]) 56 | model = convert_to_torch(lr, sample) 57 | convert_to_onnx(model, sample) 58 | create_input_file(sample) 59 | 60 | 61 | model_to_onnx() 62 | -------------------------------------------------------------------------------- /examples/agents/action_agent.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pprint 3 | import numpy as np 4 | from PIL import Image 5 | from giza.agents import GizaAgent 6 | 7 | 8 | # Process the image 9 | def process_image(img): 10 | img = np.resize(img, (28, 28)) 11 | img = img.reshape(1, 1, 28, 28) 12 | img = img / 255.0 13 | print(img.shape) 14 | # For now, we will just use a small tensor as input to a single-layer softmax. We will change this when the PoC works 15 | tensor = np.random.rand(1, 3) 16 | return tensor 17 | 18 | 19 | # Get the image 20 | def get_image(path): 21 | with Image.open(path) as img: 22 | img = img.convert("L") 23 | img = np.array(img) 24 | return img 25 | 26 | # Create the execution function 27 | def transmission(): 28 | logger = logging.getLogger(__name__) 29 | img_path = 'seven.png' 30 | img = get_image(img_path) 31 | img = process_image(img) 32 | id = ... 33 | version = ... 34 | account = ... 35 | contract_address = "0x17807a00bE76716B91d5ba1232dd1647c4414912" 36 | 37 | agent = GizaAgent( 38 | contracts={"mnist": contract_address}, 39 | id=id, 40 | chain="ethereum:sepolia:geth", 41 | version_id=version, 42 | account=account, 43 | ) 44 | 45 | result = agent.predict(input_feed={"image": img}, verifiable=True) 46 | 47 | logger.info(f"Result: {result}") 48 | with agent.execute() as contracts: 49 | logger.info("Executing contract") 50 | contract_result = contracts.mnist.mint(int(result.value[0].argmax())) 51 | logger.info("Contract executed") 52 | 53 | logger.info(f"Contract result: {contract_result}") 54 | pprint.pprint(contract_result.__dict__) 55 | logger.info("Finished") 56 | 57 | 58 | transmission() 59 | -------------------------------------------------------------------------------- /examples/uni_v3_lp/addresses.py: -------------------------------------------------------------------------------- 1 | # source: https://docs.uniswap.org/contracts/v3/reference/deployments 2 | ADDRESSES = { 3 | "WETH": { 4 | 1: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", 5 | 11155111: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", 6 | 5: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", 7 | 42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", 8 | }, 9 | "UNI": { 10 | 1: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 11 | 11155111: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 12 | 5: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 13 | 42161: "0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0", 14 | }, 15 | "USDC": { 16 | 1: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 17 | 11155111: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", 18 | 5: "", 19 | 42161: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", 20 | }, 21 | "NonfungiblePositionManager": { 22 | 1: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", 23 | 11155111: "0x1238536071E1c677A632429e3655c799b22cDA52", 24 | 5: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", 25 | 42161: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", 26 | }, 27 | "PoolFactory": { 28 | 1: "0x1F98431c8aD98523631AE4a59f267346ea31F984", 29 | 11155111: "0x0227628f3F023bb0B980b67D528571c95c6DaC1c", 30 | 5: "0x1F98431c8aD98523631AE4a59f267346ea31F984", 31 | 42161: "0x1F98431c8aD98523631AE4a59f267346ea31F984", 32 | }, 33 | "Router": { 34 | 1: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 35 | 11155111: "0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E", 36 | 5: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 37 | 42161: "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/constants.py: -------------------------------------------------------------------------------- 1 | MIN_TICK = -887272 2 | MAX_TICK = -MIN_TICK 3 | TICKS_Q = 1.0001 4 | Q96 = 2**96 5 | MAX_UINT_128 = 2 ** (128) - 1 6 | _tick_spacing = {100: 1, 500: 10, 3_000: 60, 10_000: 200} 7 | 8 | 9 | ADDRESSES = { 10 | # mainnet 11 | 1: { 12 | 3: { 13 | "PoolFactory": "0x1F98431c8aD98523631AE4a59f267346ea31F984", 14 | "NonfungiblePositionManager": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", 15 | "Router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 16 | "Quoter": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", 17 | }, 18 | }, 19 | # sepolia 20 | 11155111: { 21 | 3: { 22 | "PoolFactory": "0x0227628f3F023bb0B980b67D528571c95c6DaC1c", 23 | "NonfungiblePositionManager": "0x1238536071E1c677A632429e3655c799b22cDA52", 24 | "Router": "0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E", 25 | "Quoter": "0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3", 26 | }, 27 | }, 28 | # goerli 29 | 5: { 30 | 3: { 31 | "PoolFactory": "0x1F98431c8aD98523631AE4a59f267346ea31F984", 32 | "NonfungiblePositionManager": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", 33 | "Router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 34 | "Quoter": "", 35 | }, 36 | }, 37 | # arbitrum 38 | 42161: { 39 | 3: { 40 | "PoolFactory": "0x1F98431c8aD98523631AE4a59f267346ea31F984", 41 | "NonfungiblePositionManager": "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", 42 | "Router": "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", 43 | "Quoter": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", 44 | }, 45 | }, 46 | # TODO: fill remaining chains: https://github.com/Uniswap/v3-periphery/blob/main/deploys.md 47 | } 48 | -------------------------------------------------------------------------------- /examples/agents/read_contracts.py: -------------------------------------------------------------------------------- 1 | """ 2 | For this to work, we need to have the PASSPHRASE as an environment variable 3 | This variable should have the form of "{ACCOUNT_ALIAS}_PASSPHRASE={PASSPHRASE}" 4 | 5 | For example, if the ACCOUNT_ALIAS is "sepolia", then the environment variable should be "SEPOLIA_PASSPHRASE" 6 | 7 | This is because the GizaAgent will look for the environment variable "{ACCOUNT_ALIAS}_PASSPHRASE" to allow auto signing 8 | 9 | In this example, we instantiate the agent with the model id, version id, and the addresses of the contracts we want to interact with. 10 | The contracts are stored in a dictionary where the key is the contract name and the value is the contract address. 11 | 12 | We also specify the chain we want to interact with and the account alias we want to use to sign transactions, which, in this case is "sepolia". 13 | 14 | To interact with the contracts, we use the execute method of the agent in a `with` block, so that the contracts will be available using the dot notation. 15 | For example, if our agent has two contracts, "mnist" and "token", then we can access them using `contracts.mnist` and `contracts.token`, respectively. 16 | 17 | In this example, we call the `name` method of the contracts, which is a method that returns the name of the contract, and then we print the result. But we could execute any function of a contract in the same way. 18 | """ 19 | 20 | from giza.agents import GizaAgent, set_logging_level 21 | 22 | set_logging_level("DEBUG") 23 | 24 | MODEL_ID = ... 25 | VERSION_ID = ... 26 | ACCOUNT_ALIAS = ... 27 | 28 | agent = GizaAgent( 29 | id=MODEL_ID, 30 | version_id=VERSION_ID, 31 | contracts={ 32 | "mnist": "0x17807a00bE76716B91d5ba1232dd1647c4414912", 33 | "token": "0xeF7cCAE97ea69F5CdC89e496b0eDa2687C95D93B", 34 | }, 35 | chain="ethereum:sepolia:geth", 36 | account=ACCOUNT_ALIAS, 37 | ) 38 | 39 | with agent.execute() as contracts: 40 | result = contracts.mnist.name() 41 | print(result) 42 | result = contracts.token.name() 43 | print(result) 44 | -------------------------------------------------------------------------------- /examples/uni_v3_lp/action_agent.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pprint 3 | 4 | import numpy as np 5 | 6 | from giza.agents import GizaAgent 7 | 8 | 9 | def process_data(realized_vol, dec_price_change): 10 | pct_change_sq = (100 * dec_price_change) ** 2 11 | X = np.array([[realized_vol, pct_change_sq]]) 12 | return X 13 | 14 | 15 | def get_data(): 16 | realized_vol = 4.20 17 | dec_price_change = 0.1 18 | return realized_vol, dec_price_change 19 | 20 | 21 | def transmission(): 22 | logger = logging.getLogger(__name__) 23 | id = ... 24 | version = ... 25 | account = ... 26 | realized_vol, dec_price_change = get_data() 27 | input_data = process_data(realized_vol, dec_price_change) 28 | 29 | agent = GizaAgent( 30 | integrations=["UniswapV3"], 31 | id=id, 32 | chain="ethereum:sepolia:geth", 33 | version_id=version, 34 | account=account, 35 | ) 36 | 37 | result = agent.predict( 38 | input_feed={"val": input_data}, verifiable=True, dry_run=True 39 | ) 40 | 41 | logger.info(f"Result: {result}") 42 | with agent.execute() as contracts: 43 | UNI_address = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" 44 | WETH_address = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14" 45 | uni = contracts.UniswapV3 46 | volatility_prediction = result.value[0] 47 | pool = uni.get_pool(UNI_address, WETH_address, fee=500) 48 | curr_price = pool.get_pool_price() 49 | lower_price = curr_price * (1 - volatility_prediction) 50 | upper_price = curr_price * (1 + volatility_prediction) 51 | amount0 = 100 52 | amount1 = 100 53 | agent_result = uni.mint_position( 54 | pool, lower_price, upper_price, amount0, amount1 55 | ) 56 | logger.info( 57 | f"Current price: {curr_price}, new bounds: {lower_price}, {upper_price}" 58 | ) 59 | logger.info(f"Minted position: {agent_result}") 60 | 61 | logger.info(f"Contract result: {agent_result}") 62 | logger.info("Finished") 63 | 64 | 65 | transmission() 66 | -------------------------------------------------------------------------------- /examples/ezkl/linear_regression/predict_action.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import requests 4 | from giza.cli import API_HOST 5 | from giza.cli.client import DeploymentsClient 6 | 7 | from giza.agents.model import GizaModel 8 | 9 | MODEL_ID = ... # The ID of the model 10 | VERSION = ... # The version of the model 11 | 12 | 13 | def get_deployment_id(): 14 | """ 15 | Retrieve the deployment ID for the model and version. 16 | 17 | Returns: 18 | int: The ID of the deployment. 19 | """ 20 | client = DeploymentsClient(API_HOST) 21 | return client.list(MODEL_ID, VERSION).__root__[0].id 22 | 23 | 24 | def predict(): 25 | """ 26 | Predict using the model and version for a linear regression model. 27 | 28 | Returns: 29 | tuple: The result of the prediction and the request ID. 30 | """ 31 | model = GizaModel(id=MODEL_ID, version=VERSION) 32 | 33 | result, request_id = model.predict(input_feed=[7, 2], verifiable=True, job_size="S") 34 | 35 | print(f"Result: {result}, request_id: {request_id}") 36 | return result, request_id 37 | 38 | 39 | def wait_for_proof(request_id): 40 | """ 41 | Wait for the proof associated with the request ID. For 240 seconds, it will attempt to retrieve the proof every 5 seconds. 42 | 43 | Args: 44 | request_id (str): The ID of the request. 45 | """ 46 | print(f"Waiting for proof for request_id: {request_id}") 47 | client = DeploymentsClient(API_HOST) 48 | 49 | timeout = time.time() + 240 50 | while True: 51 | now = time.time() 52 | if now > timeout: 53 | print("Proof retrieval timed out") 54 | break 55 | try: 56 | proof = client.get_proof(MODEL_ID, VERSION, get_deployment_id(), request_id) 57 | print(f"Proof: {proof.json(exclude_unset=True)}") 58 | break 59 | except requests.exceptions.HTTPError: 60 | print("Proof retrieval failing, sleeping for 5 seconds") 61 | time.sleep(5) 62 | 63 | 64 | def inference(): 65 | result, request_id = predict() 66 | wait_for_proof(request_id) 67 | 68 | 69 | inference() 70 | -------------------------------------------------------------------------------- /examples/uni_v3_lp/lp_tools.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | MIN_TICK = -887272 4 | MAX_TICK = -MIN_TICK 5 | TICKS_Q = 1.0001 6 | Q96 = 2**96 7 | MAX_UINT_128 = 2 ** (128) - 1 8 | _tick_spacing = {100: 1, 500: 10, 3_000: 60, 10_000: 200} 9 | 10 | # https://ethereum.stackexchange.com/questions/150280/calculate-amount-of-eth-and-usdc-after-minting-a-position-in-uniswap-v3 11 | 12 | 13 | def price_to_tick(price): 14 | sqrtPriceX96 = int(price * 2**96) 15 | tick = math.floor(math.log((sqrtPriceX96 / Q96) ** 2) / math.log(TICKS_Q)) 16 | return tick 17 | 18 | 19 | def tick_to_price(tick, decimals0, decimals1, invert=False): 20 | if invert: 21 | return 1 / (TICKS_Q**tick / 10 ** (decimals1 - decimals0)) 22 | else: 23 | return TICKS_Q**tick / 10 ** (decimals1 - decimals0) 24 | 25 | 26 | def get_min_tick(fee: int): 27 | min_tick_spacing: int = _tick_spacing[fee] 28 | return -(MIN_TICK // -min_tick_spacing) * min_tick_spacing 29 | 30 | 31 | def get_max_tick(fee: int): 32 | max_tick_spacing: int = _tick_spacing[fee] 33 | return (MAX_TICK // max_tick_spacing) * max_tick_spacing 34 | 35 | 36 | def default_tick_range(fee: int): 37 | min_tick = get_min_tick(fee) 38 | max_tick = get_max_tick(fee) 39 | return min_tick, max_tick 40 | 41 | 42 | def nearest_tick(tick: int, fee: int): 43 | min_tick, max_tick = default_tick_range(fee) 44 | assert ( 45 | min_tick <= tick <= max_tick 46 | ), f"Provided tick is out of bounds: {(min_tick, max_tick)}" 47 | tick_spacing = _tick_spacing[fee] 48 | rounded_tick_spacing = round(tick / tick_spacing) * tick_spacing 49 | if rounded_tick_spacing < min_tick: 50 | return rounded_tick_spacing + tick_spacing 51 | elif rounded_tick_spacing > max_tick: 52 | return rounded_tick_spacing - tick_spacing 53 | else: 54 | return rounded_tick_spacing 55 | 56 | 57 | def get_tick_range(curr_tick, pct_dev, tokenA_decimals, tokenB_decimals, fee): 58 | curr_price = tick_to_price(curr_tick, tokenA_decimals, tokenB_decimals) 59 | upper_price = curr_price * (1 + pct_dev) 60 | lower_price = curr_price * (1 - pct_dev) 61 | lower_tick = price_to_tick(lower_price) 62 | upper_tick = price_to_tick(upper_price) 63 | lower_tick = nearest_tick(lower_tick, fee) 64 | upper_tick = nearest_tick(upper_tick, fee) 65 | return lower_tick, upper_tick 66 | -------------------------------------------------------------------------------- /examples/imagenet/inference.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import numpy as np 3 | import cv2 4 | from PIL import Image 5 | 6 | from giza.agents.model import GizaModel 7 | 8 | 9 | def download_image(): 10 | image_url = 'https://s3.amazonaws.com/model-server/inputs/kitten.jpg' 11 | image_data = requests.get(image_url).content 12 | with open('kitten.jpg', 'wb') as handler: 13 | handler.write(image_data) 14 | 15 | def download_labels(): 16 | labels_url = 'https://s3.amazonaws.com/onnx-model-zoo/synset.txt' 17 | labels_data = requests.get(labels_url).content 18 | with open('synset.txt', 'wb') as handler: 19 | handler.write(labels_data) 20 | 21 | def download_model(): 22 | model_url = 'https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v1-12.onnx' 23 | model_data = requests.get(model_url).content 24 | with open('resnet50-v1-12.onnx', 'wb') as handler: 25 | handler.write(model_data) 26 | 27 | def read_labels(): 28 | with open('synset.txt') as f: 29 | labels = [l.rstrip() for l in f] 30 | return labels 31 | 32 | def get_image(path): 33 | with Image.open(path) as img: 34 | img = np.array(img.convert('RGB')) 35 | return img 36 | 37 | def preprocess(img): 38 | img = img / 255. 39 | img = cv2.resize(img, (256, 256)) 40 | h, w = img.shape[0], img.shape[1] 41 | y0 = (h - 224) // 2 42 | x0 = (w - 224) // 2 43 | img = img[y0 : y0+224, x0 : x0+224, :] 44 | img = (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] 45 | img = np.transpose(img, axes=[2, 0, 1]) 46 | img = img.astype(np.float32) 47 | img = np.expand_dims(img, axis=0) 48 | return img 49 | 50 | def predict(model, labels, img, verifiable: bool = False): 51 | ort_inputs = {model.session.get_inputs()[0].name: img} 52 | preds = model.predict(ort_inputs, verifiable=verifiable) 53 | preds = np.squeeze(preds) 54 | a = np.argsort(preds)[::-1] 55 | print('class=%s ; probability=%f' %(labels[a[0]],preds[a[0]])) 56 | 57 | def execution(): 58 | model_path = 'resnet50-v1-12.onnx' 59 | img_path = 'kitten.jpg' 60 | verifiable = False 61 | 62 | download_model() 63 | download_image() 64 | download_labels() 65 | labels = read_labels() 66 | model = GizaModel(model_path=model_path) 67 | img = get_image(img_path) 68 | img = preprocess(img) 69 | predict(model, labels, img, verifiable=verifiable) 70 | 71 | execution() 72 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from ape import Contract 4 | 5 | from giza.agents.integrations.uniswap.constants import ( 6 | MAX_TICK, 7 | MIN_TICK, 8 | Q96, 9 | TICKS_Q, 10 | _tick_spacing, 11 | ) 12 | 13 | 14 | def load_contract(address): 15 | return Contract(address) 16 | 17 | 18 | def price_to_tick(price, decimals0, decimals1): 19 | tick = math.floor(math.log(price * 10 ** (decimals1 - decimals0), TICKS_Q)) 20 | return tick 21 | 22 | 23 | def price_to_sqrtp(p): 24 | return int(math.sqrt(p) * Q96) 25 | 26 | 27 | def tick_to_price(tick, decimals0, decimals1, invert=False): 28 | if invert: 29 | return 1 / (TICKS_Q**tick / 10 ** (decimals1 - decimals0)) 30 | else: 31 | return TICKS_Q**tick / 10 ** (decimals1 - decimals0) 32 | 33 | 34 | def get_min_tick(fee): 35 | min_tick_spacing = _tick_spacing[fee] 36 | return -(MIN_TICK // -min_tick_spacing) * min_tick_spacing 37 | 38 | 39 | def get_max_tick(fee): 40 | max_tick_spacing = _tick_spacing[fee] 41 | return (MAX_TICK // max_tick_spacing) * max_tick_spacing 42 | 43 | 44 | def default_tick_range(fee): 45 | min_tick = get_min_tick(fee) 46 | max_tick = get_max_tick(fee) 47 | return min_tick, max_tick 48 | 49 | 50 | # https://uniswapv3book.com/milestone_1/calculating-liquidity.html 51 | def calc_amount0(liq, pa, pb): 52 | if pa > pb: 53 | pa, pb = pb, pa 54 | return int(liq * Q96 * (pb - pa) / pa / pb) 55 | 56 | 57 | def calc_amount1(liq, pa, pb): 58 | if pa > pb: 59 | pa, pb = pb, pa 60 | return int(liq * (pb - pa) / Q96) 61 | 62 | 63 | def liquidity0(amount, pa, pb): 64 | if pa > pb: 65 | pa, pb = pb, pa 66 | return (amount * (pa * pb) / Q96) / (pb - pa) 67 | 68 | 69 | def liquidity1(amount, pa, pb): 70 | if pa > pb: 71 | pa, pb = pb, pa 72 | return amount * Q96 / (pb - pa) 73 | 74 | 75 | def nearest_tick(tick, fee): 76 | min_tick, max_tick = default_tick_range(fee) 77 | assert ( 78 | min_tick <= tick <= max_tick 79 | ), f"Provided tick is out of bounds: {(min_tick, max_tick)}" 80 | tick_spacing = _tick_spacing[fee] 81 | rounded_tick_spacing = round(tick / tick_spacing) * tick_spacing 82 | if rounded_tick_spacing < min_tick: 83 | return rounded_tick_spacing + tick_spacing 84 | elif rounded_tick_spacing > max_tick: 85 | return rounded_tick_spacing - tick_spacing 86 | else: 87 | return rounded_tick_spacing 88 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | import requests 6 | from giza.cli.schemas.endpoints import Endpoint, EndpointsList 7 | from giza.cli.schemas.workspaces import Workspace 8 | 9 | from giza.agents.utils import get_endpoint_uri, get_workspace_uri, read_json 10 | 11 | 12 | @patch("giza.cli.client.EndpointsClient.list") 13 | def test_get_endpoint_uri_successful(mock_get): 14 | """ 15 | Tests successful retrieval of the deployment URI for a model and version. 16 | """ 17 | endpoint_data = Endpoint( 18 | id=999, 19 | size="S", 20 | is_active=True, 21 | model_id=999, 22 | version_id=999, 23 | uri="testing.uri", 24 | ) 25 | endpoint_list = EndpointsList(root=[endpoint_data]) 26 | mock_get.return_value = endpoint_list 27 | uri = get_endpoint_uri(model_id=788, version_id=23) 28 | assert uri == "testing.uri" 29 | mock_get.assert_called_once() 30 | 31 | 32 | @patch("giza.cli.client.EndpointsClient.list") 33 | def test_get_endpoint_uri_not_found(mock_list): 34 | """ 35 | Tests the case where no active deployment is found for the model and version. 36 | """ 37 | endpoint_list = EndpointsList(root=[]) 38 | mock_list.return_value = endpoint_list 39 | uri = get_endpoint_uri(model_id=516, version_id=19) 40 | assert uri is None 41 | mock_list.assert_called_once() 42 | 43 | 44 | @mock.patch("giza.cli.client.WorkspaceClient.get") 45 | def test_get_workspace_uri_successful(mock_get): 46 | """ 47 | Tests successful retrieval the URI of the current workspace. 48 | """ 49 | mock_workspace = Workspace(status="test", url="test_url") 50 | mock_get.return_value = mock_workspace 51 | workspace_uri = get_workspace_uri() 52 | assert workspace_uri == "test_url" 53 | mock_get.assert_called_once() 54 | 55 | 56 | @mock.patch("giza.cli.client.WorkspaceClient.get") 57 | def test_get_workspace_uri_request_exception(mock_get): 58 | """ 59 | Tests RequestException in get_workspace_uri method(). 60 | """ 61 | mock_get.side_effect = requests.exceptions.RequestException 62 | 63 | with pytest.raises(requests.exceptions.RequestException): 64 | get_workspace_uri() 65 | 66 | 67 | @mock.patch("builtins.open") 68 | @mock.patch("json.load", side_effect=[{"test": "test"}]) 69 | def test_read_json_successful(*args): 70 | """ 71 | Tests when file is found 72 | """ 73 | response = read_json("path/to/open") 74 | assert response == {"test": "test"} 75 | assert response is not None 76 | 77 | 78 | @mock.patch("builtins.open", side_effect=FileNotFoundError) 79 | def test_read_json_file_not_found(mock_open): 80 | """ 81 | Tests when the JSON file is not found. 82 | """ 83 | with pytest.raises(FileNotFoundError): 84 | read_json("/notFound/") 85 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/pool_factory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from ape import Contract 5 | 6 | from giza.agents.integrations.uniswap.pool import Pool 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class PoolFactory: 12 | """ 13 | A factory class for managing Uniswap pool interactions. 14 | 15 | Attributes: 16 | contract (Contract): An instance of the Contract class to interact with the Uniswap pool factory. 17 | sender (str): The address of the transaction sender. 18 | 19 | Methods: 20 | __init__(self, address: str, sender: str): Initializes a new PoolFactory instance. 21 | get_pool(self, token0: str, token1: str, fee: int): Retrieves a pool based on the provided tokens and fee. 22 | create_pool(self, token0: str, token1: str, fee: int): Creates a new pool with the specified tokens and fee. 23 | """ 24 | 25 | def __init__(self, address: str, sender: str): 26 | """ 27 | Initializes a new instance of the PoolFactory class. 28 | 29 | Args: 30 | address (str): The address of the Uniswap pool factory contract. 31 | sender (str): The address of the entity initiating transactions. 32 | """ 33 | self.contract = Contract( 34 | address, 35 | abi=os.path.join(os.path.dirname(__file__), "assets/pool_factory.json"), 36 | ) 37 | self.sender = sender 38 | 39 | def get_pool(self, token0: str, token1: str, fee: int): 40 | """ 41 | Retrieves the address of a Uniswap pool for the specified tokens and fee. 42 | 43 | Args: 44 | token0 (str): The address of the first token. 45 | token1 (str): The address of the second token. 46 | fee (int): The pool fee in integer format. If a float is provided, it is converted to an integer. 47 | 48 | Returns: 49 | Pool: An instance of the Pool class representing the retrieved pool. 50 | """ 51 | if isinstance(fee, float): 52 | fee = int(fee * 1e6) 53 | pool_address = self.contract.getPool(token0, token1, fee) 54 | return Pool(pool_address, self.sender) 55 | 56 | def create_pool(self, token0: str, token1: str, fee: int): 57 | """ 58 | Creates a new Uniswap pool with the specified tokens and fee. 59 | 60 | Args: 61 | token0 (str): The address of the first token. 62 | token1 (str): The address of the second token. 63 | fee (int): The pool fee in integer format. If a float is provided, it is converted to an integer. 64 | 65 | Returns: 66 | The result of the pool creation transaction. 67 | """ 68 | if isinstance(fee, float): 69 | fee = int(fee * 1e6) 70 | logger.debug( 71 | f"Creating pool with token0 {token0}, token1 {token1}, and fee {fee}" 72 | ) 73 | return self.contract.createPool(token0, token1, fee, sender=self.sender) 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .chalice/deployments/ 2 | .chalice/deployed/ 3 | .chalice/venv/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | .ruff_cache 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | **/*.DS_Store 137 | **/*.onnx 138 | **/*.txt 139 | **/*.jpg 140 | **/*.whl 141 | **/data 142 | *.sierra 143 | *.proof 144 | 145 | # Pip files 146 | pydantic-1.x 147 | pydantic-2.x 148 | 149 | # Private Key info 150 | pk.txt 151 | 152 | # Forge Compiler files 153 | # examples/on-chain-mnist/contracts/cache/ 154 | # examples/on-chain-mnist/contracts/out/ 155 | 156 | # Poetry lock file 157 | poetry.lock 158 | 159 | # Cairo example files 160 | examples/on-chain_mnist/cairo/ 161 | 162 | examples/on-chain_mnist/cairo/lofi_mnst_2 163 | examples/on-chain_mnist/cairo/soft 164 | examples/on-chain_mnist/cairo/mnist_sierra 165 | examples/on-chain_mnist/contracts/out 166 | 167 | # cache files 168 | tmp 169 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/assets/quoter.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { "internalType": "address", "name": "_factory", "type": "address" }, 5 | { "internalType": "address", "name": "_WETH9", "type": "address" } 6 | ], 7 | "stateMutability": "nonpayable", 8 | "type": "constructor" 9 | }, 10 | { 11 | "inputs": [], 12 | "name": "WETH9", 13 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 14 | "stateMutability": "view", 15 | "type": "function" 16 | }, 17 | { 18 | "inputs": [], 19 | "name": "factory", 20 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 21 | "stateMutability": "view", 22 | "type": "function" 23 | }, 24 | { 25 | "inputs": [ 26 | { "internalType": "bytes", "name": "path", "type": "bytes" }, 27 | { "internalType": "uint256", "name": "amountIn", "type": "uint256" } 28 | ], 29 | "name": "quoteExactInput", 30 | "outputs": [ 31 | { "internalType": "uint256", "name": "amountOut", "type": "uint256" } 32 | ], 33 | "stateMutability": "nonpayable", 34 | "type": "function" 35 | }, 36 | { 37 | "inputs": [ 38 | { "internalType": "address", "name": "tokenIn", "type": "address" }, 39 | { "internalType": "address", "name": "tokenOut", "type": "address" }, 40 | { "internalType": "uint24", "name": "fee", "type": "uint24" }, 41 | { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, 42 | { 43 | "internalType": "uint160", 44 | "name": "sqrtPriceLimitX96", 45 | "type": "uint160" 46 | } 47 | ], 48 | "name": "quoteExactInputSingle", 49 | "outputs": [ 50 | { "internalType": "uint256", "name": "amountOut", "type": "uint256" } 51 | ], 52 | "stateMutability": "nonpayable", 53 | "type": "function" 54 | }, 55 | { 56 | "inputs": [ 57 | { "internalType": "bytes", "name": "path", "type": "bytes" }, 58 | { "internalType": "uint256", "name": "amountOut", "type": "uint256" } 59 | ], 60 | "name": "quoteExactOutput", 61 | "outputs": [ 62 | { "internalType": "uint256", "name": "amountIn", "type": "uint256" } 63 | ], 64 | "stateMutability": "nonpayable", 65 | "type": "function" 66 | }, 67 | { 68 | "inputs": [ 69 | { "internalType": "address", "name": "tokenIn", "type": "address" }, 70 | { "internalType": "address", "name": "tokenOut", "type": "address" }, 71 | { "internalType": "uint24", "name": "fee", "type": "uint24" }, 72 | { "internalType": "uint256", "name": "amountOut", "type": "uint256" }, 73 | { 74 | "internalType": "uint160", 75 | "name": "sqrtPriceLimitX96", 76 | "type": "uint160" 77 | } 78 | ], 79 | "name": "quoteExactOutputSingle", 80 | "outputs": [ 81 | { "internalType": "uint256", "name": "amountIn", "type": "uint256" } 82 | ], 83 | "stateMutability": "nonpayable", 84 | "type": "function" 85 | }, 86 | { 87 | "inputs": [ 88 | { "internalType": "int256", "name": "amount0Delta", "type": "int256" }, 89 | { "internalType": "int256", "name": "amount1Delta", "type": "int256" }, 90 | { "internalType": "bytes", "name": "path", "type": "bytes" } 91 | ], 92 | "name": "uniswapV3SwapCallback", 93 | "outputs": [], 94 | "stateMutability": "view", 95 | "type": "function" 96 | } 97 | ] 98 | -------------------------------------------------------------------------------- /giza/agents/logging.yaml: -------------------------------------------------------------------------------- 1 | # Prefect logging config file. 2 | # 3 | # Any item in this file can be overridden with an environment variable: 4 | # `PREFECT_LOGGING_[PATH]_[TO]_[KEY]=VALUE` 5 | # 6 | # Templated values can be used to insert values from the Prefect settings at runtime. 7 | 8 | version: 1 9 | disable_existing_loggers: False 10 | 11 | formatters: 12 | simple: 13 | format: "%(asctime)s.%(msecs)03d | %(message)s" 14 | datefmt: "%H:%M:%S" 15 | 16 | standard: 17 | (): prefect.logging.formatters.PrefectFormatter 18 | format: "%(asctime)s.%(msecs)03d | %(levelname)-7s | %(message)s" 19 | flow_run_fmt: "%(asctime)s.%(msecs)03d | %(levelname)-7s | Action run %(flow_run_name)r - %(message)s" 20 | task_run_fmt: "%(asctime)s.%(msecs)03d | %(levelname)-7s | Task run %(task_run_name)r - %(message)s" 21 | datefmt: "%H:%M:%S" 22 | 23 | debug: 24 | format: "%(asctime)s.%(msecs)03d | %(levelname)-7s | %(threadName)-12s | %(message)s" 25 | datefmt: "%H:%M:%S" 26 | 27 | json: 28 | class: prefect.logging.formatters.JsonFormatter 29 | format: "default" 30 | 31 | filters: 32 | flow_filter: 33 | class: giza.logger.WordReplacerFilter 34 | 35 | handlers: 36 | 37 | # The handlers we define here will output all logs they receive by default 38 | # but we include the `level` so it can be overridden by environment 39 | 40 | console: 41 | level: 0 42 | class: prefect.logging.handlers.PrefectConsoleHandler 43 | formatter: standard 44 | styles: 45 | log.web_url: bright_blue 46 | log.local_url: bright_blue 47 | 48 | log.info_level: cyan 49 | log.warning_level: yellow3 50 | log.error_level: red3 51 | log.critical_level: bright_red 52 | 53 | log.completed_state: green 54 | log.cancelled_state: yellow3 55 | log.failed_state: red3 56 | log.crashed_state: bright_red 57 | 58 | log.flow_run_name: magenta 59 | log.flow_name: bold magenta 60 | 61 | api: 62 | level: 0 63 | class: prefect.logging.handlers.APILogHandler 64 | 65 | 66 | debug: 67 | level: 0 68 | class: logging.StreamHandler 69 | formatter: debug 70 | 71 | loggers: 72 | prefect: 73 | level: "${PREFECT_LOGGING_LEVEL}" 74 | 75 | prefect.extra: 76 | level: "${PREFECT_LOGGING_LEVEL}" 77 | handlers: [api] 78 | 79 | prefect.flow_runs: 80 | level: NOTSET 81 | handlers: [api] 82 | 83 | prefect.task_runs: 84 | level: NOTSET 85 | handlers: [api] 86 | 87 | prefect.server: 88 | level: "${PREFECT_LOGGING_SERVER_LEVEL}" 89 | 90 | prefect.client: 91 | level: "${PREFECT_LOGGING_LEVEL}" 92 | 93 | prefect.infrastructure: 94 | level: "${PREFECT_LOGGING_LEVEL}" 95 | 96 | prefect._internal: 97 | level: "${PREFECT_LOGGING_INTERNAL_LEVEL}" 98 | propagate: false 99 | handlers: [debug] 100 | 101 | uvicorn: 102 | level: "${PREFECT_LOGGING_SERVER_LEVEL}" 103 | 104 | fastapi: 105 | level: "${PREFECT_LOGGING_SERVER_LEVEL}" 106 | 107 | # The root logger: any logger without propagation disabled sends to here as well 108 | root: 109 | # By default, we display warning level logs from any library in the console 110 | # to match Python's default behavior while formatting logs nicely 111 | level: WARNING 112 | handlers: [console] 113 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/quoter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ape import Contract 4 | from ape.contracts import ContractInstance 5 | 6 | 7 | class Quoter: 8 | """ 9 | A class to interact with the Uniswap Quoter contract for obtaining quotes on token swaps. 10 | 11 | Attributes: 12 | contract (Contract): An instance of the Contract class representing the Uniswap Quoter contract. 13 | sender (str): The address of the sender initiating the quotes. 14 | 15 | Methods: 16 | __init__(self, address: str, sender: str): Initializes the Quoter instance. 17 | quote_exact_input_single(self, amount_in: int, pool: ContractInstance | None = None, token_in: str | None = None, token_out: str | None = None, fee: int | None = None, sqrt_price_limit_x96: int | None = 0, block_number: int | None = None): Provides a quote for a single input swap. 18 | """ 19 | 20 | def __init__(self, address: str, sender: str): 21 | """ 22 | Initializes the Quoter instance with a contract address and sender. 23 | 24 | Args: 25 | address (str): The address of the Uniswap Quoter contract. 26 | sender (str): The address of the entity initiating the quotes. 27 | """ 28 | self.contract = Contract( 29 | address, abi=os.path.join(os.path.dirname(__file__), "assets/quoter.json") 30 | ) 31 | self.sender = sender 32 | 33 | def quote_exact_input_single( 34 | self, 35 | amount_in: int, 36 | pool: ContractInstance | None = None, 37 | token_in: str | None = None, 38 | token_out: str | None = None, 39 | fee: int | None = None, 40 | sqrt_price_limit_x96: int | None = 0, 41 | block_number: int | None = None, 42 | ): 43 | """ 44 | Provides a quote for a single input swap based on the specified parameters. 45 | 46 | Args: 47 | amount_in (int): The amount of the input token. 48 | pool (ContractInstance | None): The contract instance of the pool. If None, token_in, token_out, and fee must be provided. 49 | token_in (str | None): The address of the input token. If None, it is derived from the pool. 50 | token_out (str | None): The address of the output token. If None, it is derived from the pool. 51 | fee (int | None): The pool's fee tier. If None, it is derived from the pool. 52 | sqrt_price_limit_x96 (int | None): The square root price limit of the swap, defaults to 0. 53 | block_number (int | None): The specific block number to execute the call against. If None, the latest block is used. 54 | 55 | Returns: 56 | The quote for the swap. 57 | 58 | Raises: 59 | Exception: If neither a pool nor the required individual parameters (token_in, token_out, fee) are provided. 60 | """ 61 | if pool is None and (token_in is None or token_out is None or fee is None): 62 | raise Exception("Must provide pool or token_in, token_out, and fee") 63 | 64 | if token_in is None or token_out is None or fee is None: 65 | token_in = pool.token0 if token_in is None else token_in 66 | token_out = pool.token1 if token_out is None else token_out 67 | fee = pool.fee if fee is None else fee 68 | 69 | if block_number is None: 70 | return self.contract.quoteExactInputSingle.call( 71 | token_in, token_out, fee, amount_in, sqrt_price_limit_x96 72 | ) 73 | else: 74 | return self.contract.quoteExactInputSingle.call( 75 | token_in, 76 | token_out, 77 | fee, 78 | amount_in, 79 | sqrt_price_limit_x96, 80 | block_identifier=block_number, 81 | ) 82 | -------------------------------------------------------------------------------- /examples/verifiable_mnist/deployments/pytorch_mnist_deployment.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.optim as optim 4 | import torchvision 5 | import numpy as np 6 | from scipy.ndimage import zoom 7 | from torch.utils.data import DataLoader, TensorDataset 8 | 9 | 10 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 11 | 12 | input_size = 196 # 14x14 13 | hidden_size = 10 14 | num_classes = 10 15 | num_epochs = 10 16 | batch_size = 256 17 | learning_rate = 0.001 18 | 19 | 20 | class NeuralNet(nn.Module): 21 | def __init__(self, input_size, hidden_size, num_classes): 22 | super().__init__() 23 | self.input_size = input_size 24 | self.l1 = nn.Linear(input_size, hidden_size) 25 | self.relu = nn.ReLU() 26 | self.l2 = nn.Linear(hidden_size, num_classes) 27 | 28 | def forward(self, x): 29 | out = self.l1(x) 30 | out = self.relu(out) 31 | out = self.l2(out) 32 | return out 33 | 34 | 35 | def resize_images(images): 36 | return np.array([zoom(image[0], (0.5, 0.5)) for image in images]) 37 | 38 | 39 | async def prepare_datasets(): 40 | print("Prepare dataset ...") 41 | 42 | train_dataset = torchvision.datasets.MNIST(root="./data", train=True, download=True) 43 | test_dataset = torchvision.datasets.MNIST(root="./data", train=False) 44 | 45 | x_train = resize_images(train_dataset) 46 | x_test = resize_images(test_dataset) 47 | 48 | x_train = torch.tensor(x_train.reshape(-1, 14 * 14).astype("float32") / 255) 49 | y_train = torch.tensor([label for _, label in train_dataset], dtype=torch.long) 50 | 51 | x_test = torch.tensor(x_test.reshape(-1, 14 * 14).astype("float32") / 255) 52 | y_test = torch.tensor([label for _, label in test_dataset], dtype=torch.long) 53 | return x_train, y_train, x_test, y_test 54 | 55 | 56 | async def create_data_loaders(x_train, y_train, x_test, y_test): 57 | print("Create data loaders ...") 58 | 59 | train_loader = DataLoader( 60 | TensorDataset(x_train, y_train), batch_size=batch_size, shuffle=True 61 | ) 62 | test_loader = DataLoader( 63 | TensorDataset(x_test, y_test), batch_size=batch_size, shuffle=False 64 | ) 65 | return train_loader, test_loader 66 | 67 | 68 | async def train_model(train_loader): 69 | print("Create data loaders ...") 70 | 71 | model = NeuralNet(input_size, hidden_size, num_classes).to(device) 72 | criterion = nn.CrossEntropyLoss() 73 | optimizer = optim.Adam(model.parameters(), lr=learning_rate) 74 | 75 | for epoch in range(num_epochs): 76 | for i, (images, labels) in enumerate(train_loader): 77 | images = images.to(device).reshape(-1, 14 * 14) 78 | labels = labels.to(device) 79 | 80 | outputs = model(images) 81 | loss = criterion(outputs, labels) 82 | 83 | optimizer.zero_grad() 84 | loss.backward() 85 | optimizer.step() 86 | 87 | if (i + 1) % 100 == 0: 88 | print( 89 | f"Epoch [{epoch + 1}/{num_epochs}], Step [{i + 1}/{len(train_loader)}], Loss: {loss.item():.4f}" 90 | ) 91 | return model 92 | 93 | 94 | async def test_model(model, test_loader): 95 | with torch.no_grad(): 96 | n_correct = 0 97 | n_samples = 0 98 | for images, labels in test_loader: 99 | images = images.to(device).reshape(-1, 14 * 14) 100 | labels = labels.to(device) 101 | outputs = model(images) 102 | _, predicted = torch.max(outputs.data, 1) 103 | n_samples += labels.size(0) 104 | n_correct += (predicted == labels).sum().item() 105 | 106 | acc = 100.0 * n_correct / n_samples 107 | print(f"Accuracy of the network on the 10000 test images: {acc} %") 108 | 109 | 110 | def execution(): 111 | x_train, y_train, x_test, y_test = prepare_datasets() 112 | train_loader, test_loader = create_data_loaders(x_train, y_train, x_test, y_test) 113 | model = train_model(train_loader) 114 | test_model(model, test_loader) 115 | 116 | execution() 117 | -------------------------------------------------------------------------------- /examples/ezkl/linear_regression/README.md: -------------------------------------------------------------------------------- 1 | # Train a Linear Regression Using the EZKL backend 2 | 3 | This example demonstrates how to train a linear regression model using the EZKL backend. 4 | 5 | First, install the `torch`, `hummingbird-ml`, and `scikit-learn` packages by running the following command: 6 | 7 | ```bash 8 | pip install torch hummingbird-ml scikit-learn 9 | ``` 10 | 11 | This example uses the `scikit-learn` package to train a linear regression model and the `hummingbird-ml` package to convert the trained model to `torch` and then into ONNX, this is to maximize compatibility with `ezkl`. 12 | 13 | The code can be found in the [train_linear_regression.py](train_linear_regression.py) file, but we will explain each step. 14 | 15 | ## Train a Linear Regression Model 16 | 17 | The following code trains a linear regression model using the `scikit-learn` package: 18 | 19 | ```python 20 | import numpy as np 21 | from sklearn.linear_model import LinearRegression 22 | 23 | # Create a dataset 24 | X = np.array([[1, 1], [1, 2], [2, 2], [2, 3]]) 25 | y = np.dot(X, np.array([1, 2])) + 3 26 | 27 | # Train a linear regression model 28 | model = LinearRegression().fit(X, y) 29 | ``` 30 | 31 | ## Convert the Trained Model to `torch` 32 | 33 | The following code converts the trained model to `torch` using the `hummingbird-ml` package: 34 | 35 | ```python 36 | import hummingbird.ml 37 | 38 | # Convert the trained model to `torch` 39 | hb_model = hummingbird.ml.convert(model, "torch") 40 | ``` 41 | 42 | More information about the `hummingbird-ml` package can be found [here](https://github.com/microsoft/hummingbird). 43 | 44 | ## Convert the Trained Model to ONNX 45 | 46 | Now that we have a torch model, we can export it to ONNX using the default utilities in the `torch` package: 47 | 48 | ```python 49 | # Convert the trained model to ONNX 50 | sample = np.array([7, 2]) 51 | # Input to the model 52 | shape = sample.shape 53 | x = torch.rand(1, *shape, requires_grad=True) 54 | 55 | # Export the model 56 | torch.onnx.export( 57 | model, 58 | x, 59 | "network.onnx", 60 | export_params=True, 61 | opset_version=10, 62 | do_constant_folding=True, 63 | input_names=["input"], 64 | output_names=["output"], 65 | dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, 66 | ) 67 | ``` 68 | 69 | ## Create an `input.json` file for transpilation 70 | 71 | For the transpilation we need an example of the input data. In this case, we will use the `sample` variable to create the `input.json` file: 72 | 73 | ```python 74 | with open("input.json", "w") as f: 75 | f.write( 76 | json.dumps( 77 | { 78 | "input_shapes": [sample.shape], 79 | "input_data": [sample.tolist()], 80 | } 81 | ) 82 | ) 83 | ``` 84 | 85 | ## Deploy the verifiable model using the EZKL framework 86 | 87 | The first step is to use the `giza-cli` to transpile the model and create a version job. Once this job finishes we will be able to deploy the model as a service. 88 | 89 | ```bash 90 | giza transpile --framework EZKL --input-data? input.json network.onnx 91 | ``` 92 | 93 | The next step is to deploy the model as a service. 94 | 95 | ```bash 96 | giza deployments deploy --framework EZKL --model-id --version-id 97 | ``` 98 | 99 | ## Perform a prediction 100 | 101 | Using the `predict_action.py` you can add the generated `model_id` and `version_id` to the `predict_action.py` file and run the following command: 102 | 103 | ```bash 104 | python predict_action.py 105 | ``` 106 | 107 | This will start the action to perform the prediction. It includes two tasks, an example of how to perform a prediction using the `GizaModel`: 108 | 109 | ```python 110 | model = GizaModel(id=MODEL_ID, version=VERSION) 111 | 112 | result, request_id = model.predict(input_feed=[7, 2], verifiable=True, job_size="S") 113 | 114 | print(f"Result: {result}, request_id: {request_id}") 115 | ``` 116 | 117 | The latter will take the request and wait for the proof to be created. Check the script for [more information](predict_action.py). -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/pool.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ape import Contract, chain 4 | 5 | from giza.agents.integrations.uniswap.utils import tick_to_price 6 | 7 | 8 | class Pool: 9 | """ 10 | Represents a Uniswap pool, providing methods to interact with the pool's contract and fetch its details. 11 | 12 | Attributes: 13 | contract (Contract): The contract instance of the Uniswap pool. 14 | sender (str): The address of the sender interacting with the pool. 15 | token0 (Contract): The contract instance of the first token in the pool. 16 | token1 (Contract): The contract instance of the second token in the pool. 17 | token0_decimals (int): The number of decimals for the first token. 18 | token1_decimals (int): The number of decimals for the second token. 19 | fee (int): The fee associated with the pool transactions. 20 | """ 21 | 22 | def __init__( 23 | self, 24 | address: str, 25 | sender: str, 26 | fee: int | None = None, 27 | ): 28 | """ 29 | Initializes the Pool object with the specified address, sender, and optional fee. 30 | 31 | Args: 32 | address (str): The address of the Uniswap pool contract. 33 | sender (str): The address of the sender interacting with the pool. 34 | fee (int, optional): The fee associated with the pool. If None, the fee is fetched from the contract. 35 | """ 36 | self.contract = Contract( 37 | address, abi=os.path.join(os.path.dirname(__file__), "assets/pool.json") 38 | ) 39 | self.sender = sender 40 | self.token0 = Contract( 41 | self.contract.token0(), 42 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 43 | ) 44 | self.token0_decimals = self.token0.decimals() 45 | self.token1 = Contract( 46 | self.contract.token1(), 47 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 48 | ) 49 | self.token1_decimals = self.token1.decimals() 50 | self.fee = self.contract.fee() if fee is None else fee 51 | 52 | def get_pool_info(self, block_number: int | None = None): 53 | """ 54 | Fetches and returns various operational details about the pool at a specific block number. 55 | 56 | Args: 57 | block_number (int, optional): The block number at which to fetch the pool details. Defaults to the current block height. 58 | 59 | Returns: 60 | dict: A dictionary containing details about the pool such as sqrtPriceX96, tick, observation indices, fee protocol, and unlocked status. 61 | """ 62 | if block_number is None: 63 | block_number = chain.blocks.height 64 | ( 65 | sqrtPriceX96, 66 | tick, 67 | observationIndex, 68 | observationCardinality, 69 | observationCardinalityNext, 70 | feeProtocol, 71 | unlocked, 72 | ) = self.contract.slot0(block_identifier=block_number) 73 | return { 74 | "sqrtPriceX96": sqrtPriceX96, 75 | "tick": tick, 76 | "observationIndex": observationIndex, 77 | "observationCardinality": observationCardinality, 78 | "observationCardinalityNext": observationCardinalityNext, 79 | "feeProtocol": feeProtocol, 80 | "unlocked": unlocked, 81 | } 82 | 83 | def get_pool_price( 84 | self, block_number: int | None = None, invert: bool | None = False 85 | ): 86 | """ 87 | Calculates and returns the price of the pool based on the current tick, adjusted for token decimals and optionally inverted. 88 | 89 | Args: 90 | block_number (int, optional): The block number at which to fetch the tick for price calculation. Defaults to the current block height. 91 | invert (bool, optional): Whether to invert the price calculation. Defaults to False. 92 | 93 | Returns: 94 | float: The calculated price of the pool. 95 | """ 96 | if block_number is None: 97 | block_number = chain.blocks.height 98 | tick = self.get_pool_info(block_number)["tick"] 99 | return tick_to_price( 100 | tick, self.token0_decimals, self.token1_decimals, invert=invert 101 | ) 102 | -------------------------------------------------------------------------------- /examples/uni_v3_lp/mint_position.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from addresses import ADDRESSES 5 | from ape import Contract, accounts, chain, networks 6 | from dotenv import find_dotenv, load_dotenv 7 | from lp_tools import MAX_UINT_128, get_tick_range, tick_to_price 8 | 9 | load_dotenv(find_dotenv()) 10 | 11 | dev_passphrase = os.environ.get("DEV_PASSPHRASE") 12 | sepolia_rpc_url = os.environ.get("SEPOLIA_RPC_URL") 13 | 14 | 15 | def get_mint_params( 16 | user_address, 17 | token0_address, 18 | token1_address, 19 | amount0, 20 | amount1, 21 | pool_fee, 22 | lower_tick, 23 | upper_tick, 24 | deadline=None, 25 | slippage_tolerance=0.01, 26 | ): 27 | if deadline is None: 28 | deadline = int(time.time()) + 60 29 | mint_params = { 30 | "token0": token0_address, 31 | "token1": token1_address, 32 | "fee": pool_fee, 33 | "tickLower": lower_tick, 34 | "tickUpper": upper_tick, 35 | "amount0Desired": amount0, 36 | "amount1Desired": amount1, 37 | "amount0Min": 0, # int(amount0 * (1 - slippage_tolerance)), 38 | "amount1Min": 0, # int(amount1 * (1 - slippage_tolerance)), 39 | "recipient": user_address, 40 | "deadline": deadline, 41 | } 42 | return tuple(mint_params.values()) 43 | 44 | 45 | def get_all_user_positions(nft_manager, user_address): 46 | n_positions = nft_manager.balanceOf(user_address) 47 | positions = [] 48 | for n in range(n_positions): 49 | position = nft_manager.tokenOfOwnerByIndex(user_address, n) 50 | positions.append(position) 51 | return positions 52 | 53 | 54 | def get_pos_liquidity(nft_manager, nft_id): 55 | ( 56 | nonce, 57 | operator, 58 | token0, 59 | token1, 60 | fee, 61 | tickLower, 62 | tickUpper, 63 | liquidity, 64 | feeGrowthInside0LastX128, 65 | feeGrowthInside1LastX128, 66 | tokensOwed0, 67 | tokensOwed1, 68 | ) = nft_manager.positions(nft_id) 69 | return liquidity 70 | 71 | 72 | def close_position(user_address, nft_manager, nft_id): 73 | liq = get_pos_liquidity(nft_manager, nft_id) 74 | if liq > 0: 75 | nft_manager.decreaseLiquidity((nft_id, liq, 0, 0, int(time.time() + 60))) 76 | nft_manager.collect((nft_id, user_address, MAX_UINT_128, MAX_UINT_128)) 77 | 78 | 79 | if __name__ == "__main__": 80 | networks.parse_network_choice(f"ethereum:sepolia:{sepolia_rpc_url}").__enter__() 81 | chain_id = chain.chain_id 82 | 83 | # step 1: set params 84 | tokenA_amount = 1000 85 | tokenB_amount = 1000 86 | pct_dev = 0.1 87 | pool_fee = 3000 88 | # step 2: load contracts 89 | tokenA = Contract(ADDRESSES["UNI"][chain_id]) 90 | tokenB = Contract(ADDRESSES["WETH"][chain_id]) 91 | nft_manager = Contract(ADDRESSES["NonfungiblePositionManager"][chain_id]) 92 | pool_factory = Contract(ADDRESSES["PoolFactory"][chain_id]) 93 | pool_address = pool_factory.getPool(tokenA.address, tokenB.address, pool_fee) 94 | pool = Contract(pool_address) 95 | dev = accounts.load("dev") 96 | dev.set_autosign(True, passphrase=dev_passphrase) 97 | user_address = dev.address 98 | with accounts.use_sender("dev"): 99 | # step 3: fetch open positions 100 | positions = get_all_user_positions(nft_manager, user_address) 101 | print(f"Fouund the following open positions: {positions}") 102 | # step 4: close all positions 103 | print("Closing all open positions...") 104 | for nft_id in positions: 105 | close_position(user_address, nft_manager, nft_id) 106 | # step 4: calculate mint params 107 | print("Calculating mint params...") 108 | _, curr_tick, _, _, _, _, _ = pool.slot0() 109 | tokenA_decimals = tokenA.decimals() 110 | tokenB_decimals = tokenB.decimals() 111 | curr_price = tick_to_price(curr_tick, tokenA_decimals, tokenB_decimals) 112 | lower_tick, upper_tick = get_tick_range( 113 | curr_tick, pct_dev, tokenA_decimals, tokenB_decimals, pool_fee 114 | ) 115 | mint_params = get_mint_params( 116 | user_address, tokenA_amount, tokenB_amount, pool_fee, lower_tick, upper_tick 117 | ) 118 | # step 5: mint new position 119 | print("Minting new position...") 120 | nft_manager.mint(mint_params) 121 | -------------------------------------------------------------------------------- /giza/agents/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import textwrap 4 | from json import JSONDecodeError 5 | from typing import Dict, Optional 6 | 7 | import requests 8 | from giza.cli import API_HOST 9 | from giza.cli.client import EndpointsClient, WorkspaceClient 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def get_workspace_uri() -> str: 15 | """ 16 | Retrieves the URI of the current workspace. 17 | 18 | This function creates a WorkspaceClient instance using the API_HOST and 19 | calls its get method to retrieve the current workspace. It then returns 20 | the URL of the workspace. 21 | 22 | Returns: 23 | str: The URL of the current workspace. 24 | """ 25 | client = WorkspaceClient(API_HOST) 26 | try: 27 | workspace = client.get() 28 | except requests.exceptions.RequestException: 29 | logger.error("Failed to retrieve workspace") 30 | logger.error( 31 | "Please check that you have create a workspaces using the Giza CLI" 32 | ) 33 | raise 34 | return workspace.url 35 | 36 | 37 | def get_endpoint_uri(model_id: int, version_id: int) -> Optional[str]: 38 | """ 39 | Get the deployment URI associated with a specific model and version. 40 | 41 | Args: 42 | model_id (int): The ID of the model. 43 | version_id (int): The ID of the version. 44 | 45 | This function initializes a DeploymentsClient instance using the API_HOST and 46 | retrieves the deployment URI using its list method. The resulting URL of the 47 | deployment is returned. 48 | 49 | Returns: 50 | str: The URI of the deployment. 51 | """ 52 | client = EndpointsClient(API_HOST) 53 | deployments_list = client.list( 54 | params={"model_id": model_id, "version_id": version_id, "is_active": True} 55 | ) 56 | 57 | if len(deployments_list.root) == 1: 58 | return deployments_list.root[0].uri 59 | else: 60 | return None 61 | 62 | 63 | def read_json(file_path: str) -> dict: 64 | """ 65 | Read the JSON file from the specified path and return the 66 | JSON data. 67 | 68 | Args: 69 | file_path (str): The path to the JSON file. 70 | 71 | Returns: 72 | dict: The JSON data. 73 | """ 74 | with open(file_path) as file: 75 | return json.load(file) 76 | 77 | 78 | def requests_debug(response: requests.Response, *args, **kwargs) -> None: 79 | """ 80 | Log the request and response details. 81 | 82 | Args: 83 | response (requests.Response): The response object. 84 | *args: Variable length argument list. 85 | **kwargs: Arbitrary keyword arguments. 86 | """ 87 | 88 | def format_headers(d: Dict) -> str: 89 | return "\n".join(f"{k}: {v}" for k, v in d.items()) 90 | 91 | req_headers = response.request.headers.copy() 92 | # Remove the Authorization header as it may contain sensitive information 93 | req_headers.pop("Authorization", None) 94 | try: 95 | content = response.json() 96 | except JSONDecodeError: 97 | content = ( 98 | response.text if len(response.text) < 1000 else f"{response.text[:1000]}..." 99 | ) 100 | 101 | if response.request.body is None: 102 | body = "" 103 | elif len(response.request.body) > 1000: 104 | body = f"{response.request.body[:1000]}..." 105 | else: 106 | body = response.request.body 107 | try: 108 | print( 109 | textwrap.dedent( 110 | """ 111 | ---------------- request ---------------- 112 | {req.method} {req.url} 113 | {reqhdrs} 114 | 115 | {req_body} 116 | ---------------- response ---------------- 117 | {res.status_code} {res.reason} {res.url} 118 | {reshdrs} 119 | 120 | {res_content} 121 | """ 122 | ).format( 123 | req=response.request, 124 | req_body=body, 125 | res=response, 126 | res_content=content, 127 | reqhdrs=format_headers(req_headers), 128 | reshdrs=format_headers(response.headers), 129 | ) 130 | ) 131 | # We should not stop the execution for a print statement 132 | except Exception as e: 133 | logger.debug(f"Failed to log request and response details: {e}") 134 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/assets/pool_factory.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, 3 | { 4 | "anonymous": false, 5 | "inputs": [ 6 | { 7 | "indexed": true, 8 | "internalType": "uint24", 9 | "name": "fee", 10 | "type": "uint24" 11 | }, 12 | { 13 | "indexed": true, 14 | "internalType": "int24", 15 | "name": "tickSpacing", 16 | "type": "int24" 17 | } 18 | ], 19 | "name": "FeeAmountEnabled", 20 | "type": "event" 21 | }, 22 | { 23 | "anonymous": false, 24 | "inputs": [ 25 | { 26 | "indexed": true, 27 | "internalType": "address", 28 | "name": "oldOwner", 29 | "type": "address" 30 | }, 31 | { 32 | "indexed": true, 33 | "internalType": "address", 34 | "name": "newOwner", 35 | "type": "address" 36 | } 37 | ], 38 | "name": "OwnerChanged", 39 | "type": "event" 40 | }, 41 | { 42 | "anonymous": false, 43 | "inputs": [ 44 | { 45 | "indexed": true, 46 | "internalType": "address", 47 | "name": "token0", 48 | "type": "address" 49 | }, 50 | { 51 | "indexed": true, 52 | "internalType": "address", 53 | "name": "token1", 54 | "type": "address" 55 | }, 56 | { 57 | "indexed": true, 58 | "internalType": "uint24", 59 | "name": "fee", 60 | "type": "uint24" 61 | }, 62 | { 63 | "indexed": false, 64 | "internalType": "int24", 65 | "name": "tickSpacing", 66 | "type": "int24" 67 | }, 68 | { 69 | "indexed": false, 70 | "internalType": "address", 71 | "name": "pool", 72 | "type": "address" 73 | } 74 | ], 75 | "name": "PoolCreated", 76 | "type": "event" 77 | }, 78 | { 79 | "inputs": [ 80 | { "internalType": "address", "name": "tokenA", "type": "address" }, 81 | { "internalType": "address", "name": "tokenB", "type": "address" }, 82 | { "internalType": "uint24", "name": "fee", "type": "uint24" } 83 | ], 84 | "name": "createPool", 85 | "outputs": [ 86 | { "internalType": "address", "name": "pool", "type": "address" } 87 | ], 88 | "stateMutability": "nonpayable", 89 | "type": "function" 90 | }, 91 | { 92 | "inputs": [ 93 | { "internalType": "uint24", "name": "fee", "type": "uint24" }, 94 | { "internalType": "int24", "name": "tickSpacing", "type": "int24" } 95 | ], 96 | "name": "enableFeeAmount", 97 | "outputs": [], 98 | "stateMutability": "nonpayable", 99 | "type": "function" 100 | }, 101 | { 102 | "inputs": [{ "internalType": "uint24", "name": "", "type": "uint24" }], 103 | "name": "feeAmountTickSpacing", 104 | "outputs": [{ "internalType": "int24", "name": "", "type": "int24" }], 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "inputs": [ 110 | { "internalType": "address", "name": "", "type": "address" }, 111 | { "internalType": "address", "name": "", "type": "address" }, 112 | { "internalType": "uint24", "name": "", "type": "uint24" } 113 | ], 114 | "name": "getPool", 115 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 116 | "stateMutability": "view", 117 | "type": "function" 118 | }, 119 | { 120 | "inputs": [], 121 | "name": "owner", 122 | "outputs": [{ "internalType": "address", "name": "", "type": "address" }], 123 | "stateMutability": "view", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [], 128 | "name": "parameters", 129 | "outputs": [ 130 | { "internalType": "address", "name": "factory", "type": "address" }, 131 | { "internalType": "address", "name": "token0", "type": "address" }, 132 | { "internalType": "address", "name": "token1", "type": "address" }, 133 | { "internalType": "uint24", "name": "fee", "type": "uint24" }, 134 | { "internalType": "int24", "name": "tickSpacing", "type": "int24" } 135 | ], 136 | "stateMutability": "view", 137 | "type": "function" 138 | }, 139 | { 140 | "inputs": [ 141 | { "internalType": "address", "name": "_owner", "type": "address" } 142 | ], 143 | "name": "setOwner", 144 | "outputs": [], 145 | "stateMutability": "nonpayable", 146 | "type": "function" 147 | } 148 | ] 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/gizatechxyz/giza-agents/assets/18899187/eb01e7f2-c0ec-4467-ba29-09e96042dae4) 2 | 3 | # AI Agents 4 | 5 | Giza Agents is a framework for trust-minimized integration of machine learning into on-chain strategy and action, featuring mechanisms for agentic memory and reflection that improve performance over their lifecycle. 6 | 7 | The extensible nature of Giza Agents allows developers to enshrine custom strategies using ML and other algorithms, develop novel agent functionalities and manage continuous iteration processes. 8 | 9 | ## Where to start? 10 | 11 | Check out our extensive [documentation]([docs.gizatech.xyz/products/ai-agents) to understand the concepts and follow how-to-guides. 12 | 13 | ## Installation 14 | 15 | Start by creating a virtual environment in your project directory and activate it: 16 | 17 | ```bash 18 | $ python -m venv .env 19 | 20 | # Activate the virtual environment. On Linux and MacOs 21 | $ source .env/bin/activate 22 | 23 | # Activate Virtual environment on Windows: 24 | $ .env/Scripts/activate 25 | ``` 26 | 27 | Now you’re ready to install ⚡Agents with the following command: 28 | 29 | ```bash 30 | $ pip install giza-agents 31 | ``` 32 | 33 | ## Setup 34 | 35 | From your terminal, create a Giza user through our CLI in order to access the Giza Platform: 36 | 37 | ```bash 38 | $ giza users create 39 | ``` 40 | 41 | After creating your user, log into Giza: 42 | 43 | ```bash 44 | $ giza users login 45 | ``` 46 | 47 | Optional: you can create an API Key for your user in order to not regenerate your access token every few hours. 48 | 49 | ```bash 50 | $ giza users create-api-key 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Creating Agents 56 | 57 | Agents are the entities that interact with the Giza Platform to handle verification of predictions and interactions with Smart Contracts. They are responsible for wating until the proof of the prediction is available and verified, and then handling the interaction with the contract. 58 | 59 | To create the agent, you will first need to locally create an [ape](https://apeworx.io/framework/) account: 60 | 61 | ```bash 62 | $ ape accounts generate 63 | 64 | Enhance the security of your account by adding additional random input: 65 | Show mnemonic? [Y/n]: n 66 | Create Passphrase to encrypt account: 67 | Repeat for confirmation: 68 | SUCCESS: A new account '0x766867bB2E3E1A6E6245F4930b47E9aF54cEba0C' with HDPath m/44'/60'/0'/0/0 has been added with the id '' 69 | ``` 70 | 71 | For more information on how to create an account, check the [ape documentation](https://docs.apeworx.io/ape/stable/userguides/accounts.html). 72 | 73 | After creating the account, you can create the agent (**the agent must be associated with a version that has an existing endpoint**), this will prompt you to enter the account name: 74 | 75 | ```bash 76 | $ giza agents create --model-id --version-id --name --description 77 | 78 | [giza][2024-04-10 11:50:24.005] Creating agent ✅ 79 | [giza][2024-04-10 11:50:24.006] Using endpoint id to create agent, retrieving model id and version id 80 | [giza][2024-04-10 11:50:53.480] Select an existing account to create the agent. 81 | [giza][2024-04-10 11:50:53.480] Available accounts are: 82 | ┏━━━━━━━━━━━━━┓ 83 | ┃ Accounts ┃ 84 | ┡━━━━━━━━━━━━━┩ 85 | │ my_account │ 86 | └─────────────┘ 87 | Enter the account name: my_account 88 | { 89 | "id": 1, 90 | "name": , 91 | "description": , 92 | "parameters": { 93 | "model_id": , 94 | "version_id": , 95 | "endpoint_id": , 96 | "alias": "my_account" 97 | }, 98 | "created_date": "2024-04-10T09:51:04.226448", 99 | "last_update": "2024-04-10T09:51:04.226448" 100 | } 101 | ``` 102 | 103 | Now, to interact with the agent and the contract, wou will need to export the passphrase of the account to the environment. The variable name should be `_PASSPHRASE`, all in caps. Make sure to keep this secret: 104 | 105 | ```bash 106 | $ export _PASSPHRASE= 107 | ``` 108 | 109 | ```python 110 | from giza.agents import Agent 111 | 112 | # Here we check for the passphrase in the environment 113 | agent = Agent.from_id(id=1, contracts={"my_contract": "0x1234567890"}) 114 | 115 | # Predict the data 116 | result = agent.predict(data={"input": "data"}) 117 | 118 | # Handle the contracts 119 | with agent.execute() as contracts: 120 | # Wait for the verification and then execute the contract 121 | contract_result = contracts.my_contract.function(result.value) 122 | 123 | # Do anything with the result 124 | ... 125 | ``` 126 | 127 | ## Examples 128 | 129 | Examples of how to use the Agents can be found in the `examples` directory. Each example includes a README or a Notebook with detailed instructions on how to run the example. 130 | 131 | ## Contributing 132 | 133 | Contributions are welcome! Please submit a pull request or create an issue to get started. 134 | 135 | ## License 136 | 137 | The Giza SDK is licensed under the MIT license. 138 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | # TODO: Implement a test env. 2 | import os 3 | import tempfile 4 | from unittest.mock import patch 5 | 6 | import numpy as np 7 | from giza.cli.schemas.models import Model 8 | from giza.cli.schemas.versions import Version 9 | 10 | from giza.agents.model import GizaModel 11 | 12 | 13 | class ResponseStub: 14 | def __init__(self, json): 15 | self._json = json 16 | 17 | def json(self): 18 | return self._json 19 | 20 | def raise_for_status(self): 21 | pass 22 | 23 | 24 | @patch("giza.agents.model.GizaModel._get_credentials") 25 | @patch("giza.agents.model.GizaModel._get_model", return_value=Model(id=50)) 26 | @patch( 27 | "giza.agents.model.GizaModel._get_version", 28 | return_value=Version( 29 | version=2, 30 | framework="CAIRO", 31 | size=1, 32 | status="COMPLETED", 33 | created_date="2022-01-01T00:00:00Z", 34 | last_update="2022-01-01T00:00:00Z", 35 | ), 36 | ) 37 | @patch("giza.agents.model.GizaModel._set_session") 38 | @patch("giza.agents.model.GizaModel._retrieve_uri") 39 | @patch("giza.agents.model.GizaModel._get_endpoint_id", return_value=1) 40 | @patch( 41 | "giza.agents.model.requests.post", 42 | return_value=ResponseStub( 43 | {"request_id": "123", "result": {"arr_1": [[1, 2], [3, 4]]}} 44 | ), 45 | ) 46 | @patch( 47 | "giza.agents.model.GizaModel._parse_cairo_response", 48 | return_value=np.array([[1, 2], [3, 4]], dtype=np.uint32), 49 | ) 50 | @patch("giza.agents.model.VersionsClient.download_original", return_value=b"some bytes") 51 | def test_predict_success(*args): 52 | model = GizaModel(id=50, version=2) 53 | 54 | arr = np.array([[1, 2], [3, 4]], dtype=np.uint32) 55 | 56 | result, req_id = model.predict( 57 | input_feed={"arr_1": arr}, verifiable=True, custom_output_dtype="dummy_type" 58 | ) 59 | 60 | assert np.array_equal(result, arr) 61 | assert req_id == "123" 62 | 63 | 64 | @patch("giza.agents.model.GizaModel._get_credentials") 65 | @patch("giza.agents.model.GizaModel._get_model", return_value=Model(id=50)) 66 | @patch( 67 | "giza.agents.model.GizaModel._get_version", 68 | return_value=Version( 69 | version=2, 70 | framework="CAIRO", 71 | size=1, 72 | status="COMPLETED", 73 | created_date="2022-01-01T00:00:00Z", 74 | last_update="2022-01-01T00:00:00Z", 75 | ), 76 | ) 77 | @patch("giza.agents.model.GizaModel._set_session") 78 | @patch("giza.agents.model.GizaModel._retrieve_uri") 79 | @patch("giza.agents.model.GizaModel._get_endpoint_id", return_value=1) 80 | @patch( 81 | "giza.agents.model.requests.post", 82 | return_value=ResponseStub( 83 | {"request_id": "123", "result": {"arr_1": [[1, 2], [3, 4]]}} 84 | ), 85 | ) 86 | @patch( 87 | "giza.agents.model.GizaModel._parse_cairo_response", 88 | return_value=np.array([[1, 2], [3, 4]], dtype=np.uint32), 89 | ) 90 | @patch("giza.agents.model.VersionsClient.download_original", return_value=b"some bytes") 91 | def test_predict_success_with_file(*args): 92 | model = GizaModel(id=50, version=2) 93 | 94 | expected = np.array([[1, 2], [3, 4]], dtype=np.uint32) 95 | 96 | with tempfile.TemporaryDirectory() as tempdir: 97 | input_file = os.path.join(tempdir, "input.csv") 98 | np.savetxt(input_file, expected, delimiter=",") 99 | result, req_id = model.predict( 100 | input_file=input_file, 101 | verifiable=True, 102 | custom_output_dtype="tensor_int", 103 | ) 104 | 105 | assert np.array_equal(result, expected) 106 | assert req_id == "123" 107 | 108 | 109 | @patch("giza.agents.model.GizaModel._get_credentials") 110 | @patch("giza.agents.model.GizaModel._get_model", return_value=Model(id=50)) 111 | @patch( 112 | "giza.agents.model.GizaModel._get_version", 113 | return_value=Version( 114 | version=2, 115 | framework="CAIRO", 116 | size=1, 117 | status="COMPLETED", 118 | created_date="2022-01-01T00:00:00Z", 119 | last_update="2022-01-01T00:00:00Z", 120 | ), 121 | ) 122 | @patch("giza.agents.model.GizaModel._set_session") 123 | @patch("giza.agents.model.GizaModel._get_output_dtype") 124 | @patch("giza.agents.model.GizaModel._retrieve_uri") 125 | @patch("giza.agents.model.GizaModel._get_endpoint_id", return_value=1) 126 | @patch("giza.agents.model.VersionsClient.download_original", return_value=b"some bytes") 127 | def test_cache_implementation(*args): 128 | model = GizaModel(id=50, version=2) 129 | 130 | result1 = model._set_session() 131 | cache_size_after_first_call = len(model._cache) 132 | result2 = model._set_session() 133 | cache_size_after_second_call = len(model._cache) 134 | assert result1 == result2 135 | assert cache_size_after_first_call == cache_size_after_second_call 136 | 137 | result3 = model._get_output_dtype() 138 | cache_size_after_third_call = len(model._cache) 139 | result4 = model._get_output_dtype() 140 | cache_size_after_fourth_call = len(model._cache) 141 | assert result3 == result4 142 | assert cache_size_after_third_call == cache_size_after_fourth_call 143 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/assets/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /giza/agents/action.py: -------------------------------------------------------------------------------- 1 | import os 2 | from functools import partial, wraps 3 | from pathlib import Path 4 | from typing import Any, Callable, Optional 5 | 6 | from giza.agents.utils import get_workspace_uri # noqa: E402 7 | 8 | os.environ["PREFECT_API_URL"] = f"{get_workspace_uri()}/api" 9 | os.environ["PREFECT_UI_URL"] = get_workspace_uri() 10 | 11 | from prefect import Flow # noqa: E402 12 | from prefect import flow as _flow # noqa: E402 13 | from prefect.client.schemas.schedules import construct_schedule # noqa: E402 14 | from prefect.settings import PREFECT_API_URL # noqa: E402 15 | from prefect.settings import ( # noqa: E402 16 | PREFECT_LOGGING_SETTINGS_PATH, 17 | PREFECT_UI_URL, 18 | update_current_profile, 19 | ) 20 | from prefect.utilities.asyncutils import sync_compatible # noqa: E402 21 | from rich.console import Console # noqa: E402 22 | from rich.panel import Panel # noqa: E402 23 | 24 | from giza.agents import __module_path__ # noqa: E402 25 | 26 | 27 | class Action: 28 | """ 29 | A class to represent an Action. 30 | 31 | Attributes: 32 | name (str): The name of the action. 33 | _flow (Flow): The Prefect flow that represents the action's entrypoint. 34 | 35 | Methods: 36 | _set_settings: Updates the current profile with API and logging settings. 37 | _update_api_url: Updates the API URL in the current profile. 38 | get_flow: Returns the Prefect flow. 39 | serve: Serves the action, making it ready to poll for scheduled runs. 40 | """ 41 | 42 | def __init__(self, entrypoint: Flow, name: str): 43 | """ 44 | Constructs all the necessary attributes for the Action object. 45 | 46 | Args: 47 | entrypoint (Flow): The Prefect flow that represents the action's entrypoint. 48 | name (str): The name of the action. 49 | """ 50 | self.name = name 51 | self._flow = entrypoint 52 | self._set_settings() 53 | 54 | def _set_settings(self) -> None: 55 | """ 56 | Updates the current profile with the workspace API URL and the path to the logging configuration. 57 | """ 58 | update_current_profile(settings={PREFECT_API_URL: f"{get_workspace_uri()}/api"}) 59 | update_current_profile( 60 | settings={PREFECT_LOGGING_SETTINGS_PATH: f"{__module_path__}/logging.yaml"} 61 | ) 62 | 63 | def _update_api_url(self, api_url: str) -> None: 64 | """ 65 | Updates the API URL in the current profile. 66 | 67 | Args: 68 | api_url (str): The new API URL to set in the profile. 69 | """ 70 | update_current_profile(settings={PREFECT_API_URL: api_url}) 71 | 72 | def get_flow(self) -> Flow: 73 | """ 74 | Returns the Prefect flow associated with the action. 75 | 76 | Returns: 77 | Flow: The Prefect flow of the action. 78 | """ 79 | return self._flow 80 | 81 | @sync_compatible 82 | async def serve( 83 | self, 84 | name: str, 85 | cron: Optional[str] = None, 86 | interval: Optional[str] = None, 87 | parameters: Optional[dict] = None, 88 | print_starting_message: bool = True, 89 | ) -> None: 90 | """ 91 | Serves the action, making it ready to poll for scheduled runs. 92 | 93 | Args: 94 | name (str): The name to assign to the runner. If a file path is provided, it uses the file name without the extension. 95 | print_starting_message (bool, optional): Whether to print a starting message. Defaults to True. 96 | interval: An interval on which to schedule runs. Accepts either a number 97 | or a timedelta object. If a number is given, it will be interpreted as seconds. 98 | cron: A cron schedule for runs. 99 | """ 100 | 101 | workspace_url = get_workspace_uri() 102 | if workspace_url == "": 103 | raise ValueError( 104 | "Workspace URL cannot be empty. Please create a workspace using `giza workspace create` and wait for the workspace to have status COMPLETED." 105 | ) 106 | 107 | from prefect.runner import Runner 108 | 109 | # Handling for my_flow.serve(__file__) 110 | # Will set name to name of file where my_flow.serve() without the extension 111 | # Non filepath strings will pass through unchanged 112 | name = Path(name).stem 113 | 114 | schedule = None 115 | 116 | if interval or cron: 117 | schedule = construct_schedule( 118 | interval=interval, 119 | cron=cron, 120 | ) 121 | 122 | runner = Runner(name=name, pause_on_shutdown=False) 123 | deployment_id = await runner.add_flow( 124 | self._flow, 125 | name=name, 126 | schedule=schedule, 127 | parameters=parameters, 128 | ) 129 | if print_starting_message: 130 | help_message = ( 131 | f"[green]Your action {self.name!r} is being served and polling for" 132 | " scheduled runs!\n[/]" 133 | ) 134 | if PREFECT_UI_URL: 135 | help_message += ( 136 | "\nYou can run your action via the Actions UI:" 137 | f" [blue]{PREFECT_UI_URL.value()}/deployments/deployment/{deployment_id}[/]\n" 138 | ) 139 | 140 | console = Console() 141 | console.print(Panel(help_message)) 142 | await runner.start(webserver=False) 143 | 144 | 145 | def action(func: Callable, *task_init_args: Any, **task_init_kwargs: Any) -> Flow: 146 | """ 147 | Decorator to convert a function into a Prefect flow. 148 | 149 | Args: 150 | func (Callable, optional): The function to convert into a flow. If None, returns a partial function. 151 | **task_init_kwargs: Arbitrary keyword arguments passed to the flow initialization. 152 | 153 | Returns: 154 | Flow: The Prefect flow created from the function. 155 | """ 156 | if func is None: 157 | return partial(action, *task_init_args, **task_init_kwargs) 158 | 159 | @wraps(func) 160 | def safe_func(*args: Any, **kwargs: Any) -> Any: 161 | """ 162 | A wrapper function that calls the original function with its arguments. 163 | 164 | Args: 165 | **kwargs: Arbitrary keyword arguments passed to the original function. 166 | 167 | Returns: 168 | The return value of the original function. 169 | """ 170 | return func(*args, **kwargs) 171 | 172 | safe_func.__name__ = func.__name__ 173 | return _flow(safe_func, *task_init_args, **task_init_kwargs) 174 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/router.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | 5 | from ape import Contract 6 | 7 | from giza.agents.integrations.uniswap.pool import Pool 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Router: 13 | """ 14 | A Router class to interact with the Uniswap contract for cryptocurrency swaps. 15 | 16 | Attributes: 17 | contract (Contract): An instance of the Contract class representing the Uniswap router contract. 18 | sender (str): The address of the sender initiating the swaps. 19 | 20 | Methods: 21 | swap_exact_input_single: Executes a swap where a precise input amount is specified. 22 | swap_exact_output_single: Executes a swap where a precise output amount is specified. 23 | """ 24 | 25 | def __init__(self, address: str, sender: str): 26 | """ 27 | Initializes the Router with a contract address and a sender address. 28 | 29 | Args: 30 | address (str): The address of the Uniswap router contract. 31 | sender (str): The address of the entity initiating the swaps. 32 | """ 33 | self.contract = Contract( 34 | address, abi=os.path.join(os.path.dirname(__file__), "assets/router.json") 35 | ) 36 | self.sender = sender 37 | 38 | def swap_exact_input_single( 39 | self, 40 | amount_in: int, 41 | pool: Pool | None = None, 42 | token_in: str | None = None, 43 | token_out: str | None = None, 44 | fee: int | None = None, 45 | amount_out_min: int | None = 0, 46 | sqrt_price_limit_x96: int | None = 0, 47 | deadline: int | None = None, 48 | ): 49 | """ 50 | Executes a swap on Uniswap with an exact input amount specified. 51 | 52 | Args: 53 | amount_in (int): The amount of the input token to swap. 54 | pool (Pool, optional): The pool to perform the swap in. Defaults to None. 55 | token_in (str, optional): The address of the input token. Defaults to None. 56 | token_out (str, optional): The address of the output token. Defaults to None. 57 | fee (int, optional): The pool fee. Defaults to None. 58 | amount_out_min (int, optional): The minimum amount of the output token that must be received. Defaults to 0. 59 | sqrt_price_limit_x96 (int, optional): The price limit of the swap. Defaults to 0. 60 | deadline (int, optional): The timestamp after which the swap is invalid. Defaults to None. 61 | 62 | Raises: 63 | Exception: If necessary parameters are not provided. 64 | 65 | Returns: 66 | TransactionReceipt: A receipt of the swap transaction. 67 | """ 68 | if pool is None and (token_in is None or token_out is None or fee is None): 69 | raise Exception("Must provide pool or token_in, token_out, and fee") 70 | 71 | if token_in is None or token_out is None or fee is None: 72 | token_in = pool.token0 if token_in is None else token_in 73 | token_out = pool.token1 if token_out is None else token_out 74 | fee = pool.fee if fee is None else fee 75 | 76 | if isinstance(token_in, str): 77 | token_in = Contract( 78 | token_in, 79 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 80 | ) 81 | 82 | if amount_in > token_in.allowance(self.sender, self.contract.address): 83 | token_in.approve(self.contract.address, amount_in, sender=self.sender) 84 | 85 | # TODO: 86 | # add slippage and pool price impact protection 87 | # if amount_out_min or sqrt_price_limit_x96 are floats 88 | 89 | if deadline is None: 90 | deadline = int(time.time()) + 60 91 | 92 | swap_params = { 93 | "tokenIn": token_in, 94 | "tokenOut": token_out, 95 | "fee": fee, 96 | "recipient": self.sender, 97 | "amountIn": amount_in, 98 | "amountOutMinimum": amount_out_min, 99 | "sqrtPriceLimitX96": sqrt_price_limit_x96, 100 | } 101 | 102 | logger.debug(f"Swapping with the following parameters: {swap_params}") 103 | 104 | return self.contract.exactInputSingle(swap_params, sender=self.sender) 105 | 106 | def swap_exact_output_single( 107 | self, 108 | amount_out: int, 109 | pool: Pool | None = None, 110 | token_in: str | None = None, 111 | token_out: str | None = None, 112 | fee: int | None = None, 113 | amount_in_max: int | None = 0, 114 | sqrt_price_limit_x96: int | None = 0, 115 | deadline: int | None = None, 116 | ): 117 | """ 118 | Executes a swap on Uniswap with an exact output amount specified. 119 | 120 | Args: 121 | amount_out (int): The amount of the output token to receive. 122 | pool (Pool, optional): The pool to perform the swap in. Defaults to None. 123 | token_in (str, optional): The address of the input token. Defaults to None. 124 | token_out (str, optional): The address of the output token. Defaults to None. 125 | fee (int, optional): The pool fee. Defaults to None. 126 | amount_in_max (int, optional): The maximum amount of the input token that can be used. Defaults to 0. 127 | sqrt_price_limit_x96 (int, optional): The price limit of the swap. Defaults to 0. 128 | deadline (int, optional): The timestamp after which the swap is invalid. Defaults to None. 129 | 130 | Raises: 131 | Exception: If necessary parameters are not provided. 132 | 133 | Returns: 134 | TransactionReceipt: A receipt of the swap transaction. 135 | """ 136 | if deadline is None: 137 | deadline = int(time.time()) + 60 138 | 139 | if pool is None and (token_in is None or token_out is None or fee is None): 140 | raise Exception("Must provide pool or token_in, token_out, and fee") 141 | 142 | if token_in is None or token_out is None or fee is None: 143 | token_in = pool.token0 if token_in is None else token_in 144 | token_out = pool.token1 if token_out is None else token_out 145 | fee = pool.fee if fee is None else fee 146 | 147 | # TODO: 148 | # add slippage and pool price impact protection 149 | # if amount_out_min or sqrt_price_limit_x96 are floats 150 | 151 | swap_params = { 152 | "tokenIn": token_in, 153 | "tokenOut": token_out, 154 | "fee": fee, 155 | "recipient": self.sender, 156 | "deadline": deadline, 157 | "amountOut": amount_out, 158 | "amountInMaximum": amount_in_max, 159 | "sqrtPriceLimitX96": sqrt_price_limit_x96, 160 | } 161 | 162 | logger.debug(f"Swapping with the following parameters: {swap_params}") 163 | 164 | return self.contract.exactOutputSingle(swap_params, sender=self.sender) 165 | -------------------------------------------------------------------------------- /examples/agents/using_polygon.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using Polygon with Agents\n", 8 | "\n", 9 | "In the previous tutorial, we have seen how to use the `GizaAgent` class to create a simple agent that can interact with the Arbitrum blockchain. In this tutorial, we will learn how to use other chains with the `GizaAgent` class.\n", 10 | "\n", 11 | "As we rely on `ape` to interact with the blockchain, we can use any chain that it supports. The list of supported chains via plugins can be found [here](https://docs.apeworx.io/).\n", 12 | "\n", 13 | "In this tutorial, we will use `Polygon` as an example. `Polygon` is a layer 2 solution for Ethereum that provides low cost and fast transactions. It is supported by `ape` and we can use it with the `GizaAgent` class.\n", 14 | "\n", 15 | "## Before you begin\n", 16 | "\n", 17 | "1. Python 3.11 or later must be installed on your machine\n", 18 | "2. Giza CLI must be installed on your machine. You can install it by running `pip install giza-cli`\n", 19 | "3. giza-agents should be installed. You can install it by running `pip install giza-agents`\n", 20 | "4. You must have an active Giza account. If you don't have one, you can create one [here](https://cli.gizatech.xyz/examples/basic).\n", 21 | "5. You must have a model deployed on Giza. You can follow the tutorial [Build a Verifiable Neural Network](https://docs.gizatech.xyz/tutorials/zkml/verifiable-mnist-neural-network) to deploy an MNIST model on Giza.\n", 22 | "6. During the tutorial, you will need to interact with the Giza CLI, Ape's framework, and provide multiple inputs, like `model-id`, `version-id`, `account name`, etc.\n", 23 | "7. You must be logged in to the Giza CLI or have an API KEY.\n", 24 | "\n", 25 | "## Installing the required libraries\n", 26 | "\n", 27 | "Let's start by installing the required libraries.\n", 28 | "\n", 29 | "```bash\n", 30 | "pip install giza-agents\n", 31 | "```\n", 32 | "\n", 33 | "This will install the `giza-agents` library, which contains the necessary tools to create an AI Agent.\n", 34 | "\n", 35 | "## Creating an account\n", 36 | "\n", 37 | "Before we can create an AI Agent, we need to create an account using Ape's framework. We can do this by running the following command:\n", 38 | "\n", 39 | "```bash\n", 40 | "$ ape accounts generate \n", 41 | "Enhance the security of your account by adding additional random input:\n", 42 | "Show mnemonic? [Y/n]: n\n", 43 | "Create Passphrase to encrypt account:\n", 44 | "Repeat for confirmation:\n", 45 | "SUCCESS: A new account '0x766867bB2E3E1A6E6245F4930b47E9aF54cEba0C' with HDPath m/44'/60'/0'/0/0 has been added with the id ''\n", 46 | "```\n", 47 | "\n", 48 | "This will create a new account under `$HOME/.ape/accounts` using the keyfile structure from the [eth-keyfile library](https://github.com/ethereum/eth-keyfile)\n", 49 | ". For more information on the account management, you can refer to the [Ape's framework documentation](https://docs.apeworx.io/ape/stable/userguides/accounts.html#keyfile-accounts).\n", 50 | "\n", 51 | "During account generation we will be prompted to enter a passphrase to encrypt the account, which will be used to unlock the said account when needed, so **make sure to keep it safe**.\n", 52 | "\n", 53 | "We encourage the creation of a new account for each agent, as it will allow you to manage the agent's permissions and access control more effectively, but importing accounts is also possible.\n", 54 | "\n", 55 | "## Creating an AI Agent\n", 56 | "\n", 57 | "Now that we have a funded account, we can create an AI Agent. This can be done by running the following command:\n", 58 | "\n", 59 | "```bash\n", 60 | "giza agents create --endpoint-id --name --description \n", 61 | "\n", 62 | "# or if you don't have endpoint-id (but will be deprecated)\n", 63 | "\n", 64 | "giza agents create --model-id --version-id --name --description \n", 65 | "\n", 66 | "```\n", 67 | "\n", 68 | "This command will prompt you to choose the account you want to use with the agent. Once you select the account, the agent will be created and you will receive the agent id. The output will look like this:\n", 69 | "\n", 70 | "```console\n", 71 | "[giza][2024-04-24 21:46:35.009] Creating agent ✅ \n", 72 | "[giza][2024-04-24 21:46:35.011] Using endpoint id to create agent, retrieving model id and version id\n", 73 | "[giza][2024-04-24 21:46:35.935] Select an existing account to create the agent.\n", 74 | "[giza][2024-04-24 21:46:35.937] Available accounts are:\n", 75 | "┏━━━━━━━━━━━━━┓\n", 76 | "┃ Accounts ┃\n", 77 | "┡━━━━━━━━━━━━━┩\n", 78 | "│ my_account │\n", 79 | "└─────────────┘\n", 80 | "Enter the account name: my_account\n", 81 | "{\n", 82 | " \"id\": 1,\n", 83 | " \"name\": ,\n", 84 | " \"description\": ,\n", 85 | " \"parameters\": {\n", 86 | " \"model_id\": ,\n", 87 | " \"version_id\": ,\n", 88 | " \"endpoint_id\": ,\n", 89 | " \"account\": \"my_account\"\n", 90 | " },\n", 91 | " \"created_date\": \"2024-04-10T09:51:04.226448\",\n", 92 | " \"last_update\": \"2024-04-10T09:51:04.226448\"\n", 93 | "}\n", 94 | "```\n", 95 | "\n", 96 | "This will create an AI Agent that can be used to interact with the deployed MNIST model.\n", 97 | "\n", 98 | "## Use Agents in Polygon\n", 99 | "\n", 100 | "Let's start by installing the `ape-polygon` plugin." 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "!ape plugins install polygon" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "metadata": {}, 115 | "source": [ 116 | "We can confirm whether it has been installed, by running `ape networks list` in the terminal." 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 1, 122 | "metadata": {}, 123 | "outputs": [ 124 | { 125 | "name": "stdout", 126 | "output_type": "stream", 127 | "text": [ 128 | "\u001b[1;32methereum\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 129 | "├── \u001b[1;32mgoerli\u001b[0m\n", 130 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 131 | "├── \u001b[1;32mlocal\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 132 | "│ ├── \u001b[1;32mgeth\u001b[0m\n", 133 | "│ └── \u001b[1;32mtest\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 134 | "├── \u001b[1;32mmainnet\u001b[0m\n", 135 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 136 | "└── \u001b[1;32msepolia\u001b[0m\n", 137 | " └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 138 | "\u001b[1;32mpolygon\u001b[0m\n", 139 | "├── \u001b[1;32mamoy\u001b[0m\n", 140 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 141 | "├── \u001b[1;32mlocal\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 142 | "│ └── \u001b[1;32mtest\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 143 | "├── \u001b[1;32mmainnet\u001b[0m\n", 144 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 145 | "└── \u001b[1;32mmumbai\u001b[0m\n", 146 | " └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n" 147 | ] 148 | } 149 | ], 150 | "source": [ 151 | "!ape networks list" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Here, we can see that we have multiple networks available, including `polygon`. So now we can use it when instantiating the `GizaAgent` class.\n", 159 | "\n", 160 | "For this execution, we will use the Polygon mainnet and a private RPC node. This is because public nodes have small quotas that can easily be reached.\n", 161 | "\n", 162 | "The contract is a verified contract selected at random from [polygonscan](https://polygonscan.com/). The aim is to show that we can read properties from this contract, which means that we could also execute a write function.\n", 163 | "\n", 164 | "In this case, as we are only executing a read function, there is no need for a funded wallet because we won't be signing any transactions.\n", 165 | "\n", 166 | "Remember that you will need to specify the `_PASSPHRASE`. If you wish to launch your operation as a script, exporting it will be enough:\n", 167 | "\n", 168 | "```bash\n", 169 | "export _PASSPHRASE=your-passphrase\n", 170 | "```\n", 171 | "\n", 172 | "If you are using it from a notebook, you will need to launch the notebook instance from an environment with the passphrase variable or set it in the code before importing `giza.agents`:\n", 173 | "\n", 174 | "```python\n", 175 | "import os\n", 176 | "os.environ[\"_PASSPHRASE\"] = \"your-passphrase\"\n", 177 | "\n", 178 | "from giza.agents import GizaAgent\n", 179 | "...\n", 180 | "```\n", 181 | "\n", 182 | "Now we can instantiate the agent:" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 3, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "import os\n", 192 | "os.environ[\"_PASSPHRASE\"] = ...\n", 193 | "\n", 194 | "\n", 195 | "from giza.agents import GizaAgent\n", 196 | "\n", 197 | "MODEL_ID = ...\n", 198 | "VERSION_ID = ...\n", 199 | "ACCOUNT = ...\n", 200 | "PRIVATE_RPC = ... # This can also be loaded from the environment or a .env file\n", 201 | "\n", 202 | "agent = GizaAgent(\n", 203 | " id=MODEL_ID,\n", 204 | " version_id=VERSION_ID,\n", 205 | " chain=f\"polygon:mainnet:{PRIVATE_RPC}\",\n", 206 | " account=ACCOUNT,\n", 207 | " contracts={\n", 208 | " \"random\": \"0x577f6076E558818A5dF21Ce4acdE9A9623eC0b4c\"\n", 209 | " }\n", 210 | ")" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "Now that we have the agent instance, we can enter the `execute()` context and call the read function from an Polygon smart contract:" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 2, 223 | "metadata": {}, 224 | "outputs": [ 225 | { 226 | "name": "stdout", 227 | "output_type": "stream", 228 | "text": [ 229 | "INFO: Connecting to a 'bor' node.\n", 230 | "WARNING: Danger! This account will now sign any transaction it's given.\n", 231 | "Contract name is: Balancer 80SD-20maticX\n" 232 | ] 233 | } 234 | ], 235 | "source": [ 236 | "with agent.execute() as contracts:\n", 237 | " result = contracts.random.name()\n", 238 | " print(f\"Contract name is: {result}\")" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "## What we have learned\n", 246 | "\n", 247 | "In this tutorial, we learned how to create an AI Agent and interact with Polygon.\n", 248 | "\n", 249 | "For this, we needed to:\n", 250 | "\n", 251 | "* Install the `polygon` plugin\n", 252 | "* Check that the new network is available\n", 253 | "* Got a contract from `polygonscan`\n", 254 | "* Use an agent to execute a function from an `polygon` smart contract\n", 255 | "\n", 256 | "Now these same steps can be followed to use any other network supported by `ape` and interact with different chains." 257 | ] 258 | } 259 | ], 260 | "metadata": { 261 | "kernelspec": { 262 | "display_name": ".venv", 263 | "language": "python", 264 | "name": "python3" 265 | }, 266 | "language_info": { 267 | "codemirror_mode": { 268 | "name": "ipython", 269 | "version": 3 270 | }, 271 | "file_extension": ".py", 272 | "mimetype": "text/x-python", 273 | "name": "python", 274 | "nbconvert_exporter": "python", 275 | "pygments_lexer": "ipython3", 276 | "version": "3.11.9" 277 | } 278 | }, 279 | "nbformat": 4, 280 | "nbformat_minor": 2 281 | } 282 | -------------------------------------------------------------------------------- /examples/agents/using_arbitrum.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using Arbitrum with Agents\n", 8 | "\n", 9 | "In the previous tutorial, we have seen how to use the `GizaAgent` class to create a simple agent that can interact with the Ethereum blockchain. In this tutorial, we will learn how to use other chains with the `GizaAgent` class.\n", 10 | "\n", 11 | "As we rely on `ape` to interact with the blockchain, we can use any chain that it supports. The list of supported chains via plugins can be found [here](https://docs.apeworx.io/).\n", 12 | "\n", 13 | "In this tutorial, we will use `Arbitrum` as an example. `Arbitrum` is a layer 2 solution for Ethereum that provides low cost and fast transactions. It is supported by `ape` and we can use it with the `GizaAgent` class.\n", 14 | "\n", 15 | "## Before you begin\n", 16 | "\n", 17 | "1. Python 3.11 or later must be installed on your machine\n", 18 | "2. Giza CLI must be installed on your machine. You can install it by running `pip install giza-cli`\n", 19 | "3. giza-agents should be installed. You can install it by running `pip install giza-agents`\n", 20 | "4. You must have an active Giza account. If you don't have one, you can create one [here](https://cli.gizatech.xyz/examples/basic).\n", 21 | "5. You must have an MNIST model deployed on Giza. You can follow the tutorial [Build a Verifiable Neural Network](https://docs.gizatech.xyz/tutorials/zkml/verifiable-mnist-neural-network) to deploy an MNIST model on Giza.\n", 22 | "6. During the tutorial, you will need to interact with the Giza CLI, and Ape's framework, and provide multiple inputs, like `model-id`, `version-id`, `account name`, etc.\n", 23 | "7. You must be logged in to the Giza CLI or have an API KEY.\n", 24 | "\n", 25 | "## Installing the required libraries\n", 26 | "\n", 27 | "Let's start by installing the required libraries.\n", 28 | "\n", 29 | "```bash\n", 30 | "pip install giza-agents\n", 31 | "```\n", 32 | "\n", 33 | "This will install the `giza-agents` library along with the `agents` extra, which contains the necessary tools to create an AI Agent.\n", 34 | "\n", 35 | "## Creating an account\n", 36 | "\n", 37 | "Before we can create an AI Agent, we need to create an account using Ape's framework. We can do this by running the following command:\n", 38 | "\n", 39 | "```bash\n", 40 | "$ ape accounts generate \n", 41 | "Enhance the security of your account by adding additional random input:\n", 42 | "Show mnemonic? [Y/n]: n\n", 43 | "Create Passphrase to encrypt account:\n", 44 | "Repeat for confirmation:\n", 45 | "SUCCESS: A new account '0x766867bB2E3E1A6E6245F4930b47E9aF54cEba0C' with HDPath m/44'/60'/0'/0/0 has been added with the id ''\n", 46 | "```\n", 47 | "\n", 48 | "This will create a new account under `$HOME/.ape/accounts` using the keyfile structure from the [eth-keyfile library](https://github.com/ethereum/eth-keyfile)\n", 49 | ". For more information on the account management, you can refer to the [Ape's framework documentation](https://docs.apeworx.io/ape/stable/userguides/accounts.html#keyfile-accounts).\n", 50 | "\n", 51 | "During account generation we will be prompted to enter a passphrase to encrypt the account, which will be used to unlock the said account when needed, so **make sure to keep it safe**.\n", 52 | "\n", 53 | "We encourage the creation of a new account for each agent, as it will allow you to manage the agent's permissions and access control more effectively, but importing accounts is also possible.\n", 54 | "\n", 55 | "## Creating an AI Agent\n", 56 | "\n", 57 | "Now that we have a funded account, we can create an AI Agent. This can be done by running the following command:\n", 58 | "\n", 59 | "```bash\n", 60 | "giza agents create --model-id --version-id --name --description \n", 61 | "\n", 62 | "# or if you have the endpoint-id\n", 63 | "\n", 64 | "giza agents create --endpoint-id --name --description \n", 65 | "```\n", 66 | "\n", 67 | "This command will prompt you to choose the account you want to use with the agent. Once you select the account, the agent will be created and you will receive the agent id. The output will look like this:\n", 68 | "\n", 69 | "```console\n", 70 | "[giza][2024-04-10 11:50:24.005] Creating agent ✅\n", 71 | "[giza][2024-04-10 11:50:24.006] Using endpoint id to create agent, retrieving model id and version id\n", 72 | "[giza][2024-04-10 11:50:53.480] Select an existing account to create the agent.\n", 73 | "[giza][2024-04-10 11:50:53.480] Available accounts are:\n", 74 | "┏━━━━━━━━━━━━━┓\n", 75 | "┃ Accounts ┃\n", 76 | "┡━━━━━━━━━━━━━┩\n", 77 | "│ my_account │\n", 78 | "└─────────────┘\n", 79 | "Enter the account name: my_account\n", 80 | "{\n", 81 | " \"id\": 1,\n", 82 | " \"name\": ,\n", 83 | " \"description\": ,\n", 84 | " \"parameters\": {\n", 85 | " \"model_id\": ,\n", 86 | " \"version_id\": ,\n", 87 | " \"endpoint_id\": ,\n", 88 | " \"alias\": \"my_account\"\n", 89 | " },\n", 90 | " \"created_date\": \"2024-04-10T09:51:04.226448\",\n", 91 | " \"last_update\": \"2024-04-10T09:51:04.226448\"\n", 92 | "}\n", 93 | "```\n", 94 | "\n", 95 | "This will create an AI Agent that can be used to interact with the deployed MNIST model.\n", 96 | "\n", 97 | "## Use Agents in Arbitrum\n", 98 | "\n", 99 | "Let's start by installing the `ape-arbitrum` plugin." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "!ape plugins install arbitrum" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "We can confirm whether it has been installed, by running `ape networks list` in the terminal." 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 2, 121 | "metadata": {}, 122 | "outputs": [ 123 | { 124 | "name": "stdout", 125 | "output_type": "stream", 126 | "text": [ 127 | "\u001b[1;32marbitrum\u001b[0m\n", 128 | "├── \u001b[1;32mgoerli\u001b[0m\n", 129 | "│ ├── \u001b[1;32malchemy\u001b[0m\n", 130 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 131 | "├── \u001b[1;32mlocal\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 132 | "│ └── \u001b[1;32mtest\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 133 | "├── \u001b[1;32mmainnet\u001b[0m\n", 134 | "│ ├── \u001b[1;32malchemy\u001b[0m\n", 135 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 136 | "└── \u001b[1;32msepolia\u001b[0m\n", 137 | " ├── \u001b[1;32malchemy\u001b[0m\n", 138 | " └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 139 | "\u001b[1;32methereum\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 140 | "├── \u001b[1;32mgoerli\u001b[0m\n", 141 | "│ ├── \u001b[1;32malchemy\u001b[0m\n", 142 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 143 | "├── \u001b[1;32mlocal\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 144 | "│ ├── \u001b[1;32mgeth\u001b[0m\n", 145 | "│ └── \u001b[1;32mtest\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 146 | "├── \u001b[1;32mmainnet\u001b[0m\n", 147 | "│ ├── \u001b[1;32malchemy\u001b[0m\n", 148 | "│ └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n", 149 | "└── \u001b[1;32msepolia\u001b[0m\n", 150 | " ├── \u001b[1;32malchemy\u001b[0m\n", 151 | " └── \u001b[1;32mgeth\u001b[0m\u001b[1;2;39m (default)\u001b[0m\n" 152 | ] 153 | } 154 | ], 155 | "source": [ 156 | "!ape networks list" 157 | ] 158 | }, 159 | { 160 | "cell_type": "markdown", 161 | "metadata": {}, 162 | "source": [ 163 | "Here, we can see that we have multiple networks available, including `arbitrum`. So now we can use it when instantiating the `GizaAgent` class.\n", 164 | "\n", 165 | "For this execution, we will use the Arbitrum mainnet and a private RPC node. This is because public nodes have small quotas that can easily be reached.\n", 166 | "\n", 167 | "The contract is a verified contract selected at random from [arbiscan](https://arbiscan.io/). The aim is to show that we can read properties from this contract, which means that we could also execute a write function.\n", 168 | "\n", 169 | "In this case, as we are only executing a read function, there is no need for a funded wallet because we won't be signing any transactions.\n", 170 | "\n", 171 | "Remember that you will need to specify the `_PASSPHRASE`. If you wish to launch your operation as a script, exporting it will be enough:\n", 172 | "\n", 173 | "```bash\n", 174 | "export _PASSPHRASE=your-passphrase\n", 175 | "```\n", 176 | "\n", 177 | "If you are using it from a notebook, you will need to launch the notebook instance from an environment with the passphrase variable or set it in the code before importing `giza.agents`:\n", 178 | "\n", 179 | "```python\n", 180 | "import os\n", 181 | "os.environ[\"_PASSPHRASE\"] = \"your-passphrase\"\n", 182 | "\n", 183 | "from giza.agents import GizaAgent\n", 184 | "...\n", 185 | "```\n", 186 | "\n", 187 | "Now we can instantiate the agent:" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": 5, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "import os\n", 197 | "os.environ[\"_PASSPHRASE\"] = ...\n", 198 | "\n", 199 | "\n", 200 | "from giza.agents import GizaAgent\n", 201 | "\n", 202 | "MODEL_ID = ...\n", 203 | "VERSION_ID = ...\n", 204 | "ACCOUNT = ...\n", 205 | "PRIVATE_RPC = ... # This can also be loaded from the environment or a .env file\n", 206 | "\n", 207 | "agent = GizaAgent(\n", 208 | " id=MODEL_ID,\n", 209 | " version_id=VERSION_ID,\n", 210 | " chain=f\"arbitrum:mainnet:{PRIVATE_RPC}\",\n", 211 | " account=ACCOUNT,\n", 212 | " contracts={\n", 213 | " \"random\": \"0x8606d62fD47473Fad2db53Ce7b2B820FdEab7AAF\"\n", 214 | " }\n", 215 | ")" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "Now that we have the agent instance, we can enter the `execute()` context and call the read function from an Arbitrum smart contract:" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": 6, 228 | "metadata": {}, 229 | "outputs": [ 230 | { 231 | "name": "stdout", 232 | "output_type": "stream", 233 | "text": [ 234 | "INFO: Connecting to a 'nitro' node.\n", 235 | "WARNING: Danger! This account will now sign any transaction it's given.\n", 236 | "Contract name is: FatalDoge\n" 237 | ] 238 | } 239 | ], 240 | "source": [ 241 | "with agent.execute() as contracts:\n", 242 | " result = contracts.random.name()\n", 243 | " print(f\"Contract name is: {result}\")" 244 | ] 245 | }, 246 | { 247 | "cell_type": "markdown", 248 | "metadata": {}, 249 | "source": [ 250 | "## What we have learned\n", 251 | "\n", 252 | "In this tutorial, we learned how to create an AI Agent and interact with Arbitrum.\n", 253 | "\n", 254 | "For this, we needed to:\n", 255 | "\n", 256 | "* Install the `arbitrum` plugin\n", 257 | "* Check that the new network is available\n", 258 | "* Got a contract from `arbiscan`\n", 259 | "* Use an agent to execute a function from an `arbitrum` smart contract\n", 260 | "\n", 261 | "Now these same steps can be followed to use any other network supported by `ape` and interact with different chains." 262 | ] 263 | } 264 | ], 265 | "metadata": { 266 | "kernelspec": { 267 | "display_name": ".venv", 268 | "language": "python", 269 | "name": "python3" 270 | }, 271 | "language_info": { 272 | "codemirror_mode": { 273 | "name": "ipython", 274 | "version": 3 275 | }, 276 | "file_extension": ".py", 277 | "mimetype": "text/x-python", 278 | "name": "python", 279 | "nbconvert_exporter": "python", 280 | "pygments_lexer": "ipython3", 281 | "version": "3.11.8" 282 | } 283 | }, 284 | "nbformat": 4, 285 | "nbformat_minor": 2 286 | } 287 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/nft_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | 5 | from ape import Contract 6 | 7 | from giza.agents.integrations.uniswap.constants import MAX_UINT_128 8 | from giza.agents.integrations.uniswap.utils import ( 9 | calc_amount0, 10 | calc_amount1, 11 | liquidity0, 12 | liquidity1, 13 | nearest_tick, 14 | price_to_sqrtp, 15 | price_to_tick, 16 | ) 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class NFTManager: 22 | """ 23 | Manages NFT-based liquidity positions on Uniswap via interaction with smart contracts. 24 | 25 | Attributes: 26 | contract (Contract): An instance of the Contract class representing the NFT manager smart contract. 27 | sender (str): The address of the user managing the NFT positions. 28 | """ 29 | 30 | def __init__(self, address: str, sender: str): 31 | """ 32 | Initializes the NFTManager with a specific contract address and sender. 33 | 34 | Args: 35 | address (str): The address of the NFT manager smart contract. 36 | sender (str): The address of the user managing the NFT positions. 37 | """ 38 | self.contract = Contract( 39 | address, 40 | abi=os.path.join(os.path.dirname(__file__), "assets/nft_manager.json"), 41 | ) 42 | self.sender = sender 43 | 44 | def get_all_user_positions(self, user_address: str | None = None): 45 | """ 46 | Retrieves all NFT positions owned by a specified user. 47 | 48 | Args: 49 | user_address (str | None): The address of the user whose positions are to be retrieved. 50 | Defaults to the sender's address if None. 51 | 52 | Returns: 53 | list: A list of position identifiers owned by the user. 54 | """ 55 | if user_address is None: 56 | user_address = self.sender 57 | n_positions = self.contract.balanceOf(user_address) 58 | positions = [] 59 | for n in range(n_positions): 60 | position = self.contract.tokenOfOwnerByIndex(user_address, n) 61 | positions.append(position) 62 | return positions 63 | 64 | def close_position(self, nft_id: int, user_address: str | None = None): 65 | """ 66 | Closes an NFT position by decreasing its liquidity to zero, collecting fees, and burning the NFT. 67 | 68 | Args: 69 | nft_id (int): The identifier of the NFT position to close. 70 | user_address (str | None): The address of the user closing the position. 71 | Defaults to the sender's address if None. 72 | """ 73 | if user_address is None: 74 | user_address = self.sender 75 | liquidity = self.contract.positions(nft_id)["liquidity"] 76 | if liquidity > 0: 77 | self.decrease_liquidity(nft_id, liquidity=liquidity) 78 | self.collect_fees(nft_id, user_address=user_address) 79 | self.contract.burn(nft_id, sender=self.sender) 80 | 81 | def collect_fees( 82 | self, 83 | nft_id: int, 84 | user_address: str | None = None, 85 | amount0_max: int | None = MAX_UINT_128, 86 | amount1_max: int | None = MAX_UINT_128, 87 | ): 88 | """ 89 | Collects accumulated fees from a specified NFT position. 90 | 91 | Args: 92 | nft_id (int): The identifier of the NFT position from which to collect fees. 93 | user_address (str | None): The address of the user collecting the fees. 94 | Defaults to the sender's address if None. 95 | amount0_max (int | None): The maximum amount of token0 to collect. 96 | Defaults to the maximum uint128 value. 97 | amount1_max (int | None): The maximum amount of token1 to collect. 98 | Defaults to the maximum uint128 value. 99 | 100 | Returns: 101 | dict: A receipt of the transaction. 102 | """ 103 | if user_address is None: 104 | user_address = self.sender 105 | logger.debug(f"Collecting fees for position {nft_id} for user {user_address}") 106 | receipt = self.contract.collect( 107 | (nft_id, user_address, amount0_max, amount1_max), sender=self.sender 108 | ) 109 | logger.debug(f"Collected fees for position {nft_id} for user {user_address}") 110 | return receipt 111 | 112 | def decrease_liquidity( 113 | self, 114 | nft_id: int, 115 | liquidity: int | None = None, 116 | amount0Min: int | None = 0, 117 | amount1Min: int | None = 0, 118 | deadline: int | None = None, 119 | ): 120 | """ 121 | Decreases the liquidity of a specified NFT position. 122 | 123 | Args: 124 | nft_id (int): The identifier of the NFT position. 125 | liquidity (int | None): The amount of liquidity to remove. If None, fetches the current liquidity. 126 | amount0Min (int | None): The minimum amount of token0 that must remain after decreasing liquidity. 127 | Defaults to 0. 128 | amount1Min (int | None): The minimum amount of token1 that must remain after decreasing liquidity. 129 | Defaults to 0. 130 | deadline (int | None): The Unix timestamp after which the transaction is no longer valid. 131 | Defaults to 60 seconds from the current time. 132 | 133 | Returns: 134 | dict: A receipt of the transaction. 135 | """ 136 | if liquidity is None: 137 | liquidity = self.get_pos_info(nft_id)["liquidity"] 138 | if deadline is None: 139 | deadline = int(time.time() + 60) 140 | logger.debug( 141 | f"Decreasing liquidity for position {nft_id} for user {self.sender}" 142 | ) 143 | receipt = self.contract.decreaseLiquidity( 144 | (nft_id, liquidity, amount0Min, amount1Min, deadline), sender=self.sender 145 | ) 146 | logger.debug( 147 | f"Decreased liquidity for position {nft_id} for user {self.sender}" 148 | ) 149 | return receipt 150 | 151 | def add_liquidity( 152 | self, 153 | nft_id: int, 154 | amount0Desired: int, 155 | amount1Desired: int, 156 | amount0Min: int | None = 0, 157 | amount1Min: int | None = 0, 158 | deadline: int | None = None, 159 | ): 160 | """ 161 | Adds liquidity to a specified NFT position. 162 | 163 | Args: 164 | nft_id (int): The identifier of the NFT position. 165 | amount0Desired (int): The desired amount of token0 to add. 166 | amount1Desired (int): The desired amount of token1 to add. 167 | amount0Min (int | None): The minimum amount of token0 that must be added. Defaults to 0. 168 | amount1Min (int | None): The minimum amount of token1 that must be added. Defaults to 0. 169 | deadline (int | None): The Unix timestamp after which the transaction is no longer valid. 170 | Defaults to 60 seconds from the current time. 171 | 172 | Returns: 173 | dict: A receipt of the transaction. 174 | """ 175 | pos = self.contract.positions(nft_id) 176 | token0 = Contract( 177 | pos["token0"], 178 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 179 | ) 180 | token1 = Contract( 181 | pos["token1"], 182 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 183 | ) 184 | # give allowances if needed 185 | if amount0Desired > token0.allowance(self.sender, self.contract.address): 186 | logger.debug( 187 | f"Approving {amount0Desired} tokens for {self.contract.address}" 188 | ) 189 | token0.approve(self.contract.address, amount0Desired, sender=self.sender) 190 | if amount1Desired > token1.allowance(self.sender, self.contract.address): 191 | logger.debug( 192 | f"Approving {amount1Desired} tokens for {self.contract.address}" 193 | ) 194 | token1.approve(self.contract.address, amount1Desired, sender=self.sender) 195 | 196 | if deadline is None: 197 | deadline = int(time.time() + 60) 198 | 199 | logger.debug( 200 | f"Increasing liquidity for position {nft_id} for user {self.sender}" 201 | ) 202 | receipt = self.contract.increaseLiquidity( 203 | (nft_id, amount0Desired, amount1Desired, amount0Min, amount1Min, deadline), 204 | sender=self.sender, 205 | ) 206 | return receipt 207 | 208 | def mint_position( 209 | self, 210 | pool, 211 | lower_price: float, 212 | upper_price: float, 213 | amount0: int, 214 | amount1: int, 215 | amount0Min: int | None = None, 216 | amount1Min: int | None = None, 217 | recipient: str | None = None, 218 | deadline: int | None = None, 219 | slippage_tolerance: float | None = 1, 220 | ): 221 | """ 222 | Mints a new NFT position with specified parameters in a liquidity pool. 223 | 224 | Args: 225 | pool: The liquidity pool in which to mint the position. 226 | lower_price (float): The lower price boundary of the position. 227 | upper_price (float): The upper price boundary of the position. 228 | amount0 (int): The amount of token0 to be used. 229 | amount1 (int): The amount of token1 to be used. 230 | amount0Min (int | None): The minimum amount of token0 that must be added. Defaults to a calculated value based on slippage tolerance. 231 | amount1Min (int | None): The minimum amount of token1 that must be added. Defaults to a calculated value based on slippage tolerance. 232 | recipient (str | None): The recipient of the new position. Defaults to the sender's address if None. 233 | deadline (int | None): The Unix timestamp after which the transaction is no longer valid. 234 | Defaults to 60 seconds from the current time. 235 | slippage_tolerance (float | None): The tolerance for price slippage in percentage. Defaults to 1. 236 | 237 | Returns: 238 | dict: A receipt of the transaction. 239 | """ 240 | fee = pool.fee 241 | token0 = pool.token0 242 | token0_decimals = token0.decimals() 243 | token1 = pool.token1 244 | token1_decimals = token1.decimals() 245 | 246 | lower_tick = price_to_tick(lower_price, token0_decimals, token1_decimals) 247 | upper_tick = price_to_tick(upper_price, token0_decimals, token1_decimals) 248 | lower_tick = nearest_tick(lower_tick, fee) 249 | upper_tick = nearest_tick(upper_tick, fee) 250 | 251 | if lower_tick > upper_tick: 252 | raise ValueError("Lower tick must be less than upper tick") 253 | 254 | sqrtp_cur = pool.get_pool_info()["sqrtPriceX96"] 255 | sqrtp_low = price_to_sqrtp(lower_price) 256 | sqrtp_upp = price_to_sqrtp(upper_price) 257 | liq0 = liquidity0(amount0, sqrtp_cur, sqrtp_upp) 258 | liq1 = liquidity1(amount1, sqrtp_cur, sqrtp_low) 259 | liq = int(min(liq0, liq1)) 260 | amount0 = calc_amount0(liq, sqrtp_upp, sqrtp_cur) 261 | amount1 = calc_amount1(liq, sqrtp_low, sqrtp_cur) 262 | 263 | if amount0 > token0.allowance(self.sender, self.contract.address): 264 | logger.debug(f"Approving {amount0} tokens for {self.contract.address}") 265 | token0.approve(self.contract.address, amount0, sender=self.sender) 266 | if amount1 > token1.allowance(self.sender, self.contract.address): 267 | logger.debug(f"Approving {amount1} tokens for {self.contract.address}") 268 | token1.approve(self.contract.address, amount1, sender=self.sender) 269 | 270 | if recipient is None: 271 | recipient = self.sender 272 | if deadline is None: 273 | deadline = int(time.time() + 60) 274 | if amount0Min is None: 275 | amount0Min = int(amount0 * (1 - slippage_tolerance)) 276 | if amount1Min is None: 277 | amount1Min = int(amount1 * (1 - slippage_tolerance)) 278 | 279 | mint_params = { 280 | "token0": token0.address, 281 | "token1": token1.address, 282 | "fee": fee, 283 | "tickLower": lower_tick, 284 | "tickUpper": upper_tick, 285 | "amount0Desired": amount0, 286 | "amount1Desired": amount1, 287 | "amount0Min": amount0Min, 288 | "amount1Min": amount1Min, 289 | "recipient": recipient, 290 | "deadline": deadline, 291 | } 292 | 293 | logger.debug( 294 | f"Minting LP position with the following parameters: {mint_params}" 295 | ) 296 | 297 | return self.contract.mint(mint_params, sender=self.sender) 298 | -------------------------------------------------------------------------------- /tests/test_agent.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | from ape.exceptions import NetworkError 5 | from giza.cli.schemas.jobs import Job, JobList 6 | from giza.cli.schemas.logs import Logs 7 | from giza.cli.schemas.proofs import Proof 8 | from giza.cli.schemas.verify import VerifyResponse 9 | 10 | from giza.agents import AgentResult, ContractHandler, GizaAgent 11 | 12 | 13 | class EndpointsClientStub: 14 | def __init__(self, *args, **kwargs): 15 | self.args = args 16 | self.kwargs = kwargs 17 | 18 | def list_jobs(self, *args, **kwargs): 19 | return JobList(root=[Job(id=1, size="S", status="COMPLETED", request_id="123")]) 20 | 21 | def get_proof(self, *args, **kwargs): 22 | return Proof( 23 | id=1, job_id=1, created_date="2022-01-01T00:00:00Z", request_id="123" 24 | ) 25 | 26 | def verify_proof(self, *args, **kwargs): 27 | return VerifyResponse( 28 | verification=True, 29 | verification_time=1, 30 | ) 31 | 32 | def get_logs(self, *args, **kwargs): 33 | return Logs(logs="dummy_logs") 34 | 35 | 36 | class JobsClientStub: 37 | def __init__(self, *args, **kwargs): 38 | self.args = args 39 | self.kwargs = kwargs 40 | 41 | def create(self, *args, **kwargs): 42 | return Job(id=1, size="S", status="COMPLETED") 43 | 44 | def get(self, *args, **kwargs): 45 | return Job(id=1, size="S", status="COMPLETED") 46 | 47 | def get_logs(self, *args, **kwargs): 48 | return Logs(logs="dummy_logs") 49 | 50 | 51 | class AccountTestStub: 52 | def __init__(self, *args, **kwargs): 53 | self.args = args 54 | self.kwargs = kwargs 55 | 56 | def load(self, *args, **kwargs): 57 | return Mock() 58 | 59 | 60 | def parser(*args, **kwargs): 61 | return "dummy_network" 62 | 63 | 64 | # TODO: find a way to test the agent better than patching the __init__ method 65 | @patch("giza.agents.agent.GizaAgent._check_or_create_account") 66 | @patch("giza.agents.agent.GizaAgent._retrieve_agent_info") 67 | @patch("giza.agents.agent.GizaAgent._check_passphrase_in_env") 68 | @patch("giza.agents.model.GizaModel.__init__") 69 | def test_agent_init(mock_check, mock_agent, mock_check_passphrase_in_env, mock_init_): 70 | agent = GizaAgent( 71 | id=1, 72 | version_id=1, 73 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"}, 74 | chain="ethereum:sepolia:geth", 75 | account="test", 76 | network_parser=parser, 77 | ) 78 | 79 | assert agent.contract_handler._contracts == { 80 | "contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912" 81 | } 82 | assert agent.chain == "ethereum:sepolia:geth" 83 | assert agent.account == "test" 84 | assert agent._provider == "dummy_network" 85 | 86 | mock_check_passphrase_in_env.assert_called_once() 87 | mock_init_.assert_called_once() 88 | mock_check.assert_called_once() 89 | mock_agent.assert_called_once() 90 | 91 | 92 | @patch("giza.agents.agent.GizaAgent._retrieve_agent_info") 93 | @patch("giza.agents.model.GizaModel.__init__") 94 | def test_agent_init_with_check_succesful_raise(mock_info, mock_init_): 95 | """ 96 | Test the agent init without a passphrase in the environment variables 97 | 98 | Args: 99 | mock_check (_type_): _description_ 100 | mock_info (_type_): _description_ 101 | mock_init_ (_type_): _description_ 102 | """ 103 | with pytest.raises(ValueError): 104 | GizaAgent( 105 | id=1, 106 | version_id=1, 107 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"}, 108 | chain="ethereum:sepolia:geth", 109 | account="test", 110 | network_parser=parser, 111 | ) 112 | 113 | mock_init_.assert_called_once() 114 | mock_info.assert_called_once() 115 | 116 | 117 | @patch("giza.agents.agent.GizaAgent._check_or_create_account") 118 | @patch("giza.agents.agent.GizaAgent._retrieve_agent_info") 119 | @patch("giza.agents.model.GizaModel.__init__") 120 | @patch.dict("os.environ", {"TEST_PASSPHRASE": "test"}) 121 | def test_agent_init_with_check_succesful_check(mock_check, mock_info, mock_init_): 122 | GizaAgent( 123 | id=1, 124 | version_id=1, 125 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"}, 126 | chain="ethereum:sepolia:geth", 127 | account="test", 128 | network_parser=parser, 129 | ) 130 | 131 | mock_init_.assert_called_once() 132 | mock_info.assert_called_once() 133 | mock_check.assert_called_once() 134 | 135 | 136 | # TODO: find a better way, this should be kind of an integration test with ape, using a KeyfileAccount 137 | @patch("giza.agents.agent.GizaAgent._update_agent") 138 | @patch("giza.agents.agent.GizaAgent._check_or_create_account") 139 | @patch("giza.agents.agent.GizaAgent._retrieve_agent_info") 140 | @patch("giza.agents.model.GizaModel.__init__") 141 | @patch.dict("os.environ", {"TEST_PASSPHRASE": "test"}) 142 | @pytest.mark.use_network("ethereum:local:test") 143 | def test_agent_execute( 144 | mock_update: Mock, mock_check: Mock, mock_info: Mock, mock_init_: Mock 145 | ): 146 | agent = GizaAgent( 147 | id=1, 148 | version_id=1, 149 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"}, 150 | chain="ethereum:local:test", 151 | account="test", 152 | ) 153 | with patch("giza.agents.agent.accounts", return_value=AccountTestStub()), patch( 154 | "giza.agents.agent.Contract" 155 | ) as mock_get_contract: 156 | with agent.execute() as contract: 157 | assert contract is not None 158 | 159 | mock_init_.assert_called_once() 160 | mock_info.assert_called_once() 161 | mock_check.assert_called_once() 162 | mock_get_contract.assert_called_once() 163 | mock_update.assert_called_once() 164 | 165 | 166 | @patch("giza.agents.agent.GizaAgent._check_or_create_account") 167 | @patch("giza.agents.agent.GizaAgent._retrieve_agent_info") 168 | @patch("giza.agents.model.GizaModel.__init__") 169 | @patch("giza.agents.model.GizaModel.predict", return_value=([1], "123")) 170 | @patch( 171 | "giza.agents.agent.AgentResult._get_proof_job", 172 | return_value=Job(id=1, size="S", status="COMPLETED"), 173 | ) 174 | @patch.dict("os.environ", {"TEST_PASSPHRASE": "test"}) 175 | def test_agent_predict( 176 | mock_check: Mock, 177 | mock_info: Mock, 178 | mock_init_: Mock, 179 | mock_predict: Mock, 180 | mock_get_proof_job: Mock, 181 | ): 182 | # Mock the endpoint data 183 | GizaAgent.framework = Mock() 184 | GizaAgent.version_id = Mock() 185 | GizaAgent.model_id = Mock() 186 | 187 | agent = GizaAgent( 188 | id=1, 189 | version_id=1, 190 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"}, 191 | chain="ethereum:local:test", 192 | account="test", 193 | ) 194 | agent.endpoint_id = 1 195 | 196 | result = agent.predict(input_feed={"image": [1]}, verifiable=True) 197 | 198 | assert agent.verifiable is True 199 | assert isinstance(result, AgentResult) 200 | mock_get_proof_job.assert_called_once() 201 | mock_predict.assert_called_once() 202 | mock_init_.assert_called_once() 203 | mock_check.assert_called_once() 204 | mock_info.assert_called_once() 205 | 206 | 207 | def test_agentresult_init(): 208 | result = AgentResult( 209 | input=[], 210 | result=[1], 211 | request_id="123", 212 | agent=Mock(), 213 | endpoint_client=EndpointsClientStub(), 214 | ) 215 | assert result.input == [] 216 | assert result.request_id == "123" 217 | assert result._AgentResult__value == [1] 218 | 219 | 220 | @patch("giza.agents.agent.AgentResult._verify", return_value=True) 221 | def test_agentresult_value_already_verified(verify_mock): 222 | result = AgentResult( 223 | input=[], 224 | result=[1], 225 | request_id="123", 226 | agent=Mock(), 227 | endpoint_client=EndpointsClientStub(), 228 | ) 229 | 230 | assert result.value == [1] 231 | verify_mock.assert_called_once() 232 | 233 | 234 | def test_agentresult__get_proof_job(): 235 | result = AgentResult( 236 | input=[], 237 | result=[1], 238 | request_id="123", 239 | agent=Mock(), 240 | endpoint_client=EndpointsClientStub(), 241 | ) 242 | 243 | job = result._get_proof_job(EndpointsClientStub()) 244 | 245 | assert job.id == 1 246 | assert job.size == "S" 247 | assert job.status == "COMPLETED" 248 | assert job.request_id == "123" 249 | 250 | 251 | @patch("giza.agents.agent.AgentResult._wait_for_proof") 252 | @patch("giza.agents.agent.AgentResult._verify_proof", return_value=True) 253 | def test_agentresult__verify(mock_verify, mock_wait_for_proof): 254 | result = AgentResult( 255 | input=[], 256 | result=[1], 257 | request_id="123", 258 | agent=Mock(), 259 | endpoint_client=EndpointsClientStub(), 260 | ) 261 | 262 | result._verify() 263 | 264 | assert result.verified is True 265 | mock_wait_for_proof.assert_called_once() 266 | mock_verify.assert_called_once() 267 | 268 | 269 | @patch("giza.agents.agent.AgentResult._wait_for") 270 | def test_agentresult__wait_for_proof(mock_wait_for): 271 | result = AgentResult( 272 | input=[], 273 | result=[1], 274 | request_id="123", 275 | agent=Mock(), 276 | endpoint_client=EndpointsClientStub(), 277 | ) 278 | 279 | result._wait_for_proof(EndpointsClientStub()) 280 | 281 | assert result._proof.id == 1 282 | assert result._proof.job_id == 1 283 | mock_wait_for.assert_called_once() 284 | 285 | 286 | def test_agentresult__verify_proof(): 287 | result = AgentResult( 288 | input=[], 289 | result=[1], 290 | request_id="123", 291 | agent=Mock(), 292 | endpoint_client=EndpointsClientStub(), 293 | ) 294 | # Add a dummy proof to the result so we can verify it 295 | result._proof = Proof( 296 | id=1, job_id=1, created_date="2022-01-01T00:00:00Z", request_id="123" 297 | ) 298 | 299 | assert result._verify_proof(EndpointsClientStub()) 300 | 301 | 302 | def test_agentresult__wait_for_job_completed(): 303 | result = AgentResult( 304 | input=[], 305 | result=[1], 306 | request_id="123", 307 | agent=Mock(), 308 | endpoint_client=EndpointsClientStub(), 309 | ) 310 | 311 | wait_return = result._wait_for( 312 | job=Job(id=1, size="S", status="COMPLETED"), client=JobsClientStub() 313 | ) 314 | assert wait_return is None 315 | 316 | 317 | def test_agentresult__wait_for_job_failed(): 318 | result = AgentResult( 319 | input=[], 320 | result=[1], 321 | request_id="123", 322 | agent=Mock(), 323 | endpoint_client=EndpointsClientStub(), 324 | ) 325 | 326 | with pytest.raises(ValueError): 327 | result._wait_for( 328 | job=Job(id=1, size="S", status="FAILED"), client=JobsClientStub() 329 | ) 330 | 331 | 332 | def test_agentresult__wait_for_timeout(): 333 | result = AgentResult( 334 | input=[], 335 | result=[1], 336 | request_id="123", 337 | agent=Mock(), 338 | endpoint_client=EndpointsClientStub(), 339 | ) 340 | 341 | with pytest.raises(TimeoutError): 342 | result._wait_for( 343 | job=Job(id=1, size="S", status="PROCESSING"), 344 | client=JobsClientStub(), 345 | timeout=-1, 346 | ) 347 | 348 | 349 | @patch("giza.agents.agent.time.sleep") 350 | def test_agentresult__wait_for_poll_job(mock_sleep): 351 | result = AgentResult( 352 | input=[], 353 | result=[1], 354 | request_id="123", 355 | agent=Mock(), 356 | endpoint_client=EndpointsClientStub(), 357 | ) 358 | 359 | wait_return = result._wait_for( 360 | job=Job(id=1, size="S", status="PROCESSING"), 361 | client=JobsClientStub(), 362 | poll_interval=0.1, 363 | ) 364 | 365 | assert wait_return is None 366 | mock_sleep.assert_called_once_with(0.1) 367 | 368 | 369 | def test_contract_handler_init(): 370 | handler = ContractHandler( 371 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"} 372 | ) 373 | 374 | assert handler._contracts == { 375 | "contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912" 376 | } 377 | 378 | 379 | def test_contract_handler_getattr(): 380 | handler = ContractHandler( 381 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"} 382 | ) 383 | 384 | handler._contracts_instances = {"contract": Mock()} 385 | 386 | handler.contract.execute() 387 | 388 | handler.contract.execute.assert_called_once() 389 | 390 | 391 | @patch("giza.agents.agent.Contract") 392 | def test_contract_handler__initiate_contract(mock_contract): 393 | handler = ContractHandler( 394 | contracts={"contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912"} 395 | ) 396 | 397 | handler._initiate_contract("0x17807a00bE76716B91d5ba1232dd1647c4414912") 398 | 399 | mock_contract.assert_called_with( 400 | address="0x17807a00bE76716B91d5ba1232dd1647c4414912" 401 | ) 402 | 403 | 404 | @patch("giza.agents.agent.Contract") 405 | def test_contract_handler_handle(mock_contract): 406 | handler = ContractHandler( 407 | contracts={ 408 | "contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912", 409 | "contract2": "0x17807a00bE76716B91d5ba1232dd1647c4414912", 410 | } 411 | ) 412 | 413 | result = handler.handle() 414 | 415 | result.contract.test() 416 | result.contract2.test() 417 | 418 | assert isinstance(result, ContractHandler) 419 | mock_contract.assert_called_with( 420 | address="0x17807a00bE76716B91d5ba1232dd1647c4414912" 421 | ) 422 | 423 | 424 | @patch("giza.agents.agent.ContractHandler._initiate_contract", side_effect=NetworkError) 425 | def test_contract_handler_network_error(mock_contract): 426 | handler = ContractHandler( 427 | contracts={ 428 | "contract": "0x17807a00bE76716B91d5ba1232dd1647c4414912", 429 | "contract2": "0x17807a00bE76716B91d5ba1232dd1647c4414912", 430 | } 431 | ) 432 | 433 | with pytest.raises(ValueError): 434 | handler.handle() 435 | -------------------------------------------------------------------------------- /examples/traditional-ml/decision_tree.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Traditional ML Models for ZKML: Decision Tree\n", 8 | "\n", 9 | "*In this series of tutorials, we delve into the world of traditional machine learning models for ZKML. Despite the hype surrounding advanced AI techniques, traditional ML models often offer superior performance or sufficiently robust results for specific applications. This is particularly true for ZKML use cases, where computational proof costs can be a critical factor. We aim to equip you with guides on how to implement machine learning algorithms suitable for Giza platform applications. This includes practical steps for converting your scikit-learn models to the ONNX format, transpiling them to Orion Cairo, and deploying inference endpoints for prediction in AI Action.*\n", 10 | "\n", 11 | "In this tutorial, you will learn how to use the Giza tools through a Decision Tree model." 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## Before Starting\n", 19 | "Before we start, ensure that you have installed the Giza stack, created a user, and logged-in. " 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": { 26 | "vscode": { 27 | "languageId": "shellscript" 28 | } 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "! pipx install giza-cli # Install the Giza-CLI\n", 33 | "! pip install giza-agents\n", 34 | "\n", 35 | "! giza users create # Create a user\n", 36 | "! giza users login # Login to your account\n", 37 | "! giza users create-api-key # Create an API key. We recommend you do this so you don't have to reconnect." 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "## Create and Train a Decision Tree Model\n", 45 | "We'll start by creating a simple decision tree model using Scikit-Learn and train it on the iris dataset. We will then use the [Hummingbirds](https://github.com/microsoft/hummingbird) library to convert the model to torch graphs." 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 2, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "import json\n", 55 | "import numpy as np\n", 56 | "from sklearn.datasets import load_iris\n", 57 | "from sklearn.model_selection import train_test_split\n", 58 | "from sklearn.tree import DecisionTreeClassifier as De\n", 59 | "from hummingbird.ml import convert\n", 60 | "import torch\n", 61 | "import os\n", 62 | "\n", 63 | "\n", 64 | "\n", 65 | "iris = load_iris()\n", 66 | "X, y = iris.data, iris.target\n", 67 | "X = X.astype(np.float32)\n", 68 | "X_train, X_test, y_train, y_test = train_test_split(X, y)\n", 69 | "clr = De()\n", 70 | "clr.fit(X_train, y_train)\n", 71 | "\n", 72 | "model = convert(clr, \"torch\", X_test[:1]).model" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": {}, 78 | "source": [ 79 | "## Convert the Model to ONNX Format\n", 80 | "Giza only supports ONNX models so you'll need to convert the model to ONNX format. This can be done post training." 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 3, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "import torch.onnx\n", 90 | "\n", 91 | "input_sample = torch.from_numpy(X_test[:1])\n", 92 | "\n", 93 | "# Specify the path to save the ONNX model\n", 94 | "onnx_model_path = \"decision_tree.onnx\"\n", 95 | "\n", 96 | "# Export the model\n", 97 | "torch.onnx.export(model,\n", 98 | " input_sample,\n", 99 | " onnx_model_path, # where to save the model\n", 100 | " export_params=True, # store the trained parameter weights inside the model file\n", 101 | " opset_version=17, # the ONNX version to export the model to\n", 102 | " do_constant_folding=True, # whether to execute constant folding for optimization\n", 103 | " input_names=['input'], # the model's input names\n", 104 | " output_names=['output'], # the model's output names\n", 105 | " dynamic_axes={'input': {0: 'batch_size'}, # variable length axes\n", 106 | " 'output': {0: 'batch_size'}})" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "## Transpile your model to Orion Cairo\n", 114 | "\n", 115 | "We will use the Giza-CLI to transpile our ONNX model to Orion Cairo." 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 4, 121 | "metadata": { 122 | "vscode": { 123 | "languageId": "shellscript" 124 | } 125 | }, 126 | "outputs": [ 127 | { 128 | "name": "stdout", 129 | "output_type": "stream", 130 | "text": [ 131 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:03:36\u001b[0m.\u001b[1;36m422\u001b[0m\u001b[1m]\u001b[0m No model id provided, checking if model exists ✅\n", 132 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:03:36\u001b[0m.\u001b[1;36m425\u001b[0m\u001b[1m]\u001b[0m Model name is: decision_tree\n", 133 | "\u001b[2K\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:09\u001b[0m.\u001b[1;36m291\u001b[0m\u001b[1m]\u001b[0m Model already exists, using existing model ✅ \n", 134 | "\u001b[2K\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:09\u001b[0m.\u001b[1;36m292\u001b[0m\u001b[1m]\u001b[0m Model found with id -> \u001b[1;36m146\u001b[0m! ✅\n", 135 | "\u001b[2K\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:09\u001b[0m.\u001b[1;36m951\u001b[0m\u001b[1m]\u001b[0m Version Created with id -> \u001b[1;36m2\u001b[0m! ✅\n", 136 | "\u001b[2K\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:09\u001b[0m.\u001b[1;36m952\u001b[0m\u001b[1m]\u001b[0m Sending model for transpilation ✅ \n", 137 | "\u001b[2K\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:51\u001b[0m.\u001b[1;36m488\u001b[0m\u001b[1m]\u001b[0m Transpilation is fully compatible. Version compiled and Sierra is saved at Giza ✅\n", 138 | "\u001b[2K\u001b[32m⠇\u001b[0m Transpiling Model...\n", 139 | "\u001b[1A\u001b[2K\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:52\u001b[0m.\u001b[1;36m201\u001b[0m\u001b[1m]\u001b[0m Downloading model ✅\n", 140 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:04:52\u001b[0m.\u001b[1;36m241\u001b[0m\u001b[1m]\u001b[0m model saved at: verifiable_dt\n" 141 | ] 142 | } 143 | ], 144 | "source": [ 145 | "! giza transpile decision_tree.onnx --output-path verifiable_dt" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "## Deploy an inference endpoint\n", 153 | "\n", 154 | "Now that our model is transpiled to Cairo we can deploy an endpoint to run verifiable inferences. We will use Giza CLI again to run and deploy an endpoint.\n", 155 | "Ensure to replace `model-id` and `version-id` with your ids provided during transpilation." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 5, 161 | "metadata": { 162 | "vscode": { 163 | "languageId": "shellscript" 164 | } 165 | }, 166 | "outputs": [ 167 | { 168 | "name": "stdout", 169 | "output_type": "stream", 170 | "text": [ 171 | "\u001b[2K▰▰▱▱▱▱▱ Creating endpoint!t!\n", 172 | "\u001b[?25h\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:05:20\u001b[0m.\u001b[1;36m152\u001b[0m\u001b[1m]\u001b[0m Endpoint is successful ✅\n", 173 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:05:20\u001b[0m.\u001b[1;36m156\u001b[0m\u001b[1m]\u001b[0m Endpoint created with id -> \u001b[1;36m36\u001b[0m ✅\n", 174 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m03\u001b[0m \u001b[1;92m16:05:20\u001b[0m.\u001b[1;36m156\u001b[0m\u001b[1m]\u001b[0m Endpoint created with endpoint URL: \u001b[4;94mhttps://endpoint-raphael-doukhan-146-2-98feddf7-6nn4ryaqca-ew.a.run.app\u001b[0m 🎉\n" 175 | ] 176 | } 177 | ], 178 | "source": [ 179 | "! giza endpoints deploy --model-id 146 --version-id 2" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "## Run a verifiable inference in AI Agents\n", 187 | "\n", 188 | "To streamline a verifiable inference, you might consider using the endpoint URL obtained after transpilation. However, this approach requires manual serialization of the input for the Cairo program and handling the deserialization process. To make this process more user-friendly and keep you within a Python environment, we've introduced giza-agents. A Python package designed to facilitate the creation of ML workflows and execution of verifiable predictions. When you initiate a prediction, our system automatically retrieves the endpoint URL you deployed earlier, converts your input into Cairo-compatible format, executes the prediction, and then converts the output back into a numpy object. More info about [AI Agents here.](https://docs.gizatech.xyz/products/ai-agents)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "Now let's run a verifiable inference with AI Agents" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "from giza.agents.model import GizaModel\n", 205 | "\n", 206 | "MODEL_ID = 146 # Update with your model ID\n", 207 | "VERSION_ID = 2 # Update with your version ID\n", 208 | "\n", 209 | "\n", 210 | "def prediction(input, model_id, version_id):\n", 211 | " model = GizaModel(id=model_id, version=version_id)\n", 212 | "\n", 213 | " (result, proof_id) = model.predict(\n", 214 | " input_feed={'input': input}, \n", 215 | " verifiable=True,\n", 216 | " custom_output_dtype=\"(Tensor, Tensor)\" # Decision Tree will always have this output dtype.\n", 217 | " )\n", 218 | "\n", 219 | " return result, proof_id\n", 220 | "\n", 221 | "\n", 222 | "def execution():\n", 223 | " # The input data type should match the model's expected input\n", 224 | " input = input_sample.numpy()\n", 225 | "\n", 226 | " (result, proof_id) = prediction(input, MODEL_ID, VERSION_ID)\n", 227 | "\n", 228 | " return result, proof_id\n", 229 | "\n", 230 | "\n", 231 | "execution()" 232 | ] 233 | }, 234 | { 235 | "cell_type": "markdown", 236 | "metadata": {}, 237 | "source": [ 238 | "## Download the proof\n", 239 | "\n", 240 | "Initiating a verifiable inference sets off a proving job on our server, sparing you the complexities of installing and configuring the prover yourself. Upon completion, you can download your proof." 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "First, let's check the status of the proving job to ensure that it has been completed. \n", 248 | "\n", 249 | "🚨 Remember to substitute `endpoint-id` and `proof-id` with the specific IDs assigned to you throughout this tutorial." 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": 15, 255 | "metadata": { 256 | "vscode": { 257 | "languageId": "shellscript" 258 | } 259 | }, 260 | "outputs": [ 261 | { 262 | "name": "stdout", 263 | "output_type": "stream", 264 | "text": [ 265 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m03\u001b[0m-\u001b[1;36m19\u001b[0m \u001b[1;92m11:51:45\u001b[0m.\u001b[1;36m470\u001b[0m\u001b[1m]\u001b[0m Getting proof from endpoint \u001b[1;36m109\u001b[0m ✅ \n", 266 | "\u001b[1m{\u001b[0m\n", 267 | " \u001b[1;34m\"id\"\u001b[0m: \u001b[1;36m664\u001b[0m,\n", 268 | " \u001b[1;34m\"job_id\"\u001b[0m: \u001b[1;36m831\u001b[0m,\n", 269 | " \u001b[1;34m\"metrics\"\u001b[0m: \u001b[1m{\u001b[0m\n", 270 | " \u001b[1;34m\"proving_time\"\u001b[0m: \u001b[1;36m15.083126\u001b[0m\n", 271 | " \u001b[1m}\u001b[0m,\n", 272 | " \u001b[1;34m\"created_date\"\u001b[0m: \u001b[32m\"2024-03-19T10:41:11.120310\"\u001b[0m\n", 273 | "\u001b[1m}\u001b[0m\n" 274 | ] 275 | } 276 | ], 277 | "source": [ 278 | "! giza endpoints get-proof --endpoint-id 36 --proof-id \"3bb53193c43048b7b47abfefe32b569a\"" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "metadata": {}, 284 | "source": [ 285 | "Once the proof is ready, you can download it." 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 17, 291 | "metadata": { 292 | "vscode": { 293 | "languageId": "shellscript" 294 | } 295 | }, 296 | "outputs": [ 297 | { 298 | "name": "stdout", 299 | "output_type": "stream", 300 | "text": [ 301 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m03\u001b[0m-\u001b[1;36m19\u001b[0m \u001b[1;92m11:55:49\u001b[0m.\u001b[1;36m713\u001b[0m\u001b[1m]\u001b[0m Getting proof from endpoint \u001b[1;36m109\u001b[0m ✅ \n", 302 | "\u001b[1;33m[\u001b[0m\u001b[33mgiza\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1m[\u001b[0m\u001b[1;36m2024\u001b[0m-\u001b[1;36m03\u001b[0m-\u001b[1;36m19\u001b[0m \u001b[1;92m11:55:50\u001b[0m.\u001b[1;36m493\u001b[0m\u001b[1m]\u001b[0m Proof downloaded to zklr.proof ✅ \n" 303 | ] 304 | } 305 | ], 306 | "source": [ 307 | "! giza endpoints download-proof --endpoint-id 36 --proof-id \"3bb53193c43048b7b47abfefe32b569a\" --output-path zkdt.proof" 308 | ] 309 | } 310 | ], 311 | "metadata": { 312 | "kernelspec": { 313 | "display_name": "giza-agents-mYf3m_Lk-py3.11", 314 | "language": "python", 315 | "name": "python3" 316 | }, 317 | "language_info": { 318 | "codemirror_mode": { 319 | "name": "ipython", 320 | "version": 3 321 | }, 322 | "file_extension": ".py", 323 | "mimetype": "text/x-python", 324 | "name": "python", 325 | "nbconvert_exporter": "python", 326 | "pygments_lexer": "ipython3", 327 | "version": "3.11.6" 328 | } 329 | }, 330 | "nbformat": 4, 331 | "nbformat_minor": 2 332 | } 333 | -------------------------------------------------------------------------------- /examples/agents/mnist_agent_tutorial.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Create an AI Agent to Mint a MNIST NFT\n", 8 | "\n", 9 | "Agents are entities designed to assist users in interacting with Smart Contracts by managing the proof verification of verifiable ML models and executing these contracts using [Ape's framework](https://apeworx.io/framework/).\n", 10 | "\n", 11 | "Agents serve as intermediaries between users and Smart Contracts, facilitating seamless interaction with verifiable ML models and executing associated contracts. They handle the verification of proofs, ensuring the integrity and authenticity of data used in contract execution.\n", 12 | "\n", 13 | "In this notebook, we will create an AI Agent to mint an MNIST NFT. Using an existing MNIST endpoint, we will perform a prediction on the MNIST dataset and mint an NFT based on the prediction.\n", 14 | "\n", 15 | "This tutorial can be thought of as the continuation of the previous tutorial [Verifiable MNIST Neural Network](https://docs.gizatech.xyz/tutorials/zkml/verifiable-mnist-neural-network) where we created a verifiable MNIST model ([notebook](https://github.com/gizatechxyz/giza-agents/blob/main/examples/verifiable_mnist/verifiable_mnist.ipynb)).\n", 16 | "\n", 17 | "## Before you begin\n", 18 | "\n", 19 | "1. Python 3.11 or later must be installed on your machine\n", 20 | "2. Giza CLI must be installed on your machine. You can install it by running `pip install giza-cli`\n", 21 | "3. giza-agents should be installed. You can install it by running `pip install giza-agents`\n", 22 | "4. You must have an active Giza account. If you don't have one, you can create one [here](https://cli.gizatech.xyz/examples/basic).\n", 23 | "5. You must have an MNIST model deployed on Giza. You can follow the tutorial [Build a Verifiable Neural Network](https://docs.gizatech.xyz/tutorials/zkml/verifiable-mnist-neural-network) to deploy an MNIST model on Giza.\n", 24 | "6. During the tutorial, you will need to interact with the Giza CLI, and Ape's framework, and provide multiple inputs, like `model-id`, `version-id`, `account name`, etc.\n", 25 | "7. You must be logged in to the Giza CLI or have an API KEY.\n", 26 | "\n", 27 | "## Installing the required libraries\n", 28 | "\n", 29 | "Let's start by installing the required libraries.\n", 30 | "\n", 31 | "```bash\n", 32 | "pip install giza-agents\n", 33 | "```\n", 34 | "\n", 35 | "This will install the `giza-agents` library, which contains the necessary tools to create an AI Agent.\n", 36 | "\n", 37 | "## Creating an account\n", 38 | "\n", 39 | "Before we can create an AI Agent, we need to create an account using Ape's framework. We can do this by running the following command:\n", 40 | "\n", 41 | "```bash\n", 42 | "$ ape accounts generate \n", 43 | "Enhance the security of your account by adding additional random input:\n", 44 | "Show mnemonic? [Y/n]: n\n", 45 | "Create Passphrase to encrypt account:\n", 46 | "Repeat for confirmation:\n", 47 | "SUCCESS: A new account '0x766867bB2E3E1A6E6245F4930b47E9aF54cEba0C' with HDPath m/44'/60'/0'/0/0 has been added with the id ''\n", 48 | "```\n", 49 | "\n", 50 | "This will create a new account under `$HOME/.ape/accounts` using the keyfile structure from the [eth-keyfile library](https://github.com/ethereum/eth-keyfile)\n", 51 | ". For more information on account management, you can refer to the [Ape's framework documentation](https://docs.apeworx.io/ape/stable/userguides/accounts.html#keyfile-accounts).\n", 52 | "\n", 53 | "During account generation we will be prompted to enter a passphrase to encrypt the account, which will be used to unlock the said account when needed, so **make sure to keep it safe**.\n", 54 | "\n", 55 | "We encourage the creation of a new account for each agent, as it will allow you to manage the agent's permissions and access control more effectively, but importing accounts is also possible.\n", 56 | "\n", 57 | "## Funding the account\n", 58 | "\n", 59 | "Before we can create an AI Agent, we need to fund the account with some ETH. You can do this by sending some ETH to the account address generated in the previous step.\n", 60 | "\n", 61 | "In this case, we will use Sepolia testnet, you can get some testnet ETH from a faucet like [Alchemy Sepolia Faucet](https://www.alchemy.com/faucets/ethereum-sepolia) or [LearnWeb3 Faucet](https://learnweb3.io/faucets/sepolia/). These faucets will ask for security measures to make sure that you are not a bot (by checking whether you have a specific amount of ETH in the mainnet or a GitHub account, for example). There are many faucets available and you are free to choose the one that suits you the best.\n", 62 | "\n", 63 | "Once the testnet ETH have been received and the account has been funded, we can proceed with the creation of an AI Agent.\n", 64 | "\n", 65 | "![account](img/account.png)\n", 66 | "\n", 67 | "## Creating an AI Agent\n", 68 | "\n", 69 | "Now that we have a funded account, we can create an AI Agent. This can be done by running the following command:\n", 70 | "\n", 71 | "```bash\n", 72 | "giza agents create --model-id --version-id --name --description \n", 73 | "\n", 74 | "# or if you have the endpoint-id\n", 75 | "\n", 76 | "giza agents create --endpoint-id --name --description \n", 77 | "```\n", 78 | "\n", 79 | "This command will prompt you to choose the account you want to use with the agent. Once you select the account, the agent will be created and you will receive the agent id. The output will look like this:\n", 80 | "\n", 81 | "```console\n", 82 | "[giza][2024-04-10 11:50:24.005] Creating agent ✅\n", 83 | "[giza][2024-04-10 11:50:24.006] Using endpoint id to create agent, retrieving model id and version id\n", 84 | "[giza][2024-04-10 11:50:53.480] Select an existing account to create the agent.\n", 85 | "[giza][2024-04-10 11:50:53.480] Available accounts are:\n", 86 | "┏━━━━━━━━━━━━━┓\n", 87 | "┃ Accounts ┃\n", 88 | "┡━━━━━━━━━━━━━┩\n", 89 | "│ my_account │\n", 90 | "└─────────────┘\n", 91 | "Enter the account name: my_account\n", 92 | "{\n", 93 | " \"id\": 1,\n", 94 | " \"name\": ,\n", 95 | " \"description\": ,\n", 96 | " \"parameters\": {\n", 97 | " \"model_id\": ,\n", 98 | " \"version_id\": ,\n", 99 | " \"endpoint_id\": ,\n", 100 | " \"alias\": \"my_account\"\n", 101 | " },\n", 102 | " \"created_date\": \"2024-04-10T09:51:04.226448\",\n", 103 | " \"last_update\": \"2024-04-10T09:51:04.226448\"\n", 104 | "}\n", 105 | "```\n", 106 | "\n", 107 | "This will create an AI Agent that can be used to interact with the deployed MNIST model.\n", 108 | "\n", 109 | "## How to use the AI Agent\n", 110 | "\n", 111 | "Now that the agent is created we can start using it through the `giza-agents` library. As we will be using the agent to mint an MNIST NFT, we will need to provide the MNIST image to the agent and preprocess it before sending it to the model." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "import pprint\n", 121 | "import numpy as np\n", 122 | "from PIL import Image\n", 123 | "\n", 124 | "from giza.agents import GizaAgent, AgentResult\n", 125 | "\n", 126 | "# Make sure to fill these in\n", 127 | "MODEL_ID = ...\n", 128 | "VERSION_ID = ...\n", 129 | "# As we are executing in sepolia, we need to specify the chain\n", 130 | "CHAIN = \"ethereum:sepolia:geth\"\n", 131 | "# The address of the deployed contract\n", 132 | "MNIST_CONTRACT = \"0x17807a00bE76716B91d5ba1232dd1647c4414912\"\n" 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": {}, 138 | "source": [ 139 | "Now we will start creating the functions that are necessary to create an Action that will:\n", 140 | "\n", 141 | "* Load and preprocess the MNIST image\n", 142 | "* Create an instance of an agent\n", 143 | "* Predict the MNIST image using the agent\n", 144 | "* Access the prediction result\n", 145 | "* Mint an NFT based on the prediction result\n", 146 | "\n", 147 | "To load and preprocess the image, we can use the function that we already created in the previous tutorial [Build a Verifiable Neural Network](https://docs.gizatech.xyz/tutorials/zkml/verifiable-mnist-neural-network)." 148 | ] 149 | }, 150 | { 151 | "cell_type": "code", 152 | "execution_count": null, 153 | "metadata": {}, 154 | "outputs": [], 155 | "source": [ 156 | "# This function is for the previous MNIST tutorial\n", 157 | "def preprocess_image(image_path: str):\n", 158 | " \"\"\"\n", 159 | " Preprocess an image for the MNIST model.\n", 160 | "\n", 161 | " Args:\n", 162 | " image_path (str): Path to the image file.\n", 163 | "\n", 164 | " Returns:\n", 165 | " np.ndarray: Preprocessed image.\n", 166 | " \"\"\"\n", 167 | "\n", 168 | " # Load image, convert to grayscale, resize and normalize\n", 169 | " image = Image.open(image_path).convert('L')\n", 170 | " # Resize to match the input size of the model\n", 171 | " image = image.resize((14, 14))\n", 172 | " image = np.array(image).astype('float32') / 255\n", 173 | " image = image.reshape(1, 196) # Reshape to (1, 196) for model input\n", 174 | " return image" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "Now, we will create a function to create an instance of the agent. The agent is an extension of the `GizaModel` class, so we can execute `predict` as if we were using a model, but the agents needs more information:\n", 182 | "\n", 183 | "* chain: The chain where the contract and account are deployed\n", 184 | "* contracts: This is a dictionary in the form of `{\"contract_alias\": \"contract_address\"}` that contains the contract alias and address.\n", 185 | "\n", 186 | "This `contract_alias` is the alias that we will use when executing the contract through code.\n", 187 | "\n", 188 | "We create an instance so we can use it through the action." 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "def create_agent(model_id: int, version_id: int, chain: str, contract: str):\n", 198 | " \"\"\"\n", 199 | " Create a Giza agent for the MNIST model with MNIST \n", 200 | " \"\"\"\n", 201 | " agent = GizaAgent(\n", 202 | " contracts={\"mnist\": contract},\n", 203 | " id=model_id,\n", 204 | " version_id=version_id,\n", 205 | " chain=chain,\n", 206 | " account=\"sepolia\"\n", 207 | " )\n", 208 | " return agent" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": {}, 214 | "source": [ 215 | "This new `task` will execute the `predict` method of the agent following the same format as a `GizaModel` instance. But in this case, the agent will return an `AgentResult` object that contains the prediction result, request id and multiple utilities to handle the verification of the prediction." 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": null, 221 | "metadata": {}, 222 | "outputs": [], 223 | "source": [ 224 | "def predict(agent: GizaAgent, image: np.ndarray):\n", 225 | " \"\"\"\n", 226 | " Predict the digit in an image.\n", 227 | "\n", 228 | " Args:\n", 229 | " image (np.ndarray): Image to predict.\n", 230 | "\n", 231 | " Returns:\n", 232 | " int: Predicted digit.\n", 233 | " \"\"\"\n", 234 | " prediction = agent.predict(\n", 235 | " input_feed={\"image\": image}, verifiable=True\n", 236 | " )\n", 237 | " return prediction" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "Once we have the result, we need to access it. The `AgentResult` object contains the `value` attribute that contains the prediction result. In this case, the prediction result is a number from 0 to 9 that represents the digit that the model predicted.\n", 245 | "\n", 246 | "When we access the value, the execution will be blocked until the proof of the prediction has been created and verified. If the proof is not valid, the execution will raise an exception.\n", 247 | "\n", 248 | "This task is more for didactic purposes, to showcase in the workspaces the time that it takes to verify the proof. In a real scenario, you can use the `AgentResult` value directly in any other task." 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "def get_digit(prediction: AgentResult):\n", 258 | " \"\"\"\n", 259 | " Get the digit from the prediction.\n", 260 | "\n", 261 | " Args:\n", 262 | " prediction (dict): Prediction from the model.\n", 263 | "\n", 264 | " Returns:\n", 265 | " int: Predicted digit.\n", 266 | " \"\"\"\n", 267 | " # This will block the executon until the prediction has generated the proof and the proof has been verified\n", 268 | " return int(prediction.value[0].argmax())" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "Finally, we will create a task to mint an NFT based on the predicted result. This task will use the result to mint an NFT with the image of the MNIST digit predicted.\n", 276 | "\n", 277 | "To execute the contract, we have the `GizaAgent.execute` context that will yield all the initialized contracts and the agent instance. This context will be used to execute the contract as it handles all the connections to the nodes, networks and contracts.\n", 278 | "\n", 279 | "In our instantiation of the agent, we added an `mnist` alias to access the contract to ease its use. For example:\n", 280 | "\n", 281 | "```python\n", 282 | "# A context is created that yields all the contracts used in the agent\n", 283 | "with agent.execute() as contracts:\n", 284 | " # This is how we access the contract\n", 285 | " contracs.mnist\n", 286 | " # This is how we access the functions of the contract\n", 287 | " contracts.mnsit.mint(...)\n", 288 | "```" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": null, 294 | "metadata": {}, 295 | "outputs": [], 296 | "source": [ 297 | "def execute_contract(agent: GizaAgent, digit: int):\n", 298 | " \"\"\"\n", 299 | " Execute the MNIST contract with the predicted digit to mint a new NFT.\n", 300 | "\n", 301 | " Args:\n", 302 | " agent (GizaAgent): Giza agent.\n", 303 | " digit (int): Predicted digit.\n", 304 | "\n", 305 | " Returns:\n", 306 | " str: Transaction hash.\n", 307 | " \"\"\"\n", 308 | " with agent.execute() as contracts:\n", 309 | " contract_result = contracts.mnist.mint(digit)\n", 310 | " return contract_result" 311 | ] 312 | }, 313 | { 314 | "cell_type": "markdown", 315 | "metadata": {}, 316 | "source": [ 317 | "Now that we have all the steps defined, we can create the action to execute" 318 | ] 319 | }, 320 | { 321 | "cell_type": "code", 322 | "execution_count": null, 323 | "metadata": {}, 324 | "outputs": [], 325 | "source": [ 326 | "def mint_nft_with_prediction():\n", 327 | " # Preprocess the image\n", 328 | " image = preprocess_image(\"seven.png\")\n", 329 | " # Create the Giza agent\n", 330 | " agent = create_agent(MODEL_ID, VERSION_ID, CHAIN, MNIST_CONTRACT)\n", 331 | " # Predict the digit\n", 332 | " prediction = predict(agent, image)\n", 333 | " # Get the digit\n", 334 | " digit = get_digit(prediction)\n", 335 | " # Execute the contract\n", 336 | " result = execute_contract(agent, digit)\n", 337 | " pprint.pprint(result)\n" 338 | ] 339 | }, 340 | { 341 | "cell_type": "markdown", 342 | "metadata": {}, 343 | "source": [ 344 | "Remember the passphrase that had to be safely kept? It is now time to use it. For the purpose of learning, we will use the passphrase in the code as an example, but in a real scenario, you should keep it safe and not hardcode it." 345 | ] 346 | }, 347 | { 348 | "cell_type": "code", 349 | "execution_count": null, 350 | "metadata": {}, 351 | "outputs": [], 352 | "source": [ 353 | "# DO NOT COMMIT YOUR PASSPHRASE\n", 354 | "import os\n", 355 | "os.environ['MY_ACCOUNT1_PASSPHRASE'] = 'a'\n" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "Now let's execute the action." 363 | ] 364 | }, 365 | { 366 | "cell_type": "code", 367 | "execution_count": null, 368 | "metadata": {}, 369 | "outputs": [], 370 | "source": [ 371 | "mint_nft_with_prediction()" 372 | ] 373 | }, 374 | { 375 | "cell_type": "markdown", 376 | "metadata": {}, 377 | "source": [ 378 | "## What we have learned\n", 379 | "\n", 380 | "In this tutorial, we learned how to create an AI Agent to mint an MNIST NFT. We created an AI Agent using Ape's framework and interacted with it to mint an NFT based on the predicted result of an MNIST image.\n", 381 | "\n", 382 | "We learned how to load and preprocess the MNIST image, create an instance of the agent, predict the MNIST image using the agent, access the predicted result, and mint an NFT based on the result." 383 | ] 384 | } 385 | ], 386 | "metadata": { 387 | "kernelspec": { 388 | "display_name": ".venv", 389 | "language": "python", 390 | "name": "python3" 391 | }, 392 | "language_info": { 393 | "codemirror_mode": { 394 | "name": "ipython", 395 | "version": 3 396 | }, 397 | "file_extension": ".py", 398 | "mimetype": "text/x-python", 399 | "name": "python", 400 | "nbconvert_exporter": "python", 401 | "pygments_lexer": "ipython3", 402 | "version": "3.11.8" 403 | } 404 | }, 405 | "nbformat": 4, 406 | "nbformat_minor": 2 407 | } 408 | -------------------------------------------------------------------------------- /giza/agents/integrations/uniswap/uniswap.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ape import Contract, chain 4 | 5 | from giza.agents.integrations.uniswap.constants import ADDRESSES, MAX_UINT_128 6 | from giza.agents.integrations.uniswap.nft_manager import NFTManager 7 | from giza.agents.integrations.uniswap.pool import Pool 8 | from giza.agents.integrations.uniswap.pool_factory import PoolFactory 9 | from giza.agents.integrations.uniswap.quoter import Quoter 10 | from giza.agents.integrations.uniswap.router import Router 11 | 12 | 13 | class Uniswap: 14 | """ 15 | A class to interact with the Uniswap protocol, supporting various operations such as 16 | liquidity management, swapping, and quoting for different versions of the protocol. 17 | 18 | Attributes: 19 | sender (str): The address initiating transactions. 20 | version (int): The version of the Uniswap protocol to interact with. 21 | _chain_id (int): The chain ID of the current blockchain. 22 | pool_factory (PoolFactory): The factory class for pool-related operations. 23 | router (Router): The router class for swap-related operations. 24 | quoter (Quoter): The quoter class for price quote-related operations. 25 | nft_manager (NFTManager): The manager class for non-fungible token positions. 26 | """ 27 | 28 | def __init__(self, sender: str, version: int = 3): 29 | """ 30 | Initializes the Uniswap class with a sender address and protocol version. 31 | 32 | Args: 33 | sender (str): The address that will be used as the sender for transactions. 34 | version (int, optional): The version of the Uniswap protocol. Defaults to 3. 35 | """ 36 | self.sender = sender 37 | self._chain_id = chain.chain_id 38 | self.version = version 39 | self._load_contracts() 40 | 41 | def _load_contracts(self): 42 | """ 43 | Loads the necessary contracts based on the Uniswap version specified. 44 | Raises NotImplementedError if the version is not supported. 45 | """ 46 | if self.version == 2: 47 | # TODO 48 | pass 49 | elif self.version == 3: 50 | self.pool_factory = PoolFactory( 51 | ADDRESSES[self._chain_id][self.version]["PoolFactory"], 52 | sender=self.sender, 53 | ) 54 | self.router = Router( 55 | ADDRESSES[self._chain_id][self.version]["Router"], sender=self.sender 56 | ) 57 | self.quoter = Quoter( 58 | ADDRESSES[self._chain_id][self.version]["Quoter"], sender=self.sender 59 | ) 60 | self.nft_manager = NFTManager( 61 | ADDRESSES[self._chain_id][self.version]["NonfungiblePositionManager"], 62 | sender=self.sender, 63 | ) 64 | else: 65 | raise NotImplementedError( 66 | "Uniswap version {} not supported".format(self.version) 67 | ) 68 | 69 | def get_pool(self, token0: str, token1: str, fee: int): 70 | """ 71 | Retrieves a pool based on the provided token addresses and fee tier. 72 | 73 | Args: 74 | token0 (str): The address of the first token. 75 | token1 (str): The address of the second token. 76 | fee (int): The fee tier of the pool. 77 | 78 | Returns: 79 | Pool: The pool object corresponding to the specified tokens and fee. 80 | """ 81 | return self.pool_factory.get_pool(token0, token1, fee) 82 | 83 | def create_pool(self, token0: str, token1: str, fee: int): 84 | """ 85 | Creates a new liquidity pool for the specified token pair and fee tier. 86 | 87 | Args: 88 | token0 (str): The address of the first token. 89 | token1 (str): The address of the second token. 90 | fee (int): The fee tier for the new pool. 91 | 92 | Returns: 93 | Pool: The newly created pool object. 94 | """ 95 | return self.pool_factory.create_pool(token0, token1, fee) 96 | 97 | def get_all_user_positions(self, user_address: str | None = None): 98 | """ 99 | Retrieves all positions for a given user address. 100 | 101 | Args: 102 | user_address (str | None): The address of the user. If None, uses the sender's address. 103 | 104 | Returns: 105 | list: A list of all positions held by the user. 106 | """ 107 | return self.nft_manager.get_all_user_positions(user_address=user_address) 108 | 109 | def get_pos_info(self, nft_id: int, block_number: str | None = None): 110 | """ 111 | Retrieves position information for a specific NFT ID, optionally at a specific block number. 112 | 113 | Args: 114 | nft_id (int): The NFT ID of the position. 115 | block_number (str | None): The block number at which to retrieve the position info. If None, uses the latest block. 116 | 117 | Returns: 118 | dict: The position information. 119 | """ 120 | if block_number is None: 121 | return self.nft_manager.contract.positions(nft_id) 122 | else: 123 | return self.nft_manager.contract.positions( 124 | nft_id, block_identifier=block_number 125 | ) 126 | 127 | def close_position(self, nft_id: int, user_address: str | None = None): 128 | """ 129 | Closes a position for a given NFT ID, optionally for a specific user address. 130 | 131 | Args: 132 | nft_id (int): The NFT ID of the position to close. 133 | user_address (str | None): The address of the user. If None, uses the sender's address. 134 | 135 | Returns: 136 | bool: True if the position was successfully closed, False otherwise. 137 | """ 138 | return self.nft_manager.close_position(nft_id, user_address=user_address) 139 | 140 | def collect_fees( 141 | self, 142 | nft_id: int, 143 | user_address: str | None = None, 144 | amount0_max: int | None = MAX_UINT_128, 145 | amount1_max: int | None = MAX_UINT_128, 146 | ): 147 | """ 148 | Collects accumulated fees for a given position. 149 | 150 | Args: 151 | nft_id (int): The NFT ID of the position. 152 | user_address (str | None): The address of the user. If None, uses the sender's address. 153 | amount0_max (int | None): The maximum amount of token0 to collect. Defaults to MAX_UINT_128. 154 | amount1_max (int | None): The maximum amount of token1 to collect. Defaults to MAX_UINT_128. 155 | 156 | Returns: 157 | dict: The amounts of token0 and token1 collected. 158 | """ 159 | return self.nft_manager.collect_fees( 160 | nft_id, 161 | user_address=user_address, 162 | amount0_max=amount0_max, 163 | amount1_max=amount1_max, 164 | ) 165 | 166 | def decrease_liquidity( 167 | self, 168 | nft_id: int, 169 | liquidity: int | None = None, 170 | amount0Min: int | None = 0, 171 | amount1Min: int | None = 0, 172 | deadline: int | None = None, 173 | ): 174 | """ 175 | Decreases the liquidity of a given position. 176 | 177 | Args: 178 | nft_id (int): The NFT ID of the position. 179 | liquidity (int | None): The amount of liquidity to remove. If None, removes all liquidity. 180 | amount0Min (int | None): The minimum amount of token0 that must be returned. Defaults to 0. 181 | amount1Min (int | None): The minimum amount of token1 that must be returned. Defaults to 0. 182 | deadline (int | None): The deadline by which the transaction must be confirmed. If None, no deadline is set. 183 | 184 | Returns: 185 | dict: The amounts of token0 and token1 returned after decreasing liquidity. 186 | """ 187 | return self.nft_manager.decrease_liquidity( 188 | nft_id, 189 | liquidity=liquidity, 190 | amount0Min=amount0Min, 191 | amount1Min=amount1Min, 192 | deadline=deadline, 193 | ) 194 | 195 | def add_liquidity( 196 | self, 197 | nft_id: int, 198 | amount0_desired: int, 199 | amount1_desired: int, 200 | amount0Min: int | None = 0, 201 | amount1Min: int | None = 0, 202 | deadline: int | None = None, 203 | ): 204 | """ 205 | Adds liquidity to a given position. 206 | 207 | Args: 208 | nft_id (int): The NFT ID of the position. 209 | amount0_desired (int): The desired amount of token0 to add. 210 | amount1_desired (int): The desired amount of token1 to add. 211 | amount0Min (int | None): The minimum amount of token0 that must be added. Defaults to 0. 212 | amount1Min (int | None): The minimum amount of token1 that must be added. Defaults to 0. 213 | deadline (int | None): The deadline by which the transaction must be confirmed. If None, no deadline is set. 214 | 215 | Returns: 216 | dict: The actual amounts of token0 and token1 added to the position. 217 | """ 218 | return self.nft_manager.add_liquidity( 219 | nft_id, 220 | amount0_desired, 221 | amount1_desired, 222 | amount0Min=amount0Min, 223 | amount1Min=amount1Min, 224 | deadline=deadline, 225 | ) 226 | 227 | def mint_position( 228 | self, 229 | pool: Pool, 230 | lower_price: float, 231 | upper_price: float, 232 | amount0: int, 233 | amount1: int, 234 | amount0Min: int | None = None, 235 | amount1Min: int | None = None, 236 | recipient: str | None = None, 237 | deadline: int | None = None, 238 | slippage_tolerance: float | None = 1, 239 | ): 240 | """ 241 | Mints a new position or increases liquidity in an existing position within specified price bounds. 242 | 243 | Args: 244 | pool (Pool): The pool in which to mint the position. 245 | lower_price (float): The lower price bound of the position. 246 | upper_price (float): The upper price bound of the position. 247 | amount0 (int): The amount of token0 to be added. 248 | amount1 (int): The amount of token1 to be added. 249 | amount0Min (int | None): The minimum amount of token0 that must be added. Defaults to 0. 250 | amount1Min (int | None): The minimum amount of token1 that must be added. Defaults to 0. 251 | recipient (str | None): The recipient of the position. If None, defaults to the sender. 252 | deadline (int | None): The deadline by which the transaction must be confirmed. If None, no deadline is set. 253 | slippage_tolerance (float | None): The maximum acceptable slippage percentage. Defaults to 1. 254 | 255 | Returns: 256 | dict: Details of the minted position. 257 | """ 258 | self.nft_manager.mint_position( 259 | pool, 260 | lower_price=lower_price, 261 | upper_price=upper_price, 262 | amount0=amount0, 263 | amount1=amount1, 264 | amount0Min=amount0Min, 265 | amount1Min=amount1Min, 266 | recipient=recipient, 267 | deadline=deadline, 268 | slippage_tolerance=slippage_tolerance, 269 | ) 270 | 271 | def rebalance_lp( 272 | self, 273 | nft_id: int, 274 | lower_price: float, 275 | upper_price: float, 276 | amount0Min: int | None = None, 277 | amount1Min: int | None = None, 278 | recipient: str | None = None, 279 | deadline: int | None = None, 280 | slippage_tolerance: float | None = 1, 281 | ): 282 | """ 283 | Rebalances a liquidity position by closing the current position and minting a new one with updated parameters. 284 | 285 | Args: 286 | nft_id (int): The NFT ID of the position to rebalance. 287 | lower_price (float): The new lower price bound for the position. 288 | upper_price (float): The new upper price bound for the position. 289 | amount0Min (int | None): The minimum amount of token0 that must be returned from the closed position. Defaults to 0. 290 | amount1Min (int | None): The minimum amount of token1 that must be returned from the closed position. Defaults to 0. 291 | recipient (str | None): The recipient of the new position. If None, defaults to the sender. 292 | deadline (int | None): The deadline by which the transaction must be confirmed. If None, no deadline is set. 293 | slippage_tolerance (float | None): The maximum acceptable slippage percentage. Defaults to 1. 294 | 295 | Returns: 296 | dict: Details of the rebalanced position. 297 | """ 298 | pos = self.nft_manager.contract.positions(nft_id) 299 | pool = self.get_pool(pos["token0"], pos["token1"], pos["fee"]) 300 | 301 | token0 = Contract( 302 | pos["token0"], 303 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 304 | ) 305 | token1 = Contract( 306 | pos["token1"], 307 | abi=os.path.join(os.path.dirname(__file__), "assets/erc20.json"), 308 | ) 309 | self.nft_manager.close_position(nft_id) 310 | amount0 = token0.balanceOf(self.sender) 311 | amount1 = token1.balanceOf(self.sender) 312 | return self.nft_manager.mint_position( 313 | pool, 314 | lower_price, 315 | upper_price, 316 | amount0, 317 | amount1, 318 | amount0Min=amount0Min, 319 | amount1Min=amount1Min, 320 | recipient=recipient, 321 | deadline=deadline, 322 | slippage_tolerance=slippage_tolerance, 323 | ) 324 | 325 | def quote_exact_input_single( 326 | self, 327 | amount_in: int, 328 | pool: Pool | None = None, 329 | token_in: str | None = None, 330 | token_out: str | None = None, 331 | fee: int | None = None, 332 | sqrt_price_limit_x96: int | None = 0, 333 | block_number: int | None = None, 334 | ): 335 | """ 336 | Provides a quote for a given input amount for a single swap operation in a specified pool. 337 | 338 | Args: 339 | amount_in (int): The amount of the input token to be swapped. 340 | pool (Pool | None): The pool object to perform the swap. If None, pool is determined by token addresses and fee. 341 | token_in (str | None): The address of the input token. Required if pool is None. 342 | token_out (str | None): The address of the output token. Required if pool is None. 343 | fee (int | None): The fee tier of the pool. Required if pool is None. 344 | sqrt_price_limit_x96 (int | None): The price limit of the swap as a sqrt price. Defaults to 0 (no limit). 345 | block_number (int | None): The block number at which the quote should be fetched. If None, uses the latest block. 346 | 347 | Returns: 348 | dict: A dictionary containing the estimated output amount and other relevant swap details. 349 | """ 350 | return self.quoter.quote_exact_input_single( 351 | amount_in, 352 | pool=pool, 353 | token_in=token_in, 354 | token_out=token_out, 355 | fee=fee, 356 | sqrt_price_limit_x96=sqrt_price_limit_x96, 357 | block_number=block_number, 358 | ) 359 | 360 | def swap_exact_input_single( 361 | self, 362 | amount_in: int, 363 | pool: Pool | None = None, 364 | token_in: str | None = None, 365 | token_out: str | None = None, 366 | fee: int | None = None, 367 | amount_out_min: int | None = 0, 368 | sqrt_price_limit_x96: int | None = 0, 369 | deadline: int | None = None, 370 | ): 371 | """ 372 | Executes a swap from an exact input amount to a minimum output amount in a specified pool. 373 | 374 | Args: 375 | amount_in (int): The exact amount of the input token to be swapped. 376 | pool (Pool | None): The pool object to perform the swap. If None, pool is determined by token addresses and fee. 377 | token_in (str | None): The address of the input token. Required if pool is None. 378 | token_out (str | None): The address of the output token. Required if pool is None. 379 | fee (int | None): The fee tier of the pool. Required if pool is None. 380 | amount_out_min (int | None): The minimum amount of the output token that must be received. Defaults to 0. 381 | sqrt_price_limit_x96 (int | None): The price limit of the swap as a sqrt price. Defaults to 0 (no limit). 382 | deadline (int | None): The timestamp by which the transaction must be confirmed. If None, no deadline is set. 383 | 384 | Returns: 385 | dict: A dictionary containing the actual output amount and other relevant swap details. 386 | """ 387 | return self.router.swap_exact_input_single( 388 | amount_in=amount_in, 389 | pool=pool, 390 | token_in=token_in, 391 | token_out=token_out, 392 | fee=fee, 393 | amount_out_min=amount_out_min, 394 | sqrt_price_limit_x96=sqrt_price_limit_x96, 395 | deadline=deadline, 396 | ) 397 | 398 | def swap_exact_output_single( 399 | self, 400 | amount_out: int, 401 | pool: Pool | None = None, 402 | token_in: str | None = None, 403 | token_out: str | None = None, 404 | fee: int | None = None, 405 | amount_in_max: int | None = 0, 406 | sqrt_price_limit_x96: int | None = 0, 407 | deadline: int | None = None, 408 | ): 409 | """ 410 | Executes a swap to receive an exact output amount, specifying a maximum input amount in a specified pool. 411 | 412 | Args: 413 | amount_out (int): The exact amount of the output token to be received. 414 | pool (Pool | None): The pool object to perform the swap. If None, pool is determined by token addresses and fee. 415 | token_in (str | None): The address of the input token. Required if pool is None. 416 | token_out (str | None): The address of the output token. Required if pool is None. 417 | fee (int | None): The fee tier of the pool. Required if pool is None. 418 | amount_in_max (int | None): The maximum amount of the input token that can be used. Defaults to 0. 419 | sqrt_price_limit_x96 (int | None): The price limit of the swap as a sqrt price. Defaults to 0 (no limit). 420 | deadline (int | None): The timestamp by which the transaction must be confirmed. If None, no deadline is set. 421 | 422 | Returns: 423 | dict: A dictionary containing the actual input amount used and other relevant swap details. 424 | """ 425 | return self.router.swap_exact_output_single( 426 | amount_out=amount_out, 427 | pool=pool, 428 | token_in=token_in, 429 | token_out=token_out, 430 | fee=fee, 431 | amount_in_max=amount_in_max, 432 | sqrt_price_limit_x96=sqrt_price_limit_x96, 433 | deadline=deadline, 434 | ) 435 | --------------------------------------------------------------------------------