Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3994ffe
WIP: on issue #36
manny-uncharted May 24, 2025
63fc667
REFACTOR: added type settings to connection.py
manny-uncharted May 26, 2025
1f2a99e
FIX: added type setting to the init method in connection.py
manny-uncharted May 26, 2025
6b6af63
REFACTOR: function name and variable name shared the same name within…
manny-uncharted May 26, 2025
70b9d94
REFACTOR: refactored sdk/dag.py adding type setting
manny-uncharted May 26, 2025
a1c9491
type settings on erasure_code
manny-uncharted Jun 1, 2025
d49e9d8
Merge branches 'feat/36' and 'dev' of github.com:manny-uncharted/akav…
manny-uncharted Jun 4, 2025
98ee331
REFACTOR: refactored sdk, connection, erasure_code
manny-uncharted Jun 6, 2025
e07e805
REFACTOR: completed the sdk
manny-uncharted Jun 6, 2025
8faea3c
REFACTOR: refactored the make_gcm_cipher function to return both the …
manny-uncharted Jun 6, 2025
1c6647c
REFACTOR: removed local setup files
manny-uncharted Jun 6, 2025
307ff06
REFACTOR: done on encryption.py
manny-uncharted Jun 6, 2025
e4c05e3
REFACTOR: erasure code and test erasure code
manny-uncharted Jun 6, 2025
50b08ae
REFACTOR: completed on test_erasure_code.py
manny-uncharted Jun 6, 2025
74d28c7
REFACTOR: reverted erasure_code.py to using list comprehension
manny-uncharted Jun 6, 2025
d2baf08
REFACTOR: added type setting for test_dag.py
manny-uncharted Jun 6, 2025
29dbb54
REFACTOR: refactored the storage.py for type setting
manny-uncharted Jun 7, 2025
9c84a7d
REFACTOR: added type setting on policy.py
manny-uncharted Jun 7, 2025
1f2adc4
REFACTOR: refactored the access_manager with full type setting
manny-uncharted Jun 7, 2025
2f80aa7
WIP: on the client.py
manny-uncharted Jun 7, 2025
db4187d
REFACTOR: done for client and encryption
manny-uncharted Jun 7, 2025
141d737
REFACTOR: updated the extract_block_data function to handle the diffe…
manny-uncharted Jun 7, 2025
56ed38c
REFACTOR: on the dag file
manny-uncharted Jun 7, 2025
e963f0a
Merge branch 'dev' of github.com:manny-uncharted/akavesdk-py into fea…
manny-uncharted Jun 9, 2025
b9ef6b6
WIP:
manny-uncharted Jun 9, 2025
946a0ac
FIX: resolved issue with Duplicate class
manny-uncharted Jun 12, 2025
3010aa2
Merge branch 'dev' of github.com:manny-uncharted/akavesdk-py into fea…
manny-uncharted Jun 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
__pycache__/
*.py[cod]
*$py.class

pyproject.toml
poetry.lock
# Virtual environment directories
venv/
env/
Expand All @@ -11,6 +12,7 @@ env/
ENV/
env.bak/
venv.bak/
NOTES


# Distribution / packaging
Expand All @@ -28,6 +30,9 @@ dist/
*.swo
*.bak
*.tmp
.mypy_cache/
scripts/
.pytest_cache/

# macOS files
.DS_Store
Expand All @@ -38,7 +43,7 @@ scripts/
# Windows files
Thumbs.db
ehthumbs.db

NOTES
# Log files
*.log

Expand Down
31 changes: 31 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = False
warn_no_return = True
warn_unreachable = True

files = ./

exclude =
^__init__\.py$
_pb2\.py$
_pb2_grpc\.py$
\.proto$

[mypy-grpc.*]
ignore_missing_imports = True

[mypy-reedsolo.*]
ignore_missing_imports = True


