11"""Arkiv client - extends Web3 with entity management."""
22
3+ from __future__ import annotations
4+
35import logging
4- from typing import Any
6+ from typing import TYPE_CHECKING , Any
57
68from web3 import Web3
79from web3 .middleware import SignAndSendRawMiddlewareBuilder
1214from .account import NamedAccount
1315from .module import ArkivModule
1416
17+ if TYPE_CHECKING :
18+ pass
19+
1520# Set up logger for Arkiv client
1621logger = logging .getLogger (__name__ )
1722
@@ -20,7 +25,7 @@ class Arkiv(Web3):
2025 """
2126 Arkiv client that extends Web3 with entity management capabilities.
2227
23- Provides the familiar Web3.py interface plus arkiv.* methods for entity operations.
28+ Provides the familiar client Web3.py interface plus client. arkiv.* methods for entity operations.
2429 """
2530
2631 ACCOUNT_NAME_DEFAULT = "default"
@@ -32,11 +37,46 @@ def __init__(
3237 ** kwargs : Any ,
3338 ) -> None :
3439 """Initialize Arkiv client with Web3 provider.
40+
41+ If no Web3 provider is provided, a local development node is automatically created and started.
42+ Remember to call arkiv.node.stop() for cleanup, or use context manager:
43+
44+ Examples:
45+ Auto-managed local node:
46+ >>> with Arkiv() as arkiv:
47+ ... print(arkiv.eth.chain_id)
48+
49+ With account (auto-funded on local node):
50+ >>> account = NamedAccount.create("alice")
51+ >>> with Arkiv(account=account) as arkiv:
52+ ... balance = arkiv.eth.get_balance(account.address)
53+
54+ Custom provider:
55+ >>> provider = ProviderBuilder().kaolin().build()
56+ >>> arkiv = Arkiv(provider) # No auto-node
57+
3558 Args:
36- provider: Web3 provider instance (e.g., HTTPProvider)
37- account: Optional NamedAccount to use as the default signer
59+ provider: Web3 provider instance (e.g., HTTPProvider).
60+ If None, creates local ArkivNode (requires Docker and testcontainers).
61+ account: Optional NamedAccount to use as the default signer.
62+ Auto-funded with test ETH if using local node and balance is zero.
3863 **kwargs: Additional arguments passed to Web3 constructor
64+
65+ Note:
66+ Auto-node creation requires testcontainers: pip install arkiv-sdk[dev]
3967 """
68+ # Self managed node instance (only created/used if no provider is provided)
69+ self .node : ArkivNode | None = None
70+ if provider is None :
71+ from .node import ArkivNode
72+
73+ logger .info ("No provider given, creating managed ArkivNode..." )
74+ self .node = ArkivNode ()
75+
76+ from .provider import ProviderBuilder
77+
78+ provider = ProviderBuilder ().node (self .node ).build ()
79+
4080 super ().__init__ (provider , ** kwargs )
4181
4282 # Initialize entity management module
@@ -51,9 +91,42 @@ def __init__(
5191 logger .debug (f"Initializing Arkiv client with account: { account .name } " )
5292 self .accounts [account .name ] = account
5393 self .switch_to (account .name )
94+
95+ # If client has node and account a zero balance, also fund the account with test ETH
96+ if self .node is not None and self .eth .get_balance (account .address ) == 0 :
97+ logger .info (
98+ f"Funding account { account .name } ({ account .address } ) with test ETH..."
99+ )
100+ self .node .fund_account (account )
101+
102+ balance = self .eth .get_balance (account .address )
103+ balance_eth = self .from_wei (balance , "ether" )
104+ logger .info (
105+ f"Account balance for { account .name } ({ account .address } ): { balance_eth } ETH"
106+ )
54107 else :
55108 logger .debug ("Initializing Arkiv client without default account" )
56109
110+ def __enter__ (self ) -> Arkiv :
111+ return self
112+
113+ def __exit__ (
114+ self ,
115+ exc_type : type [BaseException ] | None ,
116+ exc_val : BaseException | None ,
117+ exc_tb : Any ,
118+ ) -> None :
119+ if self .node :
120+ logger .debug ("Stopping managed ArkivNode..." )
121+ self .node .stop ()
122+
123+ def __del__ (self ) -> None :
124+ if self .node and self .node .is_running ():
125+ logger .warning (
126+ "Arkiv client with managed node is being destroyed but node is still running. "
127+ "Call arkiv.node.stop() or use context manager: 'with Arkiv() as arkiv:'"
128+ )
129+
57130 def __repr__ (self ) -> str :
58131 """String representation of Arkiv client."""
59132 return f"<Arkiv connected={ self .is_connected ()} >"
0 commit comments