Skip to content

Commit 9584ef1

Browse files
author
Matthias Zimmermann
committed
add ArkivNode, auto node creation for Arkiv
1 parent 3d7f156 commit 9584ef1

File tree

9 files changed

+667
-49
lines changed

9 files changed

+667
-49
lines changed

pyproject.toml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ readme = "README.md"
66
requires-python = ">=3.12"
77
dependencies = [
88
"web3>=7.13.0,<7.14.0",
9-
"pytest>=8.4.2",
9+
]
10+
11+
[project.optional-dependencies]
12+
# Optional: Local development node support (ArkivNode)
13+
dev = [
14+
"testcontainers>=4.13.1",
15+
"websockets>=13.0",
1016
]
1117

1218
[build-system]
@@ -17,21 +23,20 @@ build-backend = "setuptools.build_meta"
1723
where = ["src"]
1824

1925
[dependency-groups]
20-
# Testing infrastructure
26+
# SDK development - testing
2127
test = [
22-
"testcontainers==4.13.1",
28+
"pytest>=8.4.2",
29+
"pytest-cov>=7.0.0",
30+
"testcontainers>=4.13.1",
2331
"websockets>=13.0",
32+
"python-dotenv>=1.0.0",
2433
]
2534

26-
# Code quality and linting
35+
# SDK development - linting and type checking
2736
lint = [
2837
"mypy>=1.13.0",
2938
"ruff==0.13.2",
3039
"pre-commit>=4.0.0",
31-
]# Development utilities
32-
dev = [
33-
"pytest-cov>=7.0.0",
34-
"python-dotenv>=1.0.0",
3540
]
3641

3742
[tool.pytest.ini_options]

src/arkiv/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Python SDK for Arkiv networks."""
22

33
from .client import Arkiv
4+
from .node import ArkivNode
45

56
__version__ = "0.1.0"
6-
__all__ = ["Arkiv"]
7+
__all__ = ["Arkiv", "ArkivNode"]

src/arkiv/client.py

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Arkiv client - extends Web3 with entity management."""
22

3+
from __future__ import annotations
4+
35
import logging
4-
from typing import Any
6+
from typing import TYPE_CHECKING, Any
57

68
from web3 import Web3
79
from web3.middleware import SignAndSendRawMiddlewareBuilder
@@ -12,6 +14,9 @@
1214
from .account import NamedAccount
1315
from .module import ArkivModule
1416

17+
if TYPE_CHECKING:
18+
pass
19+
1520
# Set up logger for Arkiv client
1621
logger = 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()}>"

src/arkiv/module.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,27 @@ def entity_exists(self, entity_key: EntityKey) -> bool:
174174
except Exception:
175175
return False
176176

177+
def transfer_eth(self, to: ChecksumAddress, amount_wei: int) -> TxHash:
178+
"""
179+
Transfer ETH to the given address.
180+
181+
Args:
182+
to: The recipient address or a named account
183+
amount_wei: The amount of ETH to transfer in wei
184+
185+
Returns:
186+
Transaction hash of the transfer
187+
"""
188+
tx_hash_bytes = self.client.eth.send_transaction(
189+
{
190+
"to": to,
191+
"value": Web3.to_wei(amount_wei, "wei"),
192+
"gas": 21000, # Standard gas for ETH transfer
193+
}
194+
)
195+
tx_hash = TxHash(HexStr(tx_hash_bytes.to_0x_hex()))
196+
return tx_hash
197+
177198
def get_entity(self, entity_key: EntityKey, fields: int = ALL) -> Entity:
178199
"""
179200
Get an entity by its entity key.

0 commit comments

Comments
 (0)