[mypy-ipld_dag_pb.*]
ignore_missing_imports = True
98 changes: 72 additions & 26 deletions private/ipc/client.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,69 @@
import time
from typing import Optional
from web3 import Web3
from web3.middleware import geth_poa_middleware
from web3.middleware import geth_poa_middleware # type: ignore[import-untyped]
from eth_account import Account
from eth_account.signers.local import LocalAccount
from .contracts import StorageContract, AccessManagerContract
from typing import cast, Tuple
from eth_typing import HexAddress, HexStr
from web3.types import TxReceipt

class TransactionFailedError(Exception):
"""Raised when a transaction fails (receipt status is 0)."""
pass

class Config:
"""Configuration for the Ethereum storage contract client."""
def __init__(self, dial_uri: str, private_key: str, storage_contract_address: str, access_contract_address: Optional[str] = None):
def __init__(self, dial_uri: str, private_key: str, storage_contract_address: str, access_contract_address: Optional[str] = None) -> None:
"""
Initializes the client configuration.
Args:
dial_uri: URI for the Ethereum node (e.g., HTTP or IPC path).
private_key: Private key of the account to use for transactions.
storage_contract_address: Address of the deployed Storage contract.
access_contract_address: Optional address of the AccessManager contract.
"""
if not dial_uri:
raise ValueError("Dial URI must not be empty.")
if not private_key:
raise ValueError("Private key must not be empty.")
if not storage_contract_address:
raise ValueError("Storage contract address must not be empty.")
if access_contract_address and not isinstance(access_contract_address, str):
raise ValueError("Access contract address must be a string if provided.")
self.dial_uri = dial_uri
self.private_key = private_key
self.storage_contract_address = storage_contract_address
self.access_contract_address = access_contract_address

@staticmethod
def default():
def default() -> 'Config':
return Config(dial_uri="", private_key="", storage_contract_address="", access_contract_address="")

class Client:
"""Represents the Ethereum storage client."""
def __init__(self, web3: Web3, auth: LocalAccount, storage: StorageContract, access_manager: Optional[AccessManagerContract] = None):
self.web3 = web3
self.auth = auth
self.storage = storage
self.access_manager = access_manager
def __init__(self, web3: Web3, auth: LocalAccount, storage: StorageContract, access_manager: Optional[AccessManagerContract] = None) -> None:
"""
Initializes the client with the given Web3 instance, account, and contracts.
Args:
web3: Web3 instance connected to the Ethereum node.
auth: Local account used for signing transactions.
storage: Storage contract instance.
access_manager: Optional AccessManager contract instance.
"""
if not isinstance(web3, Web3):
raise TypeError("web3 must be an instance of web3.Web3")
if not isinstance(auth, LocalAccount):
raise TypeError("auth must be an instance of eth_account.LocalAccount")
if not isinstance(storage, StorageContract):
raise TypeError("storage must be an instance of StorageContract")
if access_manager is not None and not isinstance(access_manager, AccessManagerContract):
raise TypeError("access_manager must be an instance of AccessManagerContract or None")
self.web3: Web3 = web3
self.auth: LocalAccount = auth
self.storage: StorageContract = storage
self.access_manager: Optional[AccessManagerContract] = access_manager
# self.ticker = 0.2 # 200ms polling interval (currently unused)

@classmethod
Expand Down Expand Up @@ -57,29 +92,28 @@ def dial(cls, config: Config) -> 'Client':
raise ValueError(f"Invalid private key: {e}") from e

# Initialize contracts
storage = StorageContract(web3, config.storage_contract_address)
storage_addr = cast(HexAddress, config.storage_contract_address)
storage = StorageContract(web3, storage_addr)
access_manager = None
if config.access_contract_address:
access_manager = AccessManagerContract(web3, config.access_contract_address)
access_addr = cast(HexAddress, config.access_contract_address)
access_manager = AccessManagerContract(web3, access_addr)

return cls(web3, account, storage, access_manager)

@staticmethod
def _wait_for_tx_receipt(web3_instance: Web3, tx_hash: str, timeout: int = 120, poll_latency: float = 0.5):
def _wait_for_tx_receipt(web3_instance: Web3, tx_hash: str, timeout: int = 120, poll_latency: float = 0.5) -> TxReceipt:
"""Waits for a transaction receipt and raises an error if it failed."""
try:
receipt = web3_instance.eth.wait_for_transaction_receipt(
tx_hash, timeout=timeout, poll_latency=poll_latency
)
if receipt.status == 0:
# Consider adding more details from the receipt if available
receipt: TxReceipt = web3_instance.eth.wait_for_transaction_receipt(cast(HexStr, tx_hash), timeout=timeout, poll_latency=poll_latency)
if receipt['status'] == 0:
raise TransactionFailedError(f"Transaction {tx_hash} failed.")
return receipt
except Exception as e: # Catch specific web3 exceptions if needed
except Exception as e:
raise TimeoutError(f"Timeout waiting for transaction {tx_hash}") from e

@classmethod
def deploy_storage(cls, config: Config):
def deploy_storage(cls, config: Config) -> Tuple['Client', HexAddress, HexAddress]:
"""Deploys Storage and AccessManager contracts.

Requires ABI and Bytecode to be available.
Expand All @@ -95,15 +129,18 @@ def deploy_storage(cls, config: Config):
web3.middleware_onion.inject(geth_poa_middleware, layer=0)

try:
account = Account.from_key(config.private_key)
account: LocalAccount = Account.from_key(config.private_key)
except ValueError as e:
raise ValueError(f"Invalid private key: {e}") from e

# --- Deployment requires ABI and Bytecode ---
# Ensure these are correctly imported or defined
try:
# This assumes you have ABI/Bytecode defined in your contracts package/module
from .contracts import storage_abi, storage_bytecode, access_manager_abi, access_manager_bytecode
from .contracts import (
storage_abi, storage_bytecode,
access_manager_abi, access_manager_bytecode
)
except ImportError:
raise ImportError("Storage/AccessManager ABI and Bytecode not found. Ensure they are defined in akavesdk-py/private/ipc/contracts.")

Expand All @@ -121,8 +158,10 @@ def deploy_storage(cls, config: Config):
signed_tx = account.sign_transaction(construct_txn)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"Storage deployment transaction sent: {tx_hash.hex()}")
storage_receipt = cls._wait_for_tx_receipt(web3, tx_hash)
storage_address = storage_receipt.contractAddress
storage_receipt = cls._wait_for_tx_receipt(web3, cast(HexStr, tx_hash.hex()))
storage_address = storage_receipt['contractAddress']
if storage_address is None:
raise TransactionFailedError(f"Storage contract deployment failed, no contract address found in receipt: {tx_hash.hex()}")
print(f"Storage contract deployed at: {storage_address}")

# Deploy Access Manager Contract
Expand All @@ -139,13 +178,20 @@ def deploy_storage(cls, config: Config):
signed_tx = account.sign_transaction(construct_txn)
tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"AccessManager deployment transaction sent: {tx_hash.hex()}")
access_manager_receipt = cls._wait_for_tx_receipt(web3, tx_hash)
access_manager_address = access_manager_receipt.contractAddress
access_manager_receipt = cls._wait_for_tx_receipt(web3, cast(HexStr, tx_hash.hex()))
access_manager_address = access_manager_receipt['contractAddress']
if access_manager_address is None:
raise TransactionFailedError(f"AccessManager contract deployment failed, no contract address found in receipt: {tx_hash.hex()}")
print(f"AccessManager contract deployed at: {access_manager_address}")

# Create contract instances for the client
storage_instance = StorageContract(web3, storage_address)
access_manager_instance = AccessManagerContract(web3, access_manager_address)
checksum_storage_address = web3.to_checksum_address(storage_address) # storage_address is now guaranteed to be non-None
checksum_access_manager_address = web3.to_checksum_address(access_manager_address)

storage_instance = StorageContract(web3, checksum_storage_address)
access_manager_instance = AccessManagerContract(web3, checksum_access_manager_address)

# Update config with deployed addresses if needed, or return them separately

# Update config with deployed addresses if needed, or return them separately
# config.storage_contract_address = storage_address
Expand Down
2 changes: 1 addition & 1 deletion private/ipc/contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@

# Example placeholder:
# storage_abi = [...]
# storage_bytecode = "0x..."
# storage_bytecode = "0x...
# access_manager_abi = [...]
# access_manager_bytecode = "0x..."
29 changes: 19 additions & 10 deletions private/ipc/contracts/access_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List, Tuple, Optional
from eth_typing import HexAddress, HexStr
from typing import List, Tuple, Optional, Dict, Any, cast
from eth_typing import HexAddress, HexStr, ChecksumAddress
from web3 import Web3
from web3.contract import Contract
from web3.types import TxParams, TxReceipt
from web3.contract import Contract # type: ignore[import-untyped]
import json

class AccessManagerContract:
Expand All @@ -16,6 +17,9 @@ def __init__(self, web3: Web3, contract_address: HexAddress):
"""
self.web3 = web3
self.contract_address = contract_address
self.checksum_address: ChecksumAddress = self.web3.to_checksum_address(self.contract_address)
if not self.web3.is_checksum_address(self.checksum_address):
raise ValueError(f"Invalid contract address: {contract_address}. Must be a valid checksum address.")

# Contract ABI from the Go bindings
self.abi = [
Expand Down Expand Up @@ -123,8 +127,8 @@ def __init__(self, web3: Web3, contract_address: HexAddress):
"type": "function"
}
]
self.contract = web3.eth.contract(address=contract_address, abi=self.abi)

self.contract: Contract = web3.eth.contract(address=self.checksum_address, abi=self.abi)

def change_public_access(self, file_id: bytes, is_public: bool, from_address: HexAddress) -> None:
"""Changes the public access status of a file.
Expand All @@ -134,7 +138,8 @@ def change_public_access(self, file_id: bytes, is_public: bool, from_address: He
is_public: Whether the file should be publicly accessible
from_address: Address changing the access
"""
tx_hash = self.contract.functions.changePublicAccess(file_id, is_public).transact({'from': from_address})
checksum_from_address: ChecksumAddress = self.web3.to_checksum_address(from_address)
tx_hash = self.contract.functions.changePublicAccess(file_id, is_public).transact({'from': checksum_from_address})
self.web3.eth.wait_for_transaction_receipt(tx_hash)

def get_file_access_info(self, file_id: bytes) -> Tuple[HexAddress, bool]:
Expand All @@ -146,7 +151,8 @@ def get_file_access_info(self, file_id: bytes) -> Tuple[HexAddress, bool]:
Returns:
Tuple containing (policy contract address, is public)
"""
return self.contract.functions.getFileAccessInfo(file_id).call()
result = self.contract.functions.getFileAccessInfo(file_id).call()
return cast(Tuple[HexAddress, bool], result)

def get_policy(self, file_id: bytes) -> HexAddress:
"""Gets the policy contract address for a file.
Expand All @@ -157,7 +163,8 @@ def get_policy(self, file_id: bytes) -> HexAddress:
Returns:
Address of the policy contract
"""
return self.contract.functions.getPolicy(file_id).call()
result = self.contract.functions.getPolicy(file_id).call()
return cast(HexAddress, result)

def set_policy(self, file_id: bytes, policy_contract: HexAddress, from_address: HexAddress) -> None:
"""Sets the policy contract for a file.
Expand All @@ -167,7 +174,8 @@ def set_policy(self, file_id: bytes, policy_contract: HexAddress, from_address:
policy_contract: Address of the policy contract
from_address: Address setting the policy
"""
tx_hash = self.contract.functions.setPolicy(file_id, policy_contract).transact({'from': from_address})
checksum_from_address: ChecksumAddress = self.web3.to_checksum_address(from_address)
tx_hash = self.contract.functions.setPolicy(file_id, policy_contract).transact({'from': checksum_from_address})
self.web3.eth.wait_for_transaction_receipt(tx_hash)

def get_storage_contract(self) -> HexAddress:
Expand All @@ -176,4 +184,5 @@ def get_storage_contract(self) -> HexAddress:
Returns:
Address of the storage contract
"""
return self.contract.functions.storageContract().call()
result = self.contract.functions.storageContract().call()
return cast(HexAddress, result)
Loading