diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 041c8f129..532f8453c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -35,9 +35,7 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - # TODO: We should just use nightly, but downgrading to fix a reversion - # in forge coverage. - version: nightly-10440422e63aae660104e079dfccd5b0ae5fd720 + version: nightly - name: forge version run: forge --version diff --git a/.github/workflows/python_test.yaml b/.github/workflows/python_test.yaml new file mode 100644 index 000000000..6bc22afb5 --- /dev/null +++ b/.github/workflows/python_test.yaml @@ -0,0 +1,48 @@ +name: Python test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + name: python test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} + + # NOTE: This is needed to ensure that hyperdrive-wrappers builds correctly. + - name: Install foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Install pip + uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: "pip" + token: ${{ secrets.GITHUB_TOKEN }} + - run: | + python -m pip install --upgrade pip + python -m pip install --upgrade pytest + + - name: Install hyperdrive-math-py + uses: actions/setup-python@v4 + with: + python-version: "3.10" + cache: "pip" + token: ${{ secrets.GITHUB_TOKEN }} + - run: | + python -m pip install crates/hyperdrive-math-py + + - name: Run pytest + run: | + python -m pytest python/test diff --git a/.gitignore b/.gitignore index 952cf26e7..f248d14ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,17 +4,19 @@ forge-cache/ .vscode/ broadcast .env -.venv - node_modules/ yarn-error.log -#Random files on MacOs +# random files on MacOs .DS_Store -# apeworx autogenerated files +# python build +*.egg-info .cache .build +.python-version +.venv +build/ # nix/shell extension - https://direnv.net/ .direnv diff --git a/README.md b/README.md index 66bce1b0f..ddca31d83 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ If you want to automatically format the code, run `yarn prettier`. The current suggested way of integrating your yield source with hyperdrive is through the [ERC-4626 standard](https://eips.ethereum.org/EIPS/eip-4626) although accomodations can be made if this is not possible. Hyperdrive currently makes use of [Yield Daddy](https://github.com/timeless-fi/yield-daddy) to wrap many existing yield sources into this standard. +## Python Rust wrapper +To install the Python package `hyperdrive-math-py`, which wraps the Rust `hyperdrive_math::State` struct, you need to: +- setup a [Python venv](https://docs.python.org/3/library/venv.html) that is running at least `Python 3.7` +- from inside the environment, run `pip install crates/hyperdrive-math-py` +- test the installation by running `pip install --upgrade pytest && pytest python/test` + # Disclaimer The language used in this codebase is for coding convenience only, and is not diff --git a/crates/hyperdrive-math-py/Cargo.toml b/crates/hyperdrive-math-py/Cargo.toml index b9c48f195..01541abf7 100644 --- a/crates/hyperdrive-math-py/Cargo.toml +++ b/crates/hyperdrive-math-py/Cargo.toml @@ -1,17 +1,14 @@ [package] - name = "hyperdrive-math-py" version = "0.1.0" edition = "2021" [lib] - -name = "hyperdrive_math" +name = "hyperdrive_math_py" # "cdylib" is necessary to produce a shared library for Python to import from. crate-type = ["cdylib"] [dependencies] - ethers = "2.0.8" eyre = "0.6.8" rand = "0.8.5" diff --git a/crates/hyperdrive-math-py/MANIFEST.in b/crates/hyperdrive-math-py/MANIFEST.in new file mode 100644 index 000000000..537414263 --- /dev/null +++ b/crates/hyperdrive-math-py/MANIFEST.in @@ -0,0 +1,3 @@ +include Cargo.toml +recursive-include src * +recursive-include python * diff --git a/crates/hyperdrive-math-py/pyproject.toml b/crates/hyperdrive-math-py/pyproject.toml index 82faac330..95c57735c 100644 --- a/crates/hyperdrive-math-py/pyproject.toml +++ b/crates/hyperdrive-math-py/pyproject.toml @@ -1,9 +1,6 @@ -[build-system] -requires = ["maturin>=1.2,<2.0"] -build-backend = "maturin" - [project] -name = "hyperdrive_math_lib" +name = "hyperdrive_math_py" +version = "0.1.0" requires-python = ">=3.7" classifiers = [ "Programming Language :: Rust", @@ -11,6 +8,12 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] +[build-system] +requires = ["setuptools", "wheel", "setuptools-rust"] +build-backend = "setuptools.build_meta" + +[tool.pylint] +extension-pkg-allow-list="hyperdrive-math-py" -[tool.maturin] -features = ["pyo3/extension-module"] +[tool.setuptools.packages.find] +where = ["python", "src"] diff --git a/crates/hyperdrive-math-py/python/hyperdrive_math_py/__init__.py b/crates/hyperdrive-math-py/python/hyperdrive_math_py/__init__.py new file mode 100644 index 000000000..ea2697225 --- /dev/null +++ b/crates/hyperdrive-math-py/python/hyperdrive_math_py/__init__.py @@ -0,0 +1,2 @@ +"""Python module wrapping the Rust implementation of HyperdriveMath""" +from .hyperdrive_math_py import * diff --git a/crates/hyperdrive-math-py/python/hyperdrive_math_py/hyperdrive_math_py.pyi b/crates/hyperdrive-math-py/python/hyperdrive_math_py/hyperdrive_math_py.pyi new file mode 100644 index 000000000..34f6b6da7 --- /dev/null +++ b/crates/hyperdrive-math-py/python/hyperdrive_math_py/hyperdrive_math_py.pyi @@ -0,0 +1,32 @@ +"""Stubs for hyperdrive math.""" +from __future__ import annotations + +from . import types + +class HyperdriveState: + """A class representing the hyperdrive contract state.""" + + def __new__( + cls, pool_config: types.PoolConfig, pool_info: types.PoolInfo + ) -> HyperdriveState: + """Create the HyperdriveState instance.""" + def __init__( + self, pool_config: types.PoolConfig, pool_info: types.PoolInfo + ) -> None: + """Initializes the hyperdrive state. + + Arguments + --------- + pool_config : PoolConfig + Static configuration for the hyperdrive contract. Set at deploy time. + pool_info : PoolInfo + Current state information of the hyperdrive contract. Includes things like reserve levels and share prices. + """ + def get_spot_price(self) -> str: + """Gets the spot price of the bond. + + Returns + ------- + str + The spot price as a string representation of a solidity uint256 value. + """ diff --git a/crates/hyperdrive-math-py/python/hyperdrive_math_py/types.py b/crates/hyperdrive-math-py/python/hyperdrive_math_py/types.py new file mode 100644 index 000000000..ec867e162 --- /dev/null +++ b/crates/hyperdrive-math-py/python/hyperdrive_math_py/types.py @@ -0,0 +1,43 @@ +"""Types for the hyperdrive contract.""" +from typing import NamedTuple + + +class Fees(NamedTuple): + """Protocal Fees.""" + + curve: str + flat: str + governance: str + + +class PoolConfig(NamedTuple): + """Static configuration for the hyperdrive contract. Set at deploy time.""" + + base_token: str + initial_share_price: str + minimum_share_reserves: str + position_duration: str + checkpoint_duration: str + time_stretch: str + governance: str + fee_collector: str + fees: Fees + oracle_size: str + update_gap: str + + +class PoolInfo(NamedTuple): + """Current state information of the hyperdrive contract. Includes things like reserve levels and share prices.""" + + share_reserves: str + bond_reserves: str + lp_total_supply: str + share_price: str + longs_outstanding: str + long_average_maturity_time: str + shorts_outstanding: str + short_average_maturity_time: str + short_base_volume: str + withdrawal_shares_ready_to_withdraw: str + withdrawal_shares_proceeds: str + lp_share_price: str diff --git a/crates/hyperdrive-math-py/setup.py b/crates/hyperdrive-math-py/setup.py new file mode 100644 index 000000000..3d3d6273d --- /dev/null +++ b/crates/hyperdrive-math-py/setup.py @@ -0,0 +1,15 @@ +"""Entry point for installing hyperdrive math python package""" +from setuptools import setup +from setuptools_rust import Binding, RustExtension + +setup( + name="hyperdrive_math_py", + version="0.1.0", + packages=["hyperdrive_math_py"], + package_dir={"": "python"}, + rust_extensions=[ + RustExtension("hyperdrive_math_py.hyperdrive_math_py", binding=Binding.PyO3), + ], + # rust extensions are not zip safe, just like C-extensions. + zip_safe=False, +) diff --git a/crates/hyperdrive-math-py/src/lib.rs b/crates/hyperdrive-math-py/src/lib.rs index b50b9e681..4ffb9a800 100644 --- a/crates/hyperdrive-math-py/src/lib.rs +++ b/crates/hyperdrive-math-py/src/lib.rs @@ -6,22 +6,23 @@ use pyo3::PyErr; use hyperdrive_math::hyperdrive_math::State; -#[pyclass(name = "HyperdriveState")] -pub struct PyState { +#[pyclass(module = "hyperdrive_math_py", name = "HyperdriveState")] +pub struct HyperdriveState { pub state: State, } -impl PyState { +impl HyperdriveState { pub(crate) fn new(state: State) -> Self { - PyState { state } + HyperdriveState { state } } } -impl From for PyState { +impl From for HyperdriveState { fn from(state: State) -> Self { - PyState { state } + HyperdriveState { state } } } + pub struct PyPoolConfig { pub pool_config: PoolConfig, } @@ -134,13 +135,13 @@ impl FromPyObject<'_> for PyPoolInfo { } #[pymethods] -impl PyState { +impl HyperdriveState { #[new] pub fn __init__(pool_config: &PyAny, pool_info: &PyAny) -> PyResult { let rust_pool_config = PyPoolConfig::extract(pool_config)?.pool_config; let rust_pool_info = PyPoolInfo::extract(pool_info)?.pool_info; - let hyperdrive_state = State::new(rust_pool_config, rust_pool_info); - Ok(PyState::new(hyperdrive_state)) + let state = State::new(rust_pool_config, rust_pool_info); + Ok(HyperdriveState::new(state)) } pub fn get_spot_price(&self) -> PyResult { @@ -150,12 +151,12 @@ impl PyState { } } -/// A pyO3 wrapper for the hyperdrie_math crate. The State struct will be exposed with all methods -/// listed in #[pymethods] decorated impl. The name of this function must match the `lib.name` -/// setting in the `Cargo.toml`, else Python will not be able to import the module. +/// A pyO3 wrapper for the hyperdrie_math crate. +/// The Hyperdrive State struct will be exposed with the following methods: +/// - get_spot_price #[pymodule] -#[pyo3(name = "hyperdrive_math")] -fn hyperdrive_math_lib(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::().unwrap(); +#[pyo3(name = "hyperdrive_math_py")] +fn hyperdrive_math_py(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; Ok(()) } diff --git a/python/test/test_hyperdrive_math_wrappers.py b/python/test/test_hyperdrive_math_wrappers.py index 9082a4a63..68ce5f27a 100644 --- a/python/test/test_hyperdrive_math_wrappers.py +++ b/python/test/test_hyperdrive_math_wrappers.py @@ -1,32 +1,6 @@ """Tests for hyperdrive_math.rs wrappers""" -from typing import NamedTuple - -from hyperdrive_math import HyperdriveState - - -class Fees(NamedTuple): - """Protocal Fees""" - - curve: str - flat: str - governance: str - - -class PoolConfig(NamedTuple): - """Sample Pool Config""" - - base_token: str - initial_share_price: str - minimum_share_reserves: str - position_duration: str - checkpoint_duration: str - time_stretch: str - governance: str - fee_collector: str - fees: Fees - oracle_size: str - update_gap: str - +from hyperdrive_math_py import HyperdriveState +from hyperdrive_math_py.types import Fees, PoolConfig, PoolInfo sample_pool_config = PoolConfig( base_token="0x1234567890abcdef1234567890abcdef12345678", @@ -43,23 +17,6 @@ class PoolConfig(NamedTuple): ) -class PoolInfo(NamedTuple): - """Sample Pool Info""" - - share_reserves: str - bond_reserves: str - lp_total_supply: str - share_price: str - longs_outstanding: str - long_average_maturity_time: str - shorts_outstanding: str - short_average_maturity_time: str - short_base_volume: str - withdrawal_shares_ready_to_withdraw: str - withdrawal_shares_proceeds: str - lp_share_price: str - - sample_pool_info = PoolInfo( share_reserves="1000000000000000000", bond_reserves="2000000000000000000",