Skip to content

Commit c4c975e

Browse files
author
Matthias Zimmermann
committed
feat: add (Async)Arkiv constructor option to also accept a LocalAccount
1 parent 62f155f commit c4c975e

File tree

5 files changed

+142
-23
lines changed

5 files changed

+142
-23
lines changed

src/arkiv/account.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ def __init__(self, name: str, account: LocalAccount):
3535
name: Human-readable name for the account
3636
account: The LocalAccount instance to wrap
3737
"""
38-
self.name = self._check_and_trim(name)
39-
self._account = account
38+
self.name: str = self._check_and_trim(name)
39+
self._account: LocalAccount = account
4040

4141
def __repr__(self) -> str:
4242
"""String representation showing name and address."""

src/arkiv/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
from typing import Any, cast
77

8+
from eth_account.signers.local import LocalAccount
89
from web3 import AsyncWeb3, Web3
910
from web3.middleware import SignAndSendRawMiddlewareBuilder
1011
from web3.providers import WebSocketProvider
@@ -31,7 +32,7 @@ class Arkiv(ArkivBase, Web3):
3132
def __init__(
3233
self,
3334
provider: BaseProvider | None = None,
34-
account: NamedAccount | None = None,
35+
account: NamedAccount | LocalAccount | None = None,
3536
**kwargs: Any,
3637
) -> None:
3738
"""Initialize Arkiv client with Web3 provider.
@@ -167,7 +168,7 @@ class AsyncArkiv(ArkivBase, AsyncWeb3):
167168
def __init__(
168169
self,
169170
provider: AsyncBaseProvider | None = None,
170-
account: NamedAccount | None = None,
171+
account: NamedAccount | LocalAccount | None = None,
171172
**kwargs: Any,
172173
) -> None:
173174
"""Initialize AsyncArkiv client with async Web3 provider.

src/arkiv/client_base.py

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
from typing import TYPE_CHECKING, Any, Literal
77

8+
from eth_account.signers.local import LocalAccount
89
from web3.providers.async_base import AsyncBaseProvider
910
from web3.providers.base import BaseProvider
1011

@@ -93,27 +94,10 @@ def _create_managed_node_and_provider(
9394

9495
return node, provider
9596

96-
@staticmethod
97-
def _create_default_account(account_name: str = "default") -> NamedAccount:
98-
"""
99-
Create a default named account for local node prototyping.
100-
101-
Used by both Arkiv (sync) and AsyncArkiv (async) when no account is provided
102-
and a local node is auto-created.
103-
104-
Args:
105-
account_name: Name for the account (default: "default")
106-
107-
Returns:
108-
NamedAccount with the specified name
109-
"""
110-
logger.info(f"Creating default account '{account_name}' for local node...")
111-
return NamedAccount.create(account_name)
112-
11397
def _setup_node_and_account(
11498
self,
11599
provider: Any | None,
116-
account: NamedAccount | None,
100+
account: NamedAccount | LocalAccount | None,
117101
transport: Literal["http", "ws"],
118102
) -> tuple[ArkivNode | None, Any, NamedAccount | None]:
119103
"""
@@ -133,7 +117,17 @@ def _setup_node_and_account(
133117

134118
# Create default account if none provided (for local node prototyping)
135119
if account is None:
136-
account = self._create_default_account()
120+
logger.debug(
121+
f"Creating default account '{self.ACCOUNT_NAME_DEFAULT}' for local node..."
122+
)
123+
account = NamedAccount.create(self.ACCOUNT_NAME_DEFAULT)
124+
125+
# If account is a LocalAccount, wrap it in NamedAccount with default name
126+
if isinstance(account, LocalAccount):
127+
logger.debug(
128+
f"Wrapping provided LocalAccount in NamedAccount with name '{self.ACCOUNT_NAME_DEFAULT}'"
129+
)
130+
account = NamedAccount(self.ACCOUNT_NAME_DEFAULT, account)
137131

138132
return node, provider, account
139133

tests/test_arkiv_create.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,65 @@ def test_create_arkiv_with_account(self, arkiv_node) -> None:
5454
_assert_arkiv_client_properties(client, account, "With Account")
5555
logger.info("Created Arkiv client with default account")
5656

57+
def test_create_arkiv_with_local_account(self, arkiv_node) -> None:
58+
"""Test creating Arkiv client with LocalAccount (gets wrapped in NamedAccount)."""
59+
from eth_account import Account
60+
61+
provider = HTTPProvider(arkiv_node.http_url)
62+
local_account = Account.create()
63+
client = Arkiv(provider, account=local_account)
64+
65+
# LocalAccount should be wrapped in NamedAccount with default name
66+
assert len(client.accounts) == 1, "Should have one account registered"
67+
assert "default" in client.accounts, (
68+
"LocalAccount should be wrapped with 'default' name"
69+
)
70+
assert client.current_signer == "default", "Should use 'default' as signer name"
71+
assert client.eth.default_account == local_account.address, (
72+
"Should set default account to LocalAccount address"
73+
)
74+
assert client.accounts["default"].address == local_account.address, (
75+
"Wrapped account should have same address as original LocalAccount"
76+
)
77+
78+
logger.info("Created Arkiv client with LocalAccount (wrapped in NamedAccount)")
79+
80+
def test_create_arkiv_with_local_account_then_add_named_default(
81+
self, arkiv_node
82+
) -> None:
83+
"""Test that adding another 'default' named account overwrites the wrapped LocalAccount."""
84+
from eth_account import Account
85+
86+
provider = HTTPProvider(arkiv_node.http_url)
87+
local_account = Account.create()
88+
client = Arkiv(provider, account=local_account)
89+
90+
# LocalAccount wrapped as "default"
91+
original_address = client.accounts["default"].address
92+
assert original_address == local_account.address
93+
94+
# Now manually add another account also named "default"
95+
new_account = NamedAccount.create("default")
96+
client.accounts["default"] = new_account
97+
98+
# The new account should overwrite the old one
99+
assert len(client.accounts) == 1, "Should still have one account"
100+
assert client.accounts["default"].address == new_account.address, (
101+
"New account should replace the wrapped LocalAccount"
102+
)
103+
assert client.accounts["default"].address != original_address, (
104+
"Address should be different from original"
105+
)
106+
107+
# Switch to the new account
108+
client.switch_to("default")
109+
assert client.current_signer == "default"
110+
assert client.eth.default_account == new_account.address
111+
112+
logger.info(
113+
"Successfully replaced wrapped LocalAccount with new 'default' named account"
114+
)
115+
57116
def test_create_arkiv_with_kwargs(self, arkiv_node) -> None:
58117
"""Test creating Arkiv client with additional kwargs."""
59118
provider = HTTPProvider(arkiv_node.http_url)

tests/test_async_arkiv_create.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,71 @@ async def test_create_asyncarkiv_with_account(self, arkiv_node) -> None:
4141
await _assert_asyncarkiv_client_properties(client, account, "With Account")
4242
logger.info("Created AsyncArkiv client with default account")
4343

44+
@pytest.mark.asyncio
45+
async def test_create_asyncarkiv_with_local_account(self, arkiv_node) -> None:
46+
"""Test creating AsyncArkiv client with LocalAccount (gets wrapped in NamedAccount)."""
47+
from eth_account import Account
48+
49+
provider = ProviderBuilder().node(arkiv_node).async_mode().build()
50+
local_account = Account.create()
51+
52+
async with AsyncArkiv(provider, account=local_account) as client:
53+
# LocalAccount should be wrapped in NamedAccount with default name
54+
assert len(client.accounts) == 1, "Should have one account registered"
55+
assert "default" in client.accounts, (
56+
"LocalAccount should be wrapped with 'default' name"
57+
)
58+
assert client.current_signer == "default", (
59+
"Should use 'default' as signer name"
60+
)
61+
assert client.eth.default_account == local_account.address, (
62+
"Should set default account to LocalAccount address"
63+
)
64+
assert client.accounts["default"].address == local_account.address, (
65+
"Wrapped account should have same address as original LocalAccount"
66+
)
67+
68+
logger.info(
69+
"Created AsyncArkiv client with LocalAccount (wrapped in NamedAccount)"
70+
)
71+
72+
@pytest.mark.asyncio
73+
async def test_create_asyncarkiv_with_local_account_then_add_named_default(
74+
self, arkiv_node
75+
) -> None:
76+
"""Test that adding another 'default' named account overwrites the wrapped LocalAccount."""
77+
from eth_account import Account
78+
79+
provider = ProviderBuilder().node(arkiv_node).async_mode().build()
80+
local_account = Account.create()
81+
82+
async with AsyncArkiv(provider, account=local_account) as client:
83+
# LocalAccount wrapped as "default"
84+
original_address = client.accounts["default"].address
85+
assert original_address == local_account.address
86+
87+
# Now manually add another account also named "default"
88+
new_account = NamedAccount.create("default")
89+
client.accounts["default"] = new_account
90+
91+
# The new account should overwrite the old one
92+
assert len(client.accounts) == 1, "Should still have one account"
93+
assert client.accounts["default"].address == new_account.address, (
94+
"New account should replace the wrapped LocalAccount"
95+
)
96+
assert client.accounts["default"].address != original_address, (
97+
"Address should be different from original"
98+
)
99+
100+
# Switch to the new account
101+
client.switch_to("default")
102+
assert client.current_signer == "default"
103+
assert client.eth.default_account == new_account.address
104+
105+
logger.info(
106+
"Successfully replaced wrapped LocalAccount with new 'default' named account"
107+
)
108+
44109
@pytest.mark.asyncio
45110
async def test_create_asyncarkiv_with_kwargs(self, arkiv_node) -> None:
46111
"""Test creating AsyncArkiv client with additional kwargs."""

0 commit comments

Comments
 (0)