Skip to content

Commit 0048a79

Browse files
author
Matthias Zimmermann
committed
add arkiv.extend_entity functionality
1 parent b937e1f commit 0048a79

File tree

4 files changed

+306
-3
lines changed

4 files changed

+306
-3
lines changed

src/arkiv/module.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
DeleteOp,
2222
Entity,
2323
EntityKey,
24+
ExtendOp,
2425
Operations,
2526
TransactionReceipt,
2627
TxHash,
@@ -160,6 +161,38 @@ def create_entities(
160161
entity_keys = [create.entity_key for create in receipt.creates]
161162
return entity_keys, receipt.tx_hash
162163

164+
def extend_entity(
165+
self,
166+
entity_key: EntityKey,
167+
number_of_blocks: int,
168+
tx_params: TxParams | None = None,
169+
) -> TxHash:
170+
"""
171+
Extend the lifetime of an entity by a specified number of blocks.
172+
173+
Args:
174+
entity_key: The entity key to extend
175+
number_of_blocks: Number of blocks to extend the entity's lifetime
176+
tx_params: Optional additional transaction parameters
177+
178+
Returns:
179+
Transaction hash of the extend operation
180+
"""
181+
# Create the extend operation
182+
extend_op = ExtendOp(entity_key=entity_key, number_of_blocks=number_of_blocks)
183+
184+
# Wrap in Operations container and execute
185+
operations = Operations(extensions=[extend_op])
186+
receipt = self.execute(operations, tx_params)
187+
188+
# Verify the extend succeeded
189+
if len(receipt.extensions) != 1:
190+
raise RuntimeError(
191+
f"Expected 1 extension in receipt, got {len(receipt.extensions)}"
192+
)
193+
194+
return receipt.tx_hash
195+
163196
def delete_entity(
164197
self,
165198
entity_key: EntityKey,

tests/test_entity_delete.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from arkiv.client import Arkiv
88
from arkiv.types import Annotations, CreateOp, DeleteOp, Operations
99

10-
from .utils import check_tx_hash
10+
from .utils import bulk_create_entities, check_tx_hash
1111

1212
logger = logging.getLogger(__name__)
1313

@@ -100,7 +100,9 @@ def test_delete_entity_execute_bulk(self, arkiv_client_http: Arkiv) -> None:
100100
for i in range(3)
101101
]
102102

103-
entity_keys, _ = arkiv_client_http.arkiv.create_entities(create_ops)
103+
entity_keys = bulk_create_entities(
104+
arkiv_client_http, create_ops, label="bulk_delete_test"
105+
)
104106

105107
logger.info(f"Created {len(entity_keys)} entities in bulk")
106108

tests/test_entity_extend.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""Tests for entity extension functionality in ArkivModule."""
2+
3+
import logging
4+
5+
import pytest
6+
from eth_typing import HexStr
7+
from web3.exceptions import Web3RPCError
8+
9+
from arkiv.client import Arkiv
10+
from arkiv.types import Annotations, CreateOp, ExtendOp, Operations
11+
12+
from .utils import bulk_create_entities, check_tx_hash
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class TestEntityExtend:
18+
"""Test cases for extend_entity function."""
19+
20+
def test_extend_entity_simple(self, arkiv_client_http: Arkiv) -> None:
21+
"""Test extending a single entity's lifetime."""
22+
# Create an entity to extend
23+
payload = b"Test entity for extension"
24+
annotations: Annotations = Annotations({"type": "test", "purpose": "extension"})
25+
btl = 100
26+
27+
entity_key, _ = arkiv_client_http.arkiv.create_entity(
28+
payload=payload, annotations=annotations, btl=btl
29+
)
30+
31+
logger.info(f"Created entity {entity_key} for extension test")
32+
33+
# Get initial expiration block
34+
entity_before = arkiv_client_http.arkiv.get_entity(entity_key)
35+
initial_expiration = entity_before.expires_at_block
36+
assert initial_expiration is not None, "Entity should have expiration block"
37+
logger.info(f"Initial expiration block: {initial_expiration}")
38+
39+
# Extend the entity by 50 blocks
40+
number_of_blocks = 50
41+
extend_tx_hash = arkiv_client_http.arkiv.extend_entity(
42+
entity_key, number_of_blocks
43+
)
44+
45+
label = "extend_entity"
46+
check_tx_hash(label, extend_tx_hash)
47+
logger.info(
48+
f"{label}: Extended entity {entity_key} by {number_of_blocks} blocks, tx_hash: {extend_tx_hash}"
49+
)
50+
51+
# Verify the entity still exists and expiration increased
52+
entity_after = arkiv_client_http.arkiv.get_entity(entity_key)
53+
assert entity_after.expires_at_block == initial_expiration + number_of_blocks, (
54+
f"Expiration should increase by {number_of_blocks} blocks"
55+
)
56+
57+
logger.info(
58+
f"{label}: Entity expiration increased from {initial_expiration} to {entity_after.expires_at_block}"
59+
)
60+
61+
def test_extend_entity_multiple_times(self, arkiv_client_http: Arkiv) -> None:
62+
"""Test extending the same entity multiple times."""
63+
# Create an entity
64+
entity_key, _ = arkiv_client_http.arkiv.create_entity(
65+
payload=b"Entity for multiple extensions", btl=100
66+
)
67+
68+
# Get initial expiration
69+
entity = arkiv_client_http.arkiv.get_entity(entity_key)
70+
initial_expiration = entity.expires_at_block
71+
assert initial_expiration is not None, "Entity should have expiration block"
72+
73+
# Extend multiple times
74+
extensions = [20, 30, 50]
75+
for i, blocks in enumerate(extensions):
76+
extend_tx_hash = arkiv_client_http.arkiv.extend_entity(entity_key, blocks)
77+
check_tx_hash(f"extend_{i}", extend_tx_hash)
78+
logger.info(f"Extension {i + 1}: extended by {blocks} blocks")
79+
80+
# Verify final expiration
81+
entity_final = arkiv_client_http.arkiv.get_entity(entity_key)
82+
expected_expiration = initial_expiration + sum(extensions)
83+
assert entity_final.expires_at_block == expected_expiration, (
84+
f"Expiration should be {expected_expiration}"
85+
)
86+
87+
logger.info(
88+
f"Multiple extensions successful: {initial_expiration} -> {entity_final.expires_at_block}"
89+
)
90+
91+
def test_extend_entity_execute_bulk(self, arkiv_client_http: Arkiv) -> None:
92+
"""Test extending multiple entities in a single transaction."""
93+
# Create entities using bulk transaction
94+
create_ops = [
95+
CreateOp(
96+
payload=f"Bulk entity {i}".encode(),
97+
annotations=Annotations({"batch": "bulk", "index": i}),
98+
btl=100,
99+
)
100+
for i in range(3)
101+
]
102+
103+
# Use helper function for bulk creation
104+
entity_keys = bulk_create_entities(
105+
arkiv_client_http, create_ops, "create_bulk_entity"
106+
)
107+
108+
# Get initial expirations
109+
initial_expirations = {}
110+
for entity_key in entity_keys:
111+
entity = arkiv_client_http.arkiv.get_entity(entity_key)
112+
expires_at = entity.expires_at_block
113+
assert expires_at is not None, "Entity should have expiration block"
114+
initial_expirations[entity_key] = expires_at
115+
116+
# Bulk extend
117+
number_of_blocks = 200
118+
extend_ops = [
119+
ExtendOp(entity_key=key, number_of_blocks=number_of_blocks)
120+
for key in entity_keys
121+
]
122+
operations = Operations(extensions=extend_ops)
123+
receipt = arkiv_client_http.arkiv.execute(operations)
124+
125+
# Check transaction hash of bulk extend
126+
check_tx_hash("extend_bulk_entity", receipt.tx_hash)
127+
128+
# Verify all extensions succeeded
129+
if len(receipt.extensions) != len(extend_ops):
130+
raise RuntimeError(
131+
f"Expected {len(extend_ops)} extensions in receipt, got {len(receipt.extensions)}"
132+
)
133+
134+
# Verify all expirations increased
135+
for entity_key in entity_keys:
136+
entity = arkiv_client_http.arkiv.get_entity(entity_key)
137+
expected_expiration = initial_expirations[entity_key] + number_of_blocks
138+
assert entity.expires_at_block == expected_expiration, (
139+
f"Entity {entity_key} expiration should be {expected_expiration}"
140+
)
141+
142+
logger.info("Bulk extension of entities successful")
143+
144+
def test_extend_nonexistent_entity_behavior(self, arkiv_client_http: Arkiv) -> None:
145+
"""Test that extending a non-existent entity raises an exception."""
146+
from arkiv.types import EntityKey
147+
148+
# Create a fake entity key (should not exist)
149+
fake_entity_key = EntityKey(
150+
HexStr("0x0000000000000000000000000000000000000000000000000000000000000001")
151+
)
152+
153+
# Verify it doesn't exist
154+
assert not arkiv_client_http.arkiv.entity_exists(fake_entity_key), (
155+
"Fake entity should not exist"
156+
)
157+
158+
# Attempt to extend should raise a Web3RPCError
159+
with pytest.raises(Web3RPCError) as exc_info:
160+
arkiv_client_http.arkiv.extend_entity(fake_entity_key, 100)
161+
162+
# Verify the error message indicates entity not found
163+
error_message = str(exc_info.value)
164+
assert "entity" in error_message.lower(), "Error message should mention entity"
165+
assert "not found" in error_message.lower(), (
166+
"Error message should indicate entity not found"
167+
)
168+
169+
logger.info(
170+
f"Extend of non-existent entity correctly raised {type(exc_info.value).__name__}"
171+
)
172+
173+
def test_extend_deleted_entity_behavior(self, arkiv_client_http: Arkiv) -> None:
174+
"""Test that extending a deleted entity raises an exception."""
175+
# Create an entity
176+
entity_key, _ = arkiv_client_http.arkiv.create_entity(
177+
payload=b"Entity to delete then extend", btl=100
178+
)
179+
180+
# Delete the entity
181+
delete_tx_hash = arkiv_client_http.arkiv.delete_entity(entity_key)
182+
check_tx_hash("delete_before_extend", delete_tx_hash)
183+
184+
# Verify it's deleted
185+
assert not arkiv_client_http.arkiv.entity_exists(entity_key), (
186+
"Entity should be deleted"
187+
)
188+
189+
# Attempt to extend should raise a Web3RPCError
190+
with pytest.raises(Web3RPCError) as exc_info:
191+
arkiv_client_http.arkiv.extend_entity(entity_key, 100)
192+
193+
# Verify the error message indicates entity not found
194+
error_message = str(exc_info.value)
195+
assert "entity" in error_message.lower(), "Error message should mention entity"
196+
assert "not found" in error_message.lower(), (
197+
"Error message should indicate entity not found"
198+
)
199+
200+
logger.info(
201+
f"Extend of deleted entity correctly raised {type(exc_info.value).__name__}"
202+
)
203+
204+
def test_extend_entity_minimal_blocks(self, arkiv_client_http: Arkiv) -> None:
205+
"""Test extending an entity by a minimal number of blocks."""
206+
# Create an entity
207+
entity_key, _ = arkiv_client_http.arkiv.create_entity(
208+
payload=b"Entity for minimal extension", btl=100
209+
)
210+
211+
# Get initial expiration
212+
entity_before = arkiv_client_http.arkiv.get_entity(entity_key)
213+
initial_expiration = initial_expiration = entity_before.expires_at_block
214+
assert initial_expiration is not None, "Entity should have expiration block"
215+
216+
# Extend by just 1 block
217+
extend_tx_hash = arkiv_client_http.arkiv.extend_entity(entity_key, 1)
218+
check_tx_hash("extend_minimal", extend_tx_hash)
219+
220+
# Verify expiration increased by 1
221+
entity_after = arkiv_client_http.arkiv.get_entity(entity_key)
222+
assert entity_after.expires_at_block == initial_expiration + 1, (
223+
"Expiration should increase by 1 block"
224+
)
225+
226+
logger.info(
227+
f"Minimal extension successful: {initial_expiration} -> {entity_after.expires_at_block}"
228+
)

tests/utils.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import logging
22
import os
33
from pathlib import Path
4+
from typing import TYPE_CHECKING
45

56
from arkiv.account import NamedAccount
6-
from arkiv.types import TxHash
7+
from arkiv.types import CreateOp, EntityKey, Operations, TxHash
8+
9+
if TYPE_CHECKING:
10+
from arkiv.client import Arkiv
711

812
WALLET_FILE_ENV_PREFIX = "WALLET_FILE"
913
WALLET_PASSWORD_ENV_PREFIX = "WALLET_PASSWORD"
@@ -24,6 +28,42 @@ def check_tx_hash(label: str, tx_hash: TxHash) -> None:
2428
assert tx_hash.startswith("0x"), f"{label}: Transaction hash should start with 0x"
2529

2630

31+
def bulk_create_entities(
32+
client: "Arkiv", create_ops: list[CreateOp], label: str = "bulk_create"
33+
) -> list[EntityKey]:
34+
"""Create multiple entities in a single bulk transaction.
35+
36+
Args:
37+
client: Arkiv client instance
38+
create_ops: List of CreateOp operations to execute
39+
label: Label for transaction hash validation logging
40+
41+
Returns:
42+
List of entity keys created
43+
44+
Raises:
45+
RuntimeError: If the number of creates in receipt doesn't match operations
46+
"""
47+
# Use execute() for bulk creation
48+
create_operations = Operations(creates=create_ops)
49+
create_receipt = client.arkiv.execute(create_operations)
50+
51+
# Check transaction hash of bulk create
52+
check_tx_hash(label, create_receipt.tx_hash)
53+
54+
# Verify all creates succeeded
55+
if len(create_receipt.creates) != len(create_ops):
56+
raise RuntimeError(
57+
f"Expected {len(create_ops)} creates in receipt, got {len(create_receipt.creates)}"
58+
)
59+
60+
# Extract and return entity keys from receipt
61+
entity_keys = [create.entity_key for create in create_receipt.creates]
62+
logger.info(f"{label}: Created {len(entity_keys)} entities in bulk transaction")
63+
64+
return entity_keys
65+
66+
2767
def create_account(index: int, name: str) -> NamedAccount:
2868
"""Create a named account from env vars or generate a new one."""
2969
wallet_file = os.getenv(f"{WALLET_FILE_ENV_PREFIX}_{index}")

0 commit comments

Comments
 (0)