33from __future__ import annotations
44
55import logging
6- from typing import Any
6+ from typing import Any , cast
77
88from web3 import AsyncWeb3 , Web3
99from web3 .middleware import SignAndSendRawMiddlewareBuilder
1010from web3 .providers import WebSocketProvider
1111from web3 .providers .async_base import AsyncBaseProvider
1212from web3 .providers .base import BaseProvider
13-
14- from arkiv .exceptions import NamedAccountNotFoundException
13+ from web3 .types import Wei
1514
1615from .account import NamedAccount
16+ from .client_base import ArkivBase
1717from .module import ArkivModule
1818
1919# Set up logger for Arkiv client
2020logger = 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