Skip to content

Commit c2f1b76

Browse files
author
Matthias Zimmermann
committed
feat: increase shared base of Arkiv and AsyncArkiv
1 parent b6c3920 commit c2f1b76

File tree

3 files changed

+81
-185
lines changed

3 files changed

+81
-185
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Arkiv is a permissioned storage system for decentralized apps, supporting flexib
44

55
The Arkiv SDK is the official Python library for interacting with Arkiv networks. It offers a type-safe, developer-friendly API for managing entities, querying data, subscribing to events, and offchain verification—ideal for both rapid prototyping and production use.
66

7+
## TODO AsyncArkiv
8+
9+
- check for duplicate code with Arkiv
10+
- refactor
11+
- re-check testing
12+
- commit
13+
- move to ArkivModule for async usage
14+
715
## Architecture
816

917
Principles:

src/arkiv/client.py

Lines changed: 59 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,30 @@
33
from __future__ import annotations
44

55
import logging
6-
from typing import Any
6+
from typing import Any, cast
77

88
from web3 import AsyncWeb3, Web3
99
from web3.middleware import SignAndSendRawMiddlewareBuilder
1010
from web3.providers import WebSocketProvider
1111
from web3.providers.async_base import AsyncBaseProvider
1212
from web3.providers.base import BaseProvider
13-
14-
from arkiv.exceptions import NamedAccountNotFoundException
13+
from web3.types import Wei
1514

1615
from .account import NamedAccount
16+
from .client_base import ArkivBase
1717
from .module import ArkivModule
1818

1919
# Set up logger for Arkiv client
2020
logger = logging.getLogger(__name__)
2121

2222

23-
class Arkiv(Web3):
23+
class Arkiv(ArkivBase, Web3):
2424
"""
2525
Arkiv client that extends Web3 with entity management capabilities.
2626
2727
Provides the familiar client Web3.py interface plus client.arkiv.* methods for entity operations.
2828
"""
2929

30-
ACCOUNT_NAME_DEFAULT = "default"
31-
3230
def __init__(
3331
self,
3432
provider: BaseProvider | None = None,
@@ -69,59 +67,27 @@ def __init__(
6967
Note:
7068
Auto-node creation requires testcontainers: pip install arkiv-sdk[dev]
7169
"""
72-
# Self managed node instance (only created/used if no provider is provided)
73-
self.node: ArkivNode | None = None
74-
if provider is None:
75-
from .node import ArkivNode
76-
77-
logger.info("No provider given, creating managed ArkivNode...")
78-
self.node = ArkivNode()
70+
# Initialize base class first
71+
ArkivBase.__init__(self)
7972

80-
from .provider import ProviderBuilder
81-
82-
# Build HTTP provider for sync client
83-
built_provider = ProviderBuilder().node(self.node).build()
84-
assert isinstance(built_provider, BaseProvider) # Type narrowing
85-
provider = built_provider
86-
87-
# Create default account if none provided (for local node prototyping)
88-
if account is None:
89-
logger.info(
90-
f"Creating default account '{self.ACCOUNT_NAME_DEFAULT}' for local node..."
91-
)
92-
account = NamedAccount.create(self.ACCOUNT_NAME_DEFAULT)
73+
# Setup node and account using base class helper
74+
self.node, provider, account = self._setup_node_and_account(
75+
provider, account, "http"
76+
)
9377

9478
# Validate provider compatibility
9579
if provider is not None:
9680
self._validate_provider(provider)
9781

98-
super().__init__(provider, **kwargs)
82+
# Initialize Web3 parent
83+
Web3.__init__(self, provider, **kwargs)
9984

10085
# Initialize entity management module
10186
self.arkiv = ArkivModule(self)
10287

103-
# Initialize account management
104-
self.accounts: dict[str, NamedAccount] = {}
105-
self.current_signer: str | None = None
106-
10788
# Set account if provided
10889
if account:
109-
logger.debug(f"Initializing Arkiv client with account: {account.name}")
110-
self.accounts[account.name] = account
111-
self.switch_to(account.name)
112-
113-
# If client has node and account a zero balance, also fund the account with test ETH
114-
if self.node is not None and self.eth.get_balance(account.address) == 0:
115-
logger.info(
116-
f"Funding account {account.name} ({account.address}) with test ETH..."
117-
)
118-
self.node.fund_account(account)
119-
120-
balance = self.eth.get_balance(account.address)
121-
balance_eth = self.from_wei(balance, "ether")
122-
logger.info(
123-
f"Account balance for {account.name} ({account.address}): {balance_eth} ETH"
124-
)
90+
self._initialize_account(account)
12591
else:
12692
logger.debug("Initializing Arkiv client without default account")
12793

@@ -139,59 +105,32 @@ def __exit__(
139105
self.arkiv.cleanup_filters()
140106

141107
# Then stop the node if managed
142-
if self.node:
143-
logger.debug("Stopping managed ArkivNode...")
144-
self.node.stop()
145-
146-
def __del__(self) -> None:
147-
if self.node and self.node.is_running:
148-
logger.warning(
149-
"Arkiv client with managed node is being destroyed but node is still running. "
150-
"Call arkiv.node.stop() or use context manager: 'with Arkiv() as arkiv:'"
151-
)
152-
153-
def __repr__(self) -> str:
154-
"""String representation of Arkiv client."""
155-
return f"<Arkiv connected={self.is_connected()}>"
108+
self._cleanup_node()
156109

157-
def switch_to(self, account_name: str) -> None:
158-
"""Switch signer account to specified named account."""
159-
logger.info(f"Switching to account: {account_name}")
110+
# Implement abstract methods from ArkivBase
111+
def _is_connected(self) -> bool:
112+
"""Check if client is connected to provider."""
113+
return self.is_connected()
160114

161-
if account_name not in self.accounts:
162-
logger.error(
163-
f"Account '{account_name}' not found. Available accounts: {list(self.accounts.keys())}"
164-
)
165-
raise NamedAccountNotFoundException(
166-
f"Unknown account name: '{account_name}'"
167-
)
115+
def _get_balance(self, address: str) -> Wei:
116+
"""Get account balance."""
117+
return cast(Wei, self.eth.get_balance(address))
168118

169-
# Remove existing signing middleware if present
170-
if self.current_signer is not None:
171-
logger.debug(f"Removing existing signing middleware: {self.current_signer}")
172-
try:
173-
self.middleware_onion.remove(self.current_signer)
174-
except ValueError:
175-
logger.warning(
176-
"Middleware might have been removed elsewhere, continuing"
177-
)
178-
pass
119+
def _middleware_remove(self, name: str) -> None:
120+
"""Remove middleware by name."""
121+
self.middleware_onion.remove(name)
179122

180-
# Inject signer account
181-
account = self.accounts[account_name]
182-
logger.debug(f"Injecting signing middleware for account: {account.address}")
123+
def _middleware_inject(self, account: NamedAccount, name: str) -> None:
124+
"""Inject signing middleware for account."""
183125
self.middleware_onion.inject(
184126
SignAndSendRawMiddlewareBuilder.build(account.local_account),
185-
name=account_name,
127+
name=name,
186128
layer=0,
187129
)
188130

189-
# Configure default account
190-
self.eth.default_account = account.address
191-
self.current_signer = account_name
192-
logger.info(
193-
f"Successfully switched to account '{account_name}' ({account.address})"
194-
)
131+
def _set_default_account(self, address: str) -> None:
132+
"""Set the default account address."""
133+
self.eth.default_account = address
195134

196135
def _validate_provider(self, provider: BaseProvider) -> None:
197136
"""
@@ -217,15 +156,13 @@ def _validate_provider(self, provider: BaseProvider) -> None:
217156
)
218157

219158

220-
class AsyncArkiv(AsyncWeb3):
159+
class AsyncArkiv(ArkivBase, AsyncWeb3):
221160
"""
222161
Async Arkiv client that extends AsyncWeb3 with entity management capabilities.
223162
224163
Provides async client Web3.py interface plus client.arkiv.* methods for entity operations.
225164
"""
226165

227-
ACCOUNT_NAME_DEFAULT = "default"
228-
229166
def __init__(
230167
self,
231168
provider: AsyncBaseProvider | None = None,
@@ -266,41 +203,24 @@ def __init__(
266203
Note:
267204
Auto-node creation requires testcontainers: pip install arkiv-sdk[dev]
268205
"""
269-
# Self managed node instance (only created/used if no provider is provided)
270-
self.node: ArkivNode | None = None
271-
if provider is None:
272-
from .node import ArkivNode
273-
274-
logger.info("No provider given, creating managed ArkivNode...")
275-
self.node = ArkivNode()
276-
277-
from .provider import ProviderBuilder
206+
# Initialize base class first
207+
ArkivBase.__init__(self)
278208

279-
# Build WebSocket provider for async client
280-
built_provider = ProviderBuilder().node(self.node).ws().build()
281-
assert isinstance(built_provider, AsyncBaseProvider) # Type narrowing
282-
provider = built_provider
283-
284-
# Create default account if none provided (for local node prototyping)
285-
if account is None:
286-
logger.info(
287-
f"Creating default account '{self.ACCOUNT_NAME_DEFAULT}' for local node..."
288-
)
289-
account = NamedAccount.create(self.ACCOUNT_NAME_DEFAULT)
209+
# Setup node and account using base class helper
210+
self.node, provider, account = self._setup_node_and_account(
211+
provider, account, "ws"
212+
)
290213

291214
# Validate provider compatibility
292215
if provider is not None:
293216
self._validate_provider(provider)
294217

295-
super().__init__(provider, **kwargs)
218+
# Initialize AsyncWeb3 parent
219+
AsyncWeb3.__init__(self, provider, **kwargs)
296220

297221
# Note: Async version of ArkivModule not yet implemented
298222
# Will be added in future work
299223

300-
# Initialize account management
301-
self.accounts: dict[str, NamedAccount] = {}
302-
self.current_signer: str | None = None
303-
304224
# Cache for connection status (used by __repr__)
305225
self._cached_connected: bool | None = None
306226

@@ -350,21 +270,6 @@ async def __aexit__(
350270
# Then stop the node if managed and update cache
351271
self._cleanup_node()
352272

353-
def __del__(self) -> None:
354-
if self.node and self.node.is_running:
355-
logger.warning(
356-
"AsyncArkiv client with managed node is being destroyed but node is still running. "
357-
"Call arkiv.node.stop() or use context manager: 'async with AsyncArkiv() as arkiv:'"
358-
)
359-
360-
def __repr__(self) -> str:
361-
"""String representation of AsyncArkiv client."""
362-
# Use cached connection status to avoid async issues in sync method
363-
connected = (
364-
self._cached_connected if self._cached_connected is not None else False
365-
)
366-
return f"<AsyncArkiv connected={connected}>"
367-
368273
async def _initialize_account_async(self, account: NamedAccount) -> None:
369274
"""Initialize account asynchronously.
370275
@@ -392,49 +297,36 @@ async def _initialize_account_async(self, account: NamedAccount) -> None:
392297

393298
def _cleanup_node(self) -> None:
394299
"""Cleanup node and update connection cache."""
395-
if self.node:
396-
logger.debug("Stopping managed ArkivNode...")
397-
self.node.stop()
300+
super()._cleanup_node()
398301
self._cached_connected = False
399302

400-
def switch_to(self, account_name: str) -> None:
401-
"""Switch signer account to specified named account."""
402-
logger.info(f"Switching to account: {account_name}")
303+
# Implement abstract methods from ArkivBase
304+
def _is_connected(self) -> bool:
305+
"""Check if client is connected (uses cache to avoid async issues)."""
306+
return self._cached_connected if self._cached_connected is not None else False
403307

404-
if account_name not in self.accounts:
405-
logger.error(
406-
f"Account '{account_name}' not found. Available accounts: {list(self.accounts.keys())}"
407-
)
408-
raise NamedAccountNotFoundException(
409-
f"Unknown account name: '{account_name}'"
410-
)
308+
def _get_balance(self, address: str) -> Wei:
309+
"""Get account balance - not used in async client (raises error)."""
310+
raise RuntimeError(
311+
"_get_balance should not be called directly on AsyncArkiv. "
312+
"Use 'await client.eth.get_balance(address)' instead."
313+
)
411314

412-
# Remove existing signing middleware if present
413-
if self.current_signer is not None:
414-
logger.debug(f"Removing existing signing middleware: {self.current_signer}")
415-
try:
416-
self.middleware_onion.remove(self.current_signer)
417-
except ValueError:
418-
logger.warning(
419-
"Middleware might have been removed elsewhere, continuing"
420-
)
421-
pass
315+
def _middleware_remove(self, name: str) -> None:
316+
"""Remove middleware by name."""
317+
self.middleware_onion.remove(name)
422318

423-
# Inject signer account
424-
account = self.accounts[account_name]
425-
logger.debug(f"Injecting signing middleware for account: {account.address}")
319+
def _middleware_inject(self, account: NamedAccount, name: str) -> None:
320+
"""Inject signing middleware for account."""
426321
self.middleware_onion.inject(
427322
SignAndSendRawMiddlewareBuilder.build(account.local_account),
428-
name=account_name,
323+
name=name,
429324
layer=0,
430325
)
431326

432-
# Configure default account
433-
self.eth.default_account = account.address
434-
self.current_signer = account_name
435-
logger.info(
436-
f"Successfully switched to account '{account_name}' ({account.address})"
437-
)
327+
def _set_default_account(self, address: str) -> None:
328+
"""Set the default account address."""
329+
self.eth.default_account = address
438330

439331
def _validate_provider(self, provider: AsyncBaseProvider) -> None:
440332
"""

0 commit comments

Comments
 (0)