diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000000..8bf17cd8ee --- /dev/null +++ b/.cursorrules @@ -0,0 +1,9 @@ +You are an AI assistant specialized in Python and Rust development. + +For python + +Your approach emphasizes:Clear project structure with separate directories for source code, tests, docs, and config.Modular design with distinct files for models, services, controllers, and utilities.Configuration management using environment variables.Robust error handling and logging, including context capture.Comprehensive testing with pytest.Detailed documentation using docstrings and README files.Dependency management via https://github.com/astral-sh/uv and virtual environments.Code style consistency using Ruff.CI/CD implementation with GitHub Actions or GitLab CI.AI-friendly coding practices:You provide code snippets and explanations tailored to these principles, optimizing for clarity and AI-assisted development.Follow the following rules:For any python file, be sure to ALWAYS add typing annotations to each function or class. Be sure to include return types when necessary. Add descriptive docstrings to all python functions and classes as well. Please use pep257 convention for python. Update existing docstrings if need be.Make sure you keep any comments that exist in a file.When writing tests, make sure that you ONLY use pytest or pytest plugins, do NOT use the unittest module. All tests should have typing annotations as well. All tests should be in ./tests. Be sure to create all necessary files and folders. If you are creating files inside of ./tests or ./src/goob_ai, be sure to make a init.py file if one does not exist.All tests should be fully annotated and should contain docstrings. Be sure to import the following if TYPE_CHECKING:from _pytest.capture import CaptureFixturefrom _pytest.fixtures import FixtureRequestfrom _pytest.logging import LogCaptureFixturefrom _pytest.monkeypatch import MonkeyPatchfrom pytest_mock.plugin import MockerFixture + +For Rust + +Please do not use unwraps or panics. Please ensure all methods are fully tested and annotated. \ No newline at end of file diff --git a/.github/workflows/python-publish-node.yml b/.github/workflows/python-publish-node.yml new file mode 100644 index 0000000000..e0c255a872 --- /dev/null +++ b/.github/workflows/python-publish-node.yml @@ -0,0 +1,190 @@ +name: Build and Publish Python Package + +on: + push: + tags: + - 'v*' + +permissions: + id-token: write + contents: read + +jobs: + macos: + runs-on: macos-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + run: | + mkdir -p sn_node/python/autonomi_node + cat > sn_node/python/autonomi_node/__init__.py << EOL + from ._autonomi import * + __version__ = "${{ github.ref_name }}" + EOL + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: 'true' + working-directory: ./sn_node + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: sn_node/dist/*.whl + if-no-files-found: error + + windows: + runs-on: windows-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.target }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + shell: cmd + run: | + mkdir sn_node\python\autonomi_client + echo from ._autonomi import * > autonomi\python\autonomi_node\__init__.py + echo __version__ = "0.2.33" >> autonomi\python\autonomi_node\__init__.py + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + args: --release --out dist + sccache: 'true' + working-directory: ./sn_node + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: sn_node/dist/*.whl + if-no-files-found: error + + linux: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x86_64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + target: x86_64-unknown-linux-gnu + - name: Install dependencies + run: | + python -m pip install --user cffi + python -m pip install --user patchelf + rustup component add rustfmt + - name: Create Python module structure + run: | + mkdir -p sn_node/python/autonomi_sn_node + cat > sn_node/python/autonomi_node/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist + sccache: 'true' + working-directory: ./sn_node + before-script-linux: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env + rustup component add rustfmt + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: sn_node/dist/*.whl + if-no-files-found: error + + sdist: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + run: | + mkdir -p sn_node/python/autonomi_node + cat > sn_node/python/autonomi_node/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + working-directory: ./autonomi + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.tar.gz + if-no-files-found: error + + release: + name: Release + runs-on: ubuntu-latest + needs: [macos, windows, linux, sdist] + permissions: + id-token: write + contents: read + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + path: dist + - name: Display structure of downloaded files + run: ls -R dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + verbose: true + print-hash: true \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000000..3c19691444 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,190 @@ +name: Build and Publish Python Package + +on: + push: + tags: + - 'XXX*' + +permissions: + id-token: write + contents: read + +jobs: + macos: + runs-on: macos-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + run: | + mkdir -p autonomi/python/autonomi_client + cat > autonomi/python/autonomi_client/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: 'true' + working-directory: ./autonomi + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.whl + if-no-files-found: error + + windows: + runs-on: windows-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.target }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + shell: cmd + run: | + mkdir autonomi\python\autonomi_client + echo from ._autonomi import * > autonomi\python\autonomi_client\__init__.py + echo __version__ = "0.2.33" >> autonomi\python\autonomi_client\__init__.py + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + args: --release --out dist + sccache: 'true' + working-directory: ./autonomi + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.whl + if-no-files-found: error + + linux: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x86_64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + target: x86_64-unknown-linux-gnu + - name: Install dependencies + run: | + python -m pip install --user cffi + python -m pip install --user patchelf + rustup component add rustfmt + - name: Create Python module structure + run: | + mkdir -p autonomi/python/autonomi_client + cat > autonomi/python/autonomi_client/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist + sccache: 'true' + working-directory: ./autonomi + before-script-linux: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env + rustup component add rustfmt + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.whl + if-no-files-found: error + + sdist: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + run: | + mkdir -p autonomi/python/autonomi_client + cat > autonomi/python/autonomi_client/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + working-directory: ./autonomi + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.tar.gz + if-no-files-found: error + + release: + name: Release + runs-on: ubuntu-latest + needs: [macos, windows, linux, sdist] + permissions: + id-token: write + contents: read + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + path: dist + - name: Display structure of downloaded files + run: ls -R dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + verbose: true + print-hash: true diff --git a/.gitignore b/.gitignore index 99b9fcf479..bf0d0deed0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,13 @@ metrics/prometheus/prometheus.yml *.dot sn_node_manager/.vagrant + +# Python +.venv/ +uv.lock +*.so +*.pyc + +*.pyc +*.swp + diff --git a/Cargo.lock b/Cargo.lock index d6bf9f17fb..bc5a9b1894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,6 +1111,7 @@ dependencies = [ "instant", "js-sys", "libp2p 0.54.1", + "pyo3", "rand 0.8.5", "rmp-serde", "self_encryption", @@ -4043,6 +4044,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -5555,6 +5562,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg 1.3.0", +] + [[package]] name = "merkle-cbt" version = "0.3.2" @@ -7016,6 +7032,69 @@ dependencies = [ "prost 0.9.0", ] +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.77", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -9113,6 +9192,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.12.0" @@ -9898,6 +9983,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/autonomi/Cargo.toml b/autonomi/Cargo.toml index 3bdd14f686..3ac4f23e66 100644 --- a/autonomi/Cargo.toml +++ b/autonomi/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" repository = "https://github.com/maidsafe/safe_network" [lib] +name = "autonomi" crate-type = ["cdylib", "rlib"] [features] @@ -22,6 +23,7 @@ local = ["sn_networking/local", "sn_evm/local"] registers = ["data"] loud = [] external-signer = ["sn_evm/external-signer", "data"] +extension-module = ["pyo3/extension-module"] [dependencies] bip39 = "2.0.0" @@ -55,6 +57,7 @@ serde-wasm-bindgen = "0.6.5" sha2 = "0.10.6" blst = "0.3.13" blstrs = "0.7.1" +pyo3 = { version = "0.20", optional = true, features = ["extension-module", "abi3-py38"] } [dev-dependencies] alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } diff --git a/autonomi/README.md b/autonomi/README.md index 5b95af38e4..5a638b136e 100644 --- a/autonomi/README.md +++ b/autonomi/README.md @@ -156,4 +156,193 @@ Payment token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3 Chunk payments address: 0x8464135c8F25Da09e49BC8782676a84730C318bC Deployer wallet private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Genesis wallet balance: (tokens: 20000000000000000000000000, gas: 9998998011366954730202) -``` \ No newline at end of file +``` + +## Python Bindings + +The Autonomi client library provides Python bindings for easy integration with Python applications. + +### Installation + +```bash +pip install autonomi-client +``` + +### Quick Start + +```python +from autonomi_client import Client, Wallet, PaymentOption + +# Initialize wallet with private key +wallet = Wallet("your_private_key_here") +print(f"Wallet address: {wallet.address()}") +print(f"Balance: {wallet.balance()}") + +# Connect to network +client = Client.connect(["/ip4/127.0.0.1/tcp/12000"]) + +# Create payment option +payment = PaymentOption.wallet(wallet) + +# Upload data +data = b"Hello, Safe Network!" +addr = client.data_put(data, payment) +print(f"Data uploaded to: {addr}") + +# Download data +retrieved = client.data_get(addr) +print(f"Retrieved: {retrieved.decode()}") +``` + +### Available Modules + +#### Core Components + +- `Client`: Main interface to the Autonomi network + - `connect(peers: List[str])`: Connect to network nodes + - `data_put(data: bytes, payment: PaymentOption)`: Upload data + - `data_get(addr: str)`: Download data + - `private_data_put(data: bytes, payment: PaymentOption)`: Store private data + - `private_data_get(access: PrivateDataAccess)`: Retrieve private data + - `register_generate_key()`: Generate register key + +- `Wallet`: Ethereum wallet management + - `new(private_key: str)`: Create wallet from private key + - `address()`: Get wallet address + - `balance()`: Get current balance + +- `PaymentOption`: Payment configuration + - `wallet(wallet: Wallet)`: Create payment option from wallet + +#### Private Data + +- `PrivateDataAccess`: Handle private data storage + - `from_hex(hex: str)`: Create from hex string + - `to_hex()`: Convert to hex string + - `address()`: Get short reference address + +```python +# Private data example +access = client.private_data_put(secret_data, payment) +print(f"Private data stored at: {access.to_hex()}") +retrieved = client.private_data_get(access) +``` + +#### Registers + +- Register operations for mutable data + - `register_create(value: bytes, name: str, key: RegisterSecretKey, wallet: Wallet)` + - `register_get(address: str)` + - `register_update(register: Register, value: bytes, key: RegisterSecretKey)` + +```python +# Register example +key = client.register_generate_key() +register = client.register_create(b"Initial value", "my_register", key, wallet) +client.register_update(register, b"New value", key) +``` + +#### Vaults + +- `VaultSecretKey`: Manage vault access + - `new()`: Generate new key + - `from_hex(hex: str)`: Create from hex string + - `to_hex()`: Convert to hex string + +- `UserData`: User data management + - `new()`: Create new user data + - `add_file_archive(archive: str)`: Add file archive + - `add_private_file_archive(archive: str)`: Add private archive + - `file_archives()`: List archives + - `private_file_archives()`: List private archives + +```python +# Vault example +vault_key = VaultSecretKey.new() +cost = client.vault_cost(vault_key) +client.write_bytes_to_vault(data, payment, vault_key, content_type=1) +data, content_type = client.fetch_and_decrypt_vault(vault_key) +``` + +#### Utility Functions + +- `encrypt(data: bytes)`: Self-encrypt data +- `hash_to_short_string(input: str)`: Generate short reference + +### Complete Examples + +#### Data Management + +```python +def handle_data_operations(client, payment): + # Upload text + text_data = b"Hello, Safe Network!" + text_addr = client.data_put(text_data, payment) + + # Upload binary data + with open("image.jpg", "rb") as f: + image_data = f.read() + image_addr = client.data_put(image_data, payment) + + # Download and verify + downloaded = client.data_get(text_addr) + assert downloaded == text_data +``` + +#### Private Data and Encryption + +```python +def handle_private_data(client, payment): + # Create and encrypt private data + secret = {"api_key": "secret_key"} + data = json.dumps(secret).encode() + + # Store privately + access = client.private_data_put(data, payment) + print(f"Access token: {access.to_hex()}") + + # Retrieve + retrieved = client.private_data_get(access) + secret = json.loads(retrieved.decode()) +``` + +#### Vault Management + +```python +def handle_vault(client, payment): + # Create vault + vault_key = VaultSecretKey.new() + + # Store user data + user_data = UserData() + user_data.add_file_archive("archive_address") + + # Save to vault + cost = client.put_user_data_to_vault(vault_key, payment, user_data) + + # Retrieve + retrieved = client.get_user_data_from_vault(vault_key) + archives = retrieved.file_archives() +``` + +### Error Handling + +All operations can raise exceptions. It's recommended to use try-except blocks: + +```python +try: + client = Client.connect(peers) + # ... operations ... +except Exception as e: + print(f"Error: {e}") +``` + +### Best Practices + +1. Always keep private keys secure +2. Use error handling for all network operations +3. Clean up resources when done +4. Monitor wallet balance for payments +5. Use appropriate content types for vault storage + +For more examples, see the `examples/` directory in the repository. diff --git a/autonomi/examples/autonomi_advanced.py b/autonomi/examples/autonomi_advanced.py new file mode 100644 index 0000000000..310766192e --- /dev/null +++ b/autonomi/examples/autonomi_advanced.py @@ -0,0 +1,79 @@ +from autonomi_client import Client, Wallet, PaymentOption +import sys + +def init_wallet(private_key: str) -> Wallet: + try: + wallet = Wallet(private_key) + print(f"Initialized wallet with address: {wallet.address()}") + + balance = wallet.balance() + print(f"Wallet balance: {balance}") + + return wallet + except Exception as e: + print(f"Failed to initialize wallet: {e}") + sys.exit(1) + +def connect_to_network(peers: list[str]) -> Client: + try: + client = Client.connect(peers) + print("Successfully connected to network") + return client + except Exception as e: + print(f"Failed to connect to network: {e}") + sys.exit(1) + +def upload_data(client: Client, data: bytes, payment: PaymentOption) -> str: + try: + addr = client.data_put(data, payment) + print(f"Successfully uploaded data to: {addr}") + return addr + except Exception as e: + print(f"Failed to upload data: {e}") + sys.exit(1) + +def download_data(client: Client, addr: str) -> bytes: + try: + data = client.data_get(addr) + print(f"Successfully downloaded {len(data)} bytes") + return data + except Exception as e: + print(f"Failed to download data: {e}") + sys.exit(1) + +def main(): + # Configuration + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + # Initialize + wallet = init_wallet(private_key) + client = connect_to_network(peers) + payment = PaymentOption.wallet(wallet) + + # Upload test data + test_data = b"Hello, Safe Network!" + addr = upload_data(client, test_data, payment) + + # Download and verify + downloaded = download_data(client, addr) + assert downloaded == test_data, "Data verification failed!" + print("Data verification successful!") + + # Example file handling + try: + with open("example.txt", "rb") as f: + file_data = f.read() + file_addr = upload_data(client, file_data, payment) + + # Download and save to new file + downloaded = download_data(client, file_addr) + with open("example_downloaded.txt", "wb") as f_out: + f_out.write(downloaded) + print("File operations completed successfully!") + except IOError as e: + print(f"File operation failed: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/autonomi/examples/autonomi_data_registers.py b/autonomi/examples/autonomi_data_registers.py new file mode 100644 index 0000000000..a7b8ba42ff --- /dev/null +++ b/autonomi/examples/autonomi_data_registers.py @@ -0,0 +1,89 @@ +from autonomi_client import Client, Wallet, PaymentOption, RegisterSecretKey +import hashlib + +def handle_data_operations(client: Client, payment: PaymentOption): + """Example of various data operations""" + print("\n=== Data Operations ===") + + # Upload some text data + text_data = b"Hello, Safe Network!" + text_addr = client.data_put(text_data, payment) + print(f"Text data uploaded to: {text_addr}") + + # Upload binary data (like an image) + with open("example.jpg", "rb") as f: + image_data = f.read() + image_addr = client.data_put(image_data, payment) + print(f"Image uploaded to: {image_addr}") + + # Download and verify data + downloaded_text = client.data_get(text_addr) + assert downloaded_text == text_data, "Text data verification failed!" + print("Text data verified successfully") + + # Download and save image + downloaded_image = client.data_get(image_addr) + with open("downloaded_example.jpg", "wb") as f: + f.write(downloaded_image) + print("Image downloaded successfully") + +def handle_register_operations(client: Client, wallet: Wallet): + """Example of register operations""" + print("\n=== Register Operations ===") + + # Create a register key + register_key = client.register_generate_key() + print(f"Generated register key") + + # Create a register with initial value + register_name = "my_first_register" + initial_value = b"Initial register value" + register = client.register_create( + initial_value, + register_name, + register_key, + wallet + ) + print(f"Created register at: {register.address()}") + + # Read current value + values = register.values() + print(f"Current register values: {[v.decode() for v in values]}") + + # Update register value + new_value = b"Updated register value" + client.register_update(register, new_value, register_key) + print("Register updated") + + # Read updated value + updated_register = client.register_get(register.address()) + updated_values = updated_register.values() + print(f"Updated register values: {[v.decode() for v in updated_values]}") + +def main(): + # Initialize wallet and client + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + # Setup + wallet = Wallet(private_key) + print(f"Wallet address: {wallet.address()}") + print(f"Wallet balance: {wallet.balance()}") + + client = Client.connect(peers) + payment = PaymentOption.wallet(wallet) + + # Run examples + handle_data_operations(client, payment) + handle_register_operations(client, wallet) + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("\nAll operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/autonomi_example.py b/autonomi/examples/autonomi_example.py new file mode 100644 index 0000000000..496446173c --- /dev/null +++ b/autonomi/examples/autonomi_example.py @@ -0,0 +1,38 @@ +from autonomi_client import Client, Wallet, PaymentOption + +def main(): + # Initialize a wallet with a private key + # This should be a valid Ethereum private key (64 hex chars without '0x' prefix) + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + wallet = Wallet(private_key) + print(f"Wallet address: {wallet.address()}") + print(f"Wallet balance: {wallet.balance()}") + + # Connect to the network + # These should be valid multiaddresses of network nodes + peers = [ + "/ip4/127.0.0.1/tcp/12000", + "/ip4/127.0.0.1/tcp/12001" + ] + client = Client.connect(peers) + + # Create payment option using the wallet + payment = PaymentOption.wallet(wallet) + + # Upload some data + data = b"Hello, Safe Network!" + addr = client.data_put(data, payment) + print(f"Data uploaded to address: {addr}") + + # Download the data back + downloaded = client.data_get(addr) + print(f"Downloaded data: {downloaded.decode()}") + + # You can also upload files + with open("example.txt", "rb") as f: + file_data = f.read() + file_addr = client.data_put(file_data, payment) + print(f"File uploaded to address: {file_addr}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/autonomi/examples/autonomi_private_data.py b/autonomi/examples/autonomi_private_data.py new file mode 100644 index 0000000000..3b0d9327e4 --- /dev/null +++ b/autonomi/examples/autonomi_private_data.py @@ -0,0 +1,90 @@ +from autonomi_client import Client, Wallet, PaymentOption, RegisterSecretKey, RegisterPermissions +from typing import List, Optional +import json + +class DataManager: + def __init__(self, client: Client, wallet: Wallet): + self.client = client + self.wallet = wallet + self.payment = PaymentOption.wallet(wallet) + + def store_private_data(self, data: bytes) -> str: + """Store data privately and return its address""" + addr = self.client.private_data_put(data, self.payment) + return addr + + def retrieve_private_data(self, addr: str) -> bytes: + """Retrieve privately stored data""" + return self.client.private_data_get(addr) + + def create_shared_register(self, name: str, initial_value: bytes, + allowed_writers: List[str]) -> str: + """Create a register that multiple users can write to""" + register_key = self.client.register_generate_key() + + # Create permissions for all writers + permissions = RegisterPermissions.new_with(allowed_writers) + + register = self.client.register_create_with_permissions( + initial_value, + name, + register_key, + permissions, + self.wallet + ) + + return register.address() + +def main(): + # Initialize + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + wallet = Wallet(private_key) + client = Client.connect(peers) + manager = DataManager(client, wallet) + + # Store private data + user_data = { + "username": "alice", + "preferences": { + "theme": "dark", + "notifications": True + } + } + private_data = json.dumps(user_data).encode() + private_addr = manager.store_private_data(private_data) + print(f"Stored private data at: {private_addr}") + + # Retrieve and verify private data + retrieved_data = manager.retrieve_private_data(private_addr) + retrieved_json = json.loads(retrieved_data.decode()) + print(f"Retrieved data: {retrieved_json}") + + # Create shared register + allowed_writers = [ + wallet.address(), # self + "0x1234567890abcdef1234567890abcdef12345678" # another user + ] + register_addr = manager.create_shared_register( + "shared_config", + b"initial shared data", + allowed_writers + ) + print(f"Created shared register at: {register_addr}") + + # Verify register + register = client.register_get(register_addr) + values = register.values() + print(f"Register values: {[v.decode() for v in values]}") + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("All operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/autonomi_private_encryption.py b/autonomi/examples/autonomi_private_encryption.py new file mode 100644 index 0000000000..7f71a6b8d6 --- /dev/null +++ b/autonomi/examples/autonomi_private_encryption.py @@ -0,0 +1,75 @@ +from autonomi_client import ( + Client, Wallet, PaymentOption, PrivateDataAccess, + encrypt, hash_to_short_string +) +import json + +def demonstrate_private_data(client: Client, payment: PaymentOption): + """Show private data handling""" + print("\n=== Private Data Operations ===") + + # Create some private data + secret_data = { + "password": "very_secret", + "api_key": "super_secret_key" + } + data_bytes = json.dumps(secret_data).encode() + + # Store it privately + access = client.private_data_put(data_bytes, payment) + print(f"Stored private data, access token: {access.to_hex()}") + print(f"Short reference: {access.address()}") + + # Retrieve it + retrieved_bytes = client.private_data_get(access) + retrieved_data = json.loads(retrieved_bytes.decode()) + print(f"Retrieved private data: {retrieved_data}") + + return access.to_hex() + +def demonstrate_encryption(): + """Show self-encryption functionality""" + print("\n=== Self-Encryption Operations ===") + + # Create test data + test_data = b"This is some test data for encryption" + + # Encrypt it + data_map, chunks = encrypt(test_data) + print(f"Original data size: {len(test_data)} bytes") + print(f"Data map size: {len(data_map)} bytes") + print(f"Number of chunks: {len(chunks)}") + print(f"Total chunks size: {sum(len(c) for c in chunks)} bytes") + +def main(): + # Initialize + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + # Setup + wallet = Wallet(private_key) + print(f"Wallet address: {wallet.address()}") + print(f"Wallet balance: {wallet.balance()}") + + client = Client.connect(peers) + payment = PaymentOption.wallet(wallet) + + # Run demonstrations + access_token = demonstrate_private_data(client, payment) + demonstrate_encryption() + + # Show utility function + print("\n=== Utility Functions ===") + short_hash = hash_to_short_string(access_token) + print(f"Short hash of access token: {short_hash}") + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("\nAll operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/autonomi_vault.py b/autonomi/examples/autonomi_vault.py new file mode 100644 index 0000000000..6a26d3707a --- /dev/null +++ b/autonomi/examples/autonomi_vault.py @@ -0,0 +1,53 @@ +from autonomi_client import Client, Wallet, PaymentOption, VaultSecretKey, UserData + +def main(): + # Initialize + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + # Setup + wallet = Wallet(private_key) + client = Client.connect(peers) + payment = PaymentOption.wallet(wallet) + + # Create vault key + vault_key = VaultSecretKey.new() + print(f"Created vault key: {vault_key.to_hex()}") + + # Get vault cost + cost = client.vault_cost(vault_key) + print(f"Vault cost: {cost}") + + # Create user data + user_data = UserData() + + # Store some data in vault + data = b"Hello from vault!" + content_type = 1 # Custom content type + cost = client.write_bytes_to_vault(data, payment, vault_key, content_type) + print(f"Wrote data to vault, cost: {cost}") + + # Read data back + retrieved_data, retrieved_type = client.fetch_and_decrypt_vault(vault_key) + print(f"Retrieved data: {retrieved_data.decode()}") + print(f"Content type: {retrieved_type}") + + # Store user data + cost = client.put_user_data_to_vault(vault_key, payment, user_data) + print(f"Stored user data, cost: {cost}") + + # Get user data + retrieved_user_data = client.get_user_data_from_vault(vault_key) + print("File archives:", retrieved_user_data.file_archives()) + print("Private file archives:", retrieved_user_data.private_file_archives()) + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("All vault operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/basic.py b/autonomi/examples/basic.py new file mode 100644 index 0000000000..b7d8f21619 --- /dev/null +++ b/autonomi/examples/basic.py @@ -0,0 +1,70 @@ +from autonomi_client import Client, Wallet, RegisterSecretKey, VaultSecretKey, UserData + +def external_signer_example(client: Client, data: bytes): + # Get quotes for storing data + quotes, payments, free_chunks = client.get_quotes_for_data(data) + print(f"Got {len(quotes)} quotes for storing data") + print(f"Need to make {len(payments)} payments") + print(f"{len(free_chunks)} chunks are free") + + # Get raw quotes for specific addresses + addr = "0123456789abcdef" # Example address + quotes, payments, free = client.get_quotes_for_content_addresses([addr]) + print(f"Got quotes for address {addr}") + +def main(): + # Connect to network + client = Client(["/ip4/127.0.0.1/tcp/12000"]) + + # Create wallet + wallet = Wallet() + print(f"Wallet address: {wallet.address()}") + + # Upload public data + data = b"Hello World!" + addr = client.data_put(data, wallet) + print(f"Uploaded public data to: {addr}") + retrieved = client.data_get(addr) + print(f"Retrieved public data: {retrieved}") + + # Upload private data + private_access = client.private_data_put(b"Secret message", wallet) + print(f"Private data access: {private_access}") + private_data = client.private_data_get(private_access) + print(f"Retrieved private data: {private_data}") + + # Create register + reg_addr = client.register_create(b"Initial value", "my_register", wallet) + print(f"Created register at: {reg_addr}") + reg_values = client.register_get(reg_addr) + print(f"Register values: {reg_values}") + + # Upload file/directory + file_addr = client.file_upload("./test_data", wallet) + print(f"Uploaded files to: {file_addr}") + client.file_download(file_addr, "./downloaded_data") + print("Downloaded files") + + # Vault operations + vault_key = VaultSecretKey.generate() + vault_cost = client.vault_cost(vault_key) + print(f"Vault creation cost: {vault_cost}") + + user_data = UserData() + cost = client.put_user_data_to_vault(vault_key, wallet, user_data) + print(f"Stored user data, cost: {cost}") + + retrieved_data = client.get_user_data_from_vault(vault_key) + print(f"Retrieved user data: {retrieved_data}") + + # Private directory operations + private_dir_access = client.private_dir_upload("./test_data", wallet) + print(f"Uploaded private directory, access: {private_dir_access}") + client.private_dir_download(private_dir_access, "./downloaded_private") + print("Downloaded private directory") + + # External signer example + external_signer_example(client, b"Test data") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/autonomi/pyproject.toml b/autonomi/pyproject.toml new file mode 100644 index 0000000000..db4fbc4e22 --- /dev/null +++ b/autonomi/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +features = ["extension-module"] +python-source = "python" +module-name = "autonomi_client._autonomi" +bindings = "pyo3" +target-dir = "target/wheels" + +[project] +name = "autonomi-client" +dynamic = ["version"] +description = "Autonomi client API" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "GPL-3.0"} +keywords = ["safe", "network", "autonomi"] +authors = [ + {name = "MaidSafe Developers", email = "dev@maidsafe.net"} +] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", +] diff --git a/autonomi/src/lib.rs b/autonomi/src/lib.rs index 2f29d04926..38459bf4c3 100644 --- a/autonomi/src/lib.rs +++ b/autonomi/src/lib.rs @@ -56,3 +56,6 @@ pub use bytes::Bytes; pub use libp2p::Multiaddr; pub use client::Client; + +#[cfg(feature = "extension-module")] +mod python; diff --git a/autonomi/src/python.rs b/autonomi/src/python.rs new file mode 100644 index 0000000000..86a25f941e --- /dev/null +++ b/autonomi/src/python.rs @@ -0,0 +1,350 @@ +use crate::client::{ + archive::ArchiveAddr, + archive_private::PrivateArchiveAccess, + data_private::PrivateDataAccess, + payment::PaymentOption as RustPaymentOption, + vault::{UserData, VaultSecretKey}, + Client as RustClient, +}; +use crate::{Bytes, Wallet as RustWallet}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use sn_evm::EvmNetwork; +use xor_name::XorName; + +#[pyclass(name = "Client")] +pub(crate) struct PyClient { + inner: RustClient, +} + +#[pymethods] +impl PyClient { + #[staticmethod] + fn connect(peers: Vec) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let peers = peers + .into_iter() + .map(|addr| addr.parse()) + .collect::, _>>() + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid multiaddr: {e}")) + })?; + + let client = rt.block_on(RustClient::connect(&peers)).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to connect: {e}")) + })?; + + Ok(Self { inner: client }) + } + + fn private_data_put( + &self, + data: Vec, + payment: &PyPaymentOption, + ) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let access = rt + .block_on( + self.inner + .private_data_put(Bytes::from(data), payment.inner.clone()), + ) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to put private data: {e}")) + })?; + + Ok(PyPrivateDataAccess { inner: access }) + } + + fn private_data_get(&self, access: &PyPrivateDataAccess) -> PyResult> { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let data = rt + .block_on(self.inner.private_data_get(access.inner.clone())) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get private data: {e}")) + })?; + Ok(data.to_vec()) + } + + fn data_put(&self, data: Vec, payment: &PyPaymentOption) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let addr = rt + .block_on( + self.inner + .data_put(bytes::Bytes::from(data), payment.inner.clone()), + ) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to put data: {e}")) + })?; + + Ok(crate::client::address::addr_to_str(addr)) + } + + fn data_get(&self, addr: &str) -> PyResult> { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let addr = crate::client::address::str_to_addr(addr).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid address: {e}")) + })?; + + let data = rt.block_on(self.inner.data_get(addr)).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get data: {e}")) + })?; + + Ok(data.to_vec()) + } + + fn vault_cost(&self, key: &PyVaultSecretKey) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let cost = rt + .block_on(self.inner.vault_cost(&key.inner)) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get vault cost: {e}")) + })?; + Ok(cost.to_string()) + } + + fn write_bytes_to_vault( + &self, + data: Vec, + payment: &PyPaymentOption, + key: &PyVaultSecretKey, + content_type: u64, + ) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let cost = rt + .block_on(self.inner.write_bytes_to_vault( + bytes::Bytes::from(data), + payment.inner.clone(), + &key.inner, + content_type, + )) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to write to vault: {e}")) + })?; + Ok(cost.to_string()) + } + + fn fetch_and_decrypt_vault(&self, key: &PyVaultSecretKey) -> PyResult<(Vec, u64)> { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let (data, content_type) = rt + .block_on(self.inner.fetch_and_decrypt_vault(&key.inner)) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to fetch vault: {e}")) + })?; + Ok((data.to_vec(), content_type)) + } + + fn get_user_data_from_vault(&self, key: &PyVaultSecretKey) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let user_data = rt + .block_on(self.inner.get_user_data_from_vault(&key.inner)) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get user data: {e}")) + })?; + Ok(PyUserData { inner: user_data }) + } + + fn put_user_data_to_vault( + &self, + key: &PyVaultSecretKey, + payment: &PyPaymentOption, + user_data: &PyUserData, + ) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let cost = rt + .block_on(self.inner.put_user_data_to_vault( + &key.inner, + payment.inner.clone(), + user_data.inner.clone(), + )) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to put user data: {e}")) + })?; + Ok(cost.to_string()) + } +} + +#[pyclass(name = "Wallet")] +pub(crate) struct PyWallet { + inner: RustWallet, +} + +#[pymethods] +impl PyWallet { + #[new] + fn new(private_key: String) -> PyResult { + let wallet = RustWallet::new_from_private_key( + EvmNetwork::ArbitrumOne, // TODO: Make this configurable + &private_key, + ) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid private key: {e}")) + })?; + + Ok(Self { inner: wallet }) + } + + fn address(&self) -> String { + format!("{:?}", self.inner.address()) + } + + fn balance(&self) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let balance = rt + .block_on(async { self.inner.balance_of_tokens().await }) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get balance: {e}")) + })?; + + Ok(balance.to_string()) + } + + fn balance_of_gas(&self) -> PyResult { + let rt = tokio::runtime::Runtime::new().expect("Could not start tokio runtime"); + let balance = rt + .block_on(async { self.inner.balance_of_gas_tokens().await }) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get balance: {e}")) + })?; + + Ok(balance.to_string()) + } +} + +#[pyclass(name = "PaymentOption")] +pub(crate) struct PyPaymentOption { + inner: RustPaymentOption, +} + +#[pymethods] +impl PyPaymentOption { + #[staticmethod] + fn wallet(wallet: &PyWallet) -> Self { + Self { + inner: RustPaymentOption::Wallet(wallet.inner.clone()), + } + } +} + +#[pyclass(name = "VaultSecretKey")] +pub(crate) struct PyVaultSecretKey { + inner: VaultSecretKey, +} + +#[pymethods] +impl PyVaultSecretKey { + #[new] + fn new() -> PyResult { + Ok(Self { + inner: VaultSecretKey::random(), + }) + } + + #[staticmethod] + fn from_hex(hex_str: &str) -> PyResult { + VaultSecretKey::from_hex(hex_str) + .map(|key| Self { inner: key }) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid hex key: {e}"))) + } + + fn to_hex(&self) -> String { + self.inner.to_hex() + } +} + +#[pyclass(name = "UserData")] +pub(crate) struct PyUserData { + inner: UserData, +} + +#[pymethods] +impl PyUserData { + #[new] + fn new() -> Self { + Self { + inner: UserData::new(), + } + } + + fn add_file_archive(&mut self, archive: &str) -> Option { + let name = XorName::from_content(archive.as_bytes()); + let archive_addr = ArchiveAddr::from_content(&name); + self.inner.add_file_archive(archive_addr) + } + + fn add_private_file_archive(&mut self, archive: &str) -> Option { + let name = XorName::from_content(archive.as_bytes()); + let private_access = match PrivateArchiveAccess::from_hex(&name.to_string()) { + Ok(access) => access, + Err(_e) => return None, + }; + self.inner.add_private_file_archive(private_access) + } + + fn file_archives(&self) -> Vec<(String, String)> { + self.inner + .file_archives + .iter() + .map(|(addr, name)| (format!("{addr:x}"), name.clone())) + .collect() + } + + fn private_file_archives(&self) -> Vec<(String, String)> { + self.inner + .private_file_archives + .iter() + .map(|(addr, name)| (addr.to_hex(), name.clone())) + .collect() + } +} + +#[pyclass(name = "PrivateDataAccess")] +#[derive(Clone)] +pub(crate) struct PyPrivateDataAccess { + inner: PrivateDataAccess, +} + +#[pymethods] +impl PyPrivateDataAccess { + #[staticmethod] + fn from_hex(hex: &str) -> PyResult { + PrivateDataAccess::from_hex(hex) + .map(|access| Self { inner: access }) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid hex: {e}"))) + } + + fn to_hex(&self) -> String { + self.inner.to_hex() + } + + fn address(&self) -> String { + self.inner.address().to_string() + } +} + +#[pyfunction] +fn encrypt(data: Vec) -> PyResult<(Vec, Vec>)> { + let (data_map, chunks) = self_encryption::encrypt(Bytes::from(data)) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Encryption failed: {e}")))?; + + let data_map_bytes = rmp_serde::to_vec(&data_map) + .map_err(|e| PyValueError::new_err(format!("Failed to serialize data map: {e}")))?; + + let chunks_bytes: Vec> = chunks + .into_iter() + .map(|chunk| chunk.content.to_vec()) + .collect(); + + Ok((data_map_bytes, chunks_bytes)) +} + +#[pymodule] +fn _autonomi(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(encrypt, m)?)?; + Ok(()) +}