From a17b8c30a5ce7dbf0cccb7aee1d0429d530bd1b3 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 28 Aug 2025 15:08:01 +0200 Subject: [PATCH 01/32] Websockets wip --- starknet_py/net/client_models.py | 21 ++--- starknet_py/net/schemas/rpc/event.py | 3 +- starknet_py/net/schemas/rpc/websockets.py | 61 +++++++++++---- starknet_py/net/websockets/models.py | 41 +++++----- .../net/websockets/websocket_client.py | 78 ++++++++++++++++--- 5 files changed, 151 insertions(+), 53 deletions(-) diff --git a/starknet_py/net/client_models.py b/starknet_py/net/client_models.py index c05f35b0b..90cdded97 100644 --- a/starknet_py/net/client_models.py +++ b/starknet_py/net/client_models.py @@ -36,6 +36,16 @@ LatestTag = Literal["latest"] +class TransactionFinalityStatus(Enum): + """ + Enum representing transaction finality statuses. + """ + + PRE_CONFIRMED = "PRE_CONFIRMED" + ACCEPTED_ON_L2 = "ACCEPTED_ON_L2" + ACCEPTED_ON_L1 = "ACCEPTED_ON_L1" + + @dataclass class Call: """ @@ -68,6 +78,7 @@ class EmittedEvent(Event): """ transaction_hash: int + finality_status: TransactionFinalityStatus block_hash: Optional[int] = None block_number: Optional[int] = None @@ -411,16 +422,6 @@ class TransactionExecutionStatus(Enum): REVERTED = "REVERTED" -class TransactionFinalityStatus(Enum): - """ - Enum representing transaction finality statuses. - """ - - PRE_CONFIRMED = "PRE_CONFIRMED" - ACCEPTED_ON_L2 = "ACCEPTED_ON_L2" - ACCEPTED_ON_L1 = "ACCEPTED_ON_L1" - - @dataclass class InnerCallExecutionResources: """ diff --git a/starknet_py/net/schemas/rpc/event.py b/starknet_py/net/schemas/rpc/event.py index 4b80ff362..ce0a36e35 100644 --- a/starknet_py/net/schemas/rpc/event.py +++ b/starknet_py/net/schemas/rpc/event.py @@ -1,7 +1,7 @@ from marshmallow import fields, post_load from starknet_py.net.client_models import EmittedEvent, Event, EventsChunk -from starknet_py.net.schemas.common import Felt +from starknet_py.net.schemas.common import Felt, FinalityStatusField from starknet_py.utils.schema import Schema @@ -17,6 +17,7 @@ def make_dataclass(self, data, **kwargs) -> Event: class EmittedEventSchema(EventSchema): transaction_hash = Felt(data_key="transaction_hash", required=True) + finality_status = FinalityStatusField(data_key="finality_status", required=True) block_hash = Felt(data_key="block_hash", load_default=None) block_number = fields.Integer(data_key="block_number", load_default=None) diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index 72c167a3a..d3edcaf8c 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -4,21 +4,23 @@ from starknet_py.net.client_models import Transaction from starknet_py.net.client_utils import _to_rpc_felt -from starknet_py.net.schemas.common import Felt +from starknet_py.net.schemas.common import Felt, TransactionFinalityStatusField from starknet_py.net.schemas.rpc.block import BlockHeaderSchema from starknet_py.net.schemas.rpc.event import EmittedEventSchema from starknet_py.net.schemas.rpc.transactions import ( TransactionStatusResponseSchema, TypesOfTransactionsSchema, + TransactionReceiptSchema, ) from starknet_py.net.websockets.models import ( NewEventsNotification, NewHeadsNotification, NewTransactionStatus, - PendingTransactionsNotification, ReorgData, ReorgNotification, TransactionStatusNotification, + NewTransactionReceiptsNotification, + NewTransactionNotification, ) from starknet_py.utils.schema import Schema @@ -89,17 +91,6 @@ def _deserialize( ) -class PendingTransactionsNotificationSchema(Schema): - subscription_id = fields.Str(data_key="subscription_id", required=True) - result = PendingTransactionsNotificationResultField( - data_key="result", required=True - ) - - @post_load - def make_dataclass(self, data, **kwargs) -> PendingTransactionsNotification: - return PendingTransactionsNotification(**data) - - class ReorgDataSchema(Schema): starting_block_hash = Felt(data_key="starting_block_hash", required=True) starting_block_number = fields.Integer( @@ -122,3 +113,47 @@ class ReorgNotificationSchema(Schema): @post_load def make_dataclass(self, data, **kwargs) -> ReorgNotification: return ReorgNotification(**data) + + +class NewTransactionReceiptsNotificationSchema(Schema): + subscription_id = fields.Str(data_key="subscription_id", required=True) + result = fields.Nested(TransactionReceiptSchema(), data_key="result", required=True) + + @post_load + def make_dataclass(self, data, **kwargs) -> NewTransactionReceiptsNotification: + return NewTransactionReceiptsNotification(**data) + + +class TypesOfTransactionWithFinalitySchema(TypesOfTransactionsSchema): + + def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): + if not isinstance(data, dict): + raise ValidationError( + "Invalid payload for transaction with finality_status." + ) + + finality_status_field = TransactionFinalityStatusField( + data_key="finality_status", required=True + ) + finality_status = finality_status_field.deserialize( + data.get("finality_status"), + attr="finality_status", + data=data, + ) + + tx_payload = dict(data) + tx_payload.pop("finality_status", None) + tx_obj = super().load(tx_payload, many=many, partial=partial, unknown=unknown) + + return {"transaction": tx_obj, "finality_status": finality_status} + + +class NewTransactionNotificationSchema(Schema): + subscription_id = fields.Str(data_key="subscription_id", required=True) + result = fields.Nested( + TypesOfTransactionWithFinalitySchema(), data_key="result", required=True + ) + + @post_load + def make_dataclass(self, data, **kwargs) -> NewTransactionNotification: + return NewTransactionNotification(**data) diff --git a/starknet_py/net/websockets/models.py b/starknet_py/net/websockets/models.py index 7b8bb2f82..5f7efc38c 100644 --- a/starknet_py/net/websockets/models.py +++ b/starknet_py/net/websockets/models.py @@ -9,6 +9,9 @@ BlockHeader, EmittedEvent, TransactionStatusResponse, + TransactionReceipt, + Transaction, + TransactionFinalityStatus, ) from starknet_py.net.models import ( DeclareV1, @@ -64,24 +67,6 @@ class TransactionStatusNotification(Notification[NewTransactionStatus]): """ -Transaction = Union[ - DeclareV1, - DeclareV2, - DeclareV3, - DeployAccountV1, - DeployAccountV3, - InvokeV1, - InvokeV3, -] - - -@dataclass -class PendingTransactionsNotification(Notification[Union[int, Transaction]]): - """ - Notification to the client of a new pending transaction. - """ - - @dataclass class ReorgData: """ @@ -99,3 +84,23 @@ class ReorgNotification(Notification[ReorgData]): """ Notification of a reorganization of the chain. """ + + +@dataclass +class NewTransactionReceiptsNotification(Notification[TransactionReceipt]): + """ + Notification of a new transaction receipt + """ + + +@dataclass +class NewTransactionNotificationResult: + transaction: Transaction + finality_status: TransactionFinalityStatus + + +@dataclass +class NewTransactionNotification(Notification[NewTransactionNotificationResult]): + """ + Notification of a new transaction + """ diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index b717766f4..3b1ee1d1d 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -10,24 +10,25 @@ from starknet_py.net.schemas.rpc.websockets import ( NewEventsNotificationSchema, NewHeadsNotificationSchema, - PendingTransactionsNotificationSchema, ReorgNotificationSchema, TransactionStatusNotificationSchema, + NewTransactionReceiptsNotificationSchema, + NewTransactionNotificationSchema, ) from starknet_py.net.websockets.errors import WebsocketClientError from starknet_py.net.websockets.models import ( NewEventsNotification, NewHeadsNotification, - PendingTransactionsNotification, ReorgNotification, TransactionStatusNotification, + NewTransactionNotification, + NewTransactionReceiptsNotification, ) Notification = Union[ NewHeadsNotification, NewEventsNotification, TransactionStatusNotification, - PendingTransactionsNotification, ReorgNotification, ] NotificationHandler = Callable[[Notification], Any] @@ -36,16 +37,18 @@ "starknet_subscriptionNewHeads", "starknet_subscriptionEvents", "starknet_subscriptionTransactionStatus", - "starknet_subscriptionPendingTransactions", "starknet_subscriptionReorg", + "starknet_subscriptionNewTransactionReceipts", + "starknet_subscriptionNewTransaction", ] _NOTIFICATION_SCHEMA_MAPPING = { "starknet_subscriptionNewHeads": NewHeadsNotificationSchema, "starknet_subscriptionEvents": NewEventsNotificationSchema, "starknet_subscriptionTransactionStatus": TransactionStatusNotificationSchema, - "starknet_subscriptionPendingTransactions": PendingTransactionsNotificationSchema, "starknet_subscriptionReorg": ReorgNotificationSchema, + "starknet_subscriptionNewTransactionReceipts": NewTransactionReceiptsNotificationSchema, + "starknet_subscriptionNewTransaction": NewTransactionNotificationSchema, } @@ -125,6 +128,7 @@ async def subscribe_events( keys: Optional[List[List[int]]] = None, block_hash: Optional[Union[Hash, LatestTag]] = None, block_number: Optional[Union[int, LatestTag]] = None, + finality_status: Optional[Literal["pre_confirmed", "accepted_on_l2"]] = None, ) -> str: """ Creates a WebSocket stream which will fire events for new Starknet events with applied filters. @@ -144,6 +148,13 @@ async def subscribe_events( params["keys"] = [ [_to_rpc_felt(key) for key in key_group] for key_group in keys ] + if finality_status is not None: + if finality_status == "pre_confirmed": + params["finality_status"] = "PRE_CONFIRMED" + elif finality_status == "accepted_on_l2": + params["finality_status"] = "ACCEPTED_ON_L2" + else: + raise ValueError(f"Invalid finality status: {finality_status}") block_id = get_block_identifier(block_hash, block_number, "latest") params = { **params, @@ -176,11 +187,13 @@ async def subscribe_transaction_status( return subscription_id - async def subscribe_pending_transactions( + async def subscribe_new_transactions( self, - handler: Callable[[PendingTransactionsNotification], Any], - transaction_details: Optional[bool] = None, + handler: Callable[[NewTransactionNotification], Any], sender_address: Optional[List[int]] = None, + finality_status: Optional[ + Literal["received", "candidate", "pre_confirmed", "accepted_on_l2"] + ] = None, ) -> str: """ Creates a WebSocket stream which will fire events when a new pending transaction is added. @@ -193,15 +206,58 @@ async def subscribe_pending_transactions( :return: The subscription ID. """ params = {} - if transaction_details is not None: - params["transaction_details"] = transaction_details if sender_address is not None: params["sender_address"] = [ _to_rpc_felt(address) for address in sender_address ] + if finality_status is not None: + if finality_status == "received": + params["finality_status"] = "RECEIVED" + elif finality_status == "candidate": + params["finality_status"] = "CANDIDATE" + elif finality_status == "pre_confirmed": + params["finality_status"] = "PRE_CONFIRMED" + elif finality_status == "accepted_on_l2": + params["finality_status"] = "ACCEPTED_ON_L2" + else: + raise ValueError(f"Invalid finality status: {finality_status}") subscription_id = await self._subscribe( - handler, "starknet_subscribePendingTransactions", params + handler, "starknet_subscribeNewTransactions", params + ) + return subscription_id + + async def subscribe_new_transaction_receipts( + self, + handler: Callable[[NewTransactionReceiptsNotification], Any], + sender_address: Optional[List[int]] = None, + finality_status: Optional[Literal["pre_confirmed", "accepted_on_l2"]] = None, + ) -> str: + """ + Creates a WebSocket stream which will fire events when a new pending transaction is added. + While there is no mempool, this notifies of transactions in the pending block. + + :param handler: The function to call when a new pending transaction is received. + :param transaction_details: If false, only hash is returned, otherwise full transaction details. + :param sender_address: The sender address to filter transactions by. + + :return: The subscription ID. + """ + params = {} + if sender_address is not None: + params["sender_address"] = [ + _to_rpc_felt(address) for address in sender_address + ] + if finality_status is not None: + if finality_status == "pre_confirmed": + params["finality_status"] = "PRE_CONFIRMED" + elif finality_status == "accepted_on_l2": + params["finality_status"] = "ACCEPTED_ON_L2" + else: + raise ValueError(f"Invalid finality status: {finality_status}") + + subscription_id = await self._subscribe( + handler, "starknet_subscribeNewTransactionReceipts", params ) return subscription_id From 0a1bbc858ea11c0960858e9a33686a6acb61eae0 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Fri, 29 Aug 2025 01:32:21 +0200 Subject: [PATCH 02/32] Further adjustments of models and methods --- starknet_py/net/schemas/rpc/websockets.py | 6 +- starknet_py/net/websockets/models.py | 15 +--- .../net/websockets/websocket_client.py | 71 ++++++++++--------- .../code_examples/test_websocket_client.py | 56 ++++++--------- 4 files changed, 66 insertions(+), 82 deletions(-) diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index d3edcaf8c..896293d40 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -8,19 +8,19 @@ from starknet_py.net.schemas.rpc.block import BlockHeaderSchema from starknet_py.net.schemas.rpc.event import EmittedEventSchema from starknet_py.net.schemas.rpc.transactions import ( + TransactionReceiptSchema, TransactionStatusResponseSchema, TypesOfTransactionsSchema, - TransactionReceiptSchema, ) from starknet_py.net.websockets.models import ( NewEventsNotification, NewHeadsNotification, + NewTransactionNotification, + NewTransactionReceiptsNotification, NewTransactionStatus, ReorgData, ReorgNotification, TransactionStatusNotification, - NewTransactionReceiptsNotification, - NewTransactionNotification, ) from starknet_py.utils.schema import Schema diff --git a/starknet_py/net/websockets/models.py b/starknet_py/net/websockets/models.py index 5f7efc38c..11d8ac2f6 100644 --- a/starknet_py/net/websockets/models.py +++ b/starknet_py/net/websockets/models.py @@ -3,24 +3,15 @@ """ from dataclasses import dataclass -from typing import Generic, TypeVar, Union +from typing import Generic, TypeVar from starknet_py.net.client_models import ( BlockHeader, EmittedEvent, - TransactionStatusResponse, - TransactionReceipt, Transaction, TransactionFinalityStatus, -) -from starknet_py.net.models import ( - DeclareV1, - DeclareV2, - DeclareV3, - DeployAccountV1, - DeployAccountV3, - InvokeV1, - InvokeV3, + TransactionReceipt, + TransactionStatusResponse, ) T = TypeVar("T") diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 3b1ee1d1d..ae319ce4f 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -5,24 +5,29 @@ from websockets import InvalidState, State from websockets.asyncio.client import ClientConnection, connect -from starknet_py.net.client_models import Hash, LatestTag +from starknet_py.net.client_models import ( + Hash, + LatestTag, + TransactionFinalityStatus, + TransactionStatus, +) from starknet_py.net.client_utils import _to_rpc_felt, get_block_identifier from starknet_py.net.schemas.rpc.websockets import ( NewEventsNotificationSchema, NewHeadsNotificationSchema, + NewTransactionNotificationSchema, + NewTransactionReceiptsNotificationSchema, ReorgNotificationSchema, TransactionStatusNotificationSchema, - NewTransactionReceiptsNotificationSchema, - NewTransactionNotificationSchema, ) from starknet_py.net.websockets.errors import WebsocketClientError from starknet_py.net.websockets.models import ( NewEventsNotification, NewHeadsNotification, - ReorgNotification, - TransactionStatusNotification, NewTransactionNotification, NewTransactionReceiptsNotification, + ReorgNotification, + TransactionStatusNotification, ) Notification = Union[ @@ -121,6 +126,7 @@ async def subscribe_new_heads( return subscription_id + # pylint: disable=too-many-arguments async def subscribe_events( self, handler: Callable[[NewEventsNotification], Any], @@ -128,7 +134,7 @@ async def subscribe_events( keys: Optional[List[List[int]]] = None, block_hash: Optional[Union[Hash, LatestTag]] = None, block_number: Optional[Union[int, LatestTag]] = None, - finality_status: Optional[Literal["pre_confirmed", "accepted_on_l2"]] = None, + finality_status: Optional[TransactionFinalityStatus] = None, ) -> str: """ Creates a WebSocket stream which will fire events for new Starknet events with applied filters. @@ -149,12 +155,12 @@ async def subscribe_events( [_to_rpc_felt(key) for key in key_group] for key_group in keys ] if finality_status is not None: - if finality_status == "pre_confirmed": - params["finality_status"] = "PRE_CONFIRMED" - elif finality_status == "accepted_on_l2": - params["finality_status"] = "ACCEPTED_ON_L2" - else: - raise ValueError(f"Invalid finality status: {finality_status}") + if finality_status == "ACCEPTED_ON_L1": + raise ValueError( + f"{finality_status} is not allowed to be used for events subscription." + ) + params["finality_status"] = finality_status.value + block_id = get_block_identifier(block_hash, block_number, "latest") params = { **params, @@ -191,17 +197,15 @@ async def subscribe_new_transactions( self, handler: Callable[[NewTransactionNotification], Any], sender_address: Optional[List[int]] = None, - finality_status: Optional[ - Literal["received", "candidate", "pre_confirmed", "accepted_on_l2"] - ] = None, + finality_status: Optional[TransactionStatus] = None, ) -> str: """ Creates a WebSocket stream which will fire events when a new pending transaction is added. While there is no mempool, this notifies of transactions in the pending block. :param handler: The function to call when a new pending transaction is received. - :param transaction_details: If false, only hash is returned, otherwise full transaction details. - :param sender_address: The sender address to filter transactions by. + :param sender_address: List of sender addresses to filter transactions by. + :param finality_status: The finality statuses to filter transaction receipts by. :return: The subscription ID. """ @@ -211,16 +215,11 @@ async def subscribe_new_transactions( _to_rpc_felt(address) for address in sender_address ] if finality_status is not None: - if finality_status == "received": - params["finality_status"] = "RECEIVED" - elif finality_status == "candidate": - params["finality_status"] = "CANDIDATE" - elif finality_status == "pre_confirmed": - params["finality_status"] = "PRE_CONFIRMED" - elif finality_status == "accepted_on_l2": - params["finality_status"] = "ACCEPTED_ON_L2" - else: - raise ValueError(f"Invalid finality status: {finality_status}") + if finality_status == "L1_ACCEPTED": + raise ValueError( + f"{finality_status} is not allowed to be used for new transactions subscription." + ) + params["finality_status"] = finality_status.value subscription_id = await self._subscribe( handler, "starknet_subscribeNewTransactions", params @@ -230,20 +229,22 @@ async def subscribe_new_transactions( async def subscribe_new_transaction_receipts( self, handler: Callable[[NewTransactionReceiptsNotification], Any], + finality_status: Optional[List[TransactionFinalityStatus]] = None, sender_address: Optional[List[int]] = None, - finality_status: Optional[Literal["pre_confirmed", "accepted_on_l2"]] = None, ) -> str: """ - Creates a WebSocket stream which will fire events when a new pending transaction is added. - While there is no mempool, this notifies of transactions in the pending block. + Creates a WebSocket stream which will fire events when new transaction receipts are created. + An event is fired for each finality status update. It is possible for receipts of pre-confirmed transactions + to be received multiple times, or not at all. :param handler: The function to call when a new pending transaction is received. - :param transaction_details: If false, only hash is returned, otherwise full transaction details. - :param sender_address: The sender address to filter transactions by. + :param finality_status: The finality statuses to filter transaction receipts by. + :param sender_address: List of addresses to filter transactions by. :return: The subscription ID. """ params = {} + if sender_address is not None: params["sender_address"] = [ _to_rpc_felt(address) for address in sender_address @@ -254,7 +255,10 @@ async def subscribe_new_transaction_receipts( elif finality_status == "accepted_on_l2": params["finality_status"] = "ACCEPTED_ON_L2" else: - raise ValueError(f"Invalid finality status: {finality_status}") + raise ValueError( + f"{finality_status} is not allowed to be used for new transaction receipts " + f"subscription." + ) subscription_id = await self._subscribe( handler, "starknet_subscribeNewTransactionReceipts", params @@ -392,6 +396,7 @@ def _handle_notification(self, data: Dict): """ method: NotificationMethod = data["method"] schema = _NOTIFICATION_SCHEMA_MAPPING[method] + print(data["params"]) notification: Notification = schema().load(data["params"]) if notification.subscription_id not in self._subscriptions: diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index 036bce48a..f3233be53 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -1,6 +1,6 @@ # pylint: disable=import-outside-toplevel import asyncio -from typing import List, Optional, Union +from typing import List, Optional import pytest @@ -12,13 +12,11 @@ Call, EmittedEvent, TransactionExecutionStatus, - TransactionStatus, + TransactionStatus, TransactionReceipt, ) -from starknet_py.net.models import StarknetChainId from starknet_py.net.websockets.models import ( NewTransactionStatus, ReorgData, - Transaction, ) from starknet_py.net.websockets.websocket_client import WebsocketClient from starknet_py.tests.e2e.fixtures.constants import MAX_RESOURCE_BOUNDS @@ -89,9 +87,6 @@ def handler(new_heads_notification: NewHeadsNotification): assert unsubscribe_result is True -@pytest.mark.skip( - reason="TODO(cptartur): Unskip when adding changes for websockets in RPC 0.9.0 " -) @pytest.mark.asyncio async def test_subscribe_events( websocket_client: WebsocketClient, @@ -145,7 +140,6 @@ def handler(new_events_notification: NewEventsNotification): assert unsubscribe_result is True -@pytest.mark.skip(reason="TODO(#1644)") @pytest.mark.asyncio async def test_subscribe_transaction_status( websocket_client: WebsocketClient, @@ -211,38 +205,38 @@ def handler(transaction_status_notification: TransactionStatusNotification): @pytest.mark.skip( - reason="TODO(cptartur): Unskip when adding changes for websockets in RPC 0.9.0 " + reason="TODO(cptartur): Investigate why tx hashes assertion fails" ) @pytest.mark.asyncio -async def test_subscribe_pending_transactions( +async def test_subscribe_new_transaction_receipts( websocket_client: WebsocketClient, deployed_balance_contract, argent_account_v040: BaseAccount, ): account = argent_account_v040 - pending_transactions: List[Union[int, Transaction]] = [] + transaction_receipts: List[TransactionReceipt] = [] - # docs-start: subscribe_pending_transactions - from starknet_py.net.websockets.models import PendingTransactionsNotification + # docs-start: subscribe_new_transaction_receipts + from starknet_py.net.websockets.models import NewTransactionReceiptsNotification - # Create a handler function that will be called when a new pending transaction is emitted - def handler(pending_transaction_notification: PendingTransactionsNotification): - # Perform the necessary actions with the new pending transaction... + # Create a handler function that will be called when a new transaction is emitted + def handler(new_transaction_receipts_notification: NewTransactionReceiptsNotification): + # Perform the necessary actions with the new transaction receipts... - # docs-end: subscribe_pending_transactions - nonlocal pending_transactions - pending_transactions.append(pending_transaction_notification.result) + # docs-end: subscribe_new_transaction_receipts + nonlocal transaction_receipts + transaction_receipts.append(new_transaction_receipts_notification.result) - # docs-start: subscribe_pending_transactions - # Subscribe to pending transactions notifications - subscription_id = await websocket_client.subscribe_pending_transactions( + # docs-start: subscribe_new_transaction_receipts + # Subscribe to new transaction receipts notifications + subscription_id = await websocket_client.subscribe_new_transaction_receipts( handler=handler, sender_address=[account.address], ) # Here you can put code which will keep the application running (e.g. using loop and `asyncio.sleep`) # ... - # docs-end: subscribe_pending_transactions + # docs-end: subscribe_new_transaction_receipts increase_balance_call = Call( to_addr=deployed_balance_contract.address, selector=get_selector_from_name("increase_balance"), @@ -257,21 +251,15 @@ def handler(pending_transaction_notification: PendingTransactionsNotification): tx_hash=execute.transaction_hash ) - assert len(pending_transactions) == 1 - pending_transaction = pending_transactions[0] - - transaction_hash = ( - execute.transaction_hash - if isinstance(pending_transaction, int) - else pending_transaction.calculate_hash(StarknetChainId.SEPOLIA) - ) - assert execute.transaction_hash == transaction_hash + assert len(transaction_receipts) == 1 + transaction_receipt = transaction_receipts[0] + assert execute.transaction_hash == transaction_receipt.transaction_hash - # docs-start: subscribe_pending_transactions + # docs-start: subscribe_new_transaction_receipts # Unsubscribe from the notifications unsubscribe_result = await websocket_client.unsubscribe(subscription_id) - # docs-end: subscribe_pending_transactions + # docs-end: subscribe_new_transaction_receipts assert unsubscribe_result is True From 0ae1fb97c49d0eaa361a41b29ca2cd7f7cd8f39b Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Fri, 29 Aug 2025 02:04:05 +0200 Subject: [PATCH 03/32] Fix formatting --- .../docs/code_examples/test_websocket_client.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index f3233be53..b3ad4aa9a 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -12,12 +12,10 @@ Call, EmittedEvent, TransactionExecutionStatus, - TransactionStatus, TransactionReceipt, -) -from starknet_py.net.websockets.models import ( - NewTransactionStatus, - ReorgData, + TransactionReceipt, + TransactionStatus, ) +from starknet_py.net.websockets.models import NewTransactionStatus, ReorgData from starknet_py.net.websockets.websocket_client import WebsocketClient from starknet_py.tests.e2e.fixtures.constants import MAX_RESOURCE_BOUNDS @@ -204,9 +202,7 @@ def handler(transaction_status_notification: TransactionStatusNotification): assert unsubscribe_result is True -@pytest.mark.skip( - reason="TODO(cptartur): Investigate why tx hashes assertion fails" -) +@pytest.mark.skip(reason="TODO(cptartur): Investigate why tx hashes assertion fails") @pytest.mark.asyncio async def test_subscribe_new_transaction_receipts( websocket_client: WebsocketClient, @@ -220,7 +216,9 @@ async def test_subscribe_new_transaction_receipts( from starknet_py.net.websockets.models import NewTransactionReceiptsNotification # Create a handler function that will be called when a new transaction is emitted - def handler(new_transaction_receipts_notification: NewTransactionReceiptsNotification): + def handler( + new_transaction_receipts_notification: NewTransactionReceiptsNotification, + ): # Perform the necessary actions with the new transaction receipts... # docs-end: subscribe_new_transaction_receipts From 6b73b018deb6e7d547f82376c73c02c34d15668c Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Fri, 29 Aug 2025 02:49:04 +0200 Subject: [PATCH 04/32] Fixes --- starknet_py/net/client_models.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/starknet_py/net/client_models.py b/starknet_py/net/client_models.py index 90cdded97..c05f35b0b 100644 --- a/starknet_py/net/client_models.py +++ b/starknet_py/net/client_models.py @@ -36,16 +36,6 @@ LatestTag = Literal["latest"] -class TransactionFinalityStatus(Enum): - """ - Enum representing transaction finality statuses. - """ - - PRE_CONFIRMED = "PRE_CONFIRMED" - ACCEPTED_ON_L2 = "ACCEPTED_ON_L2" - ACCEPTED_ON_L1 = "ACCEPTED_ON_L1" - - @dataclass class Call: """ @@ -78,7 +68,6 @@ class EmittedEvent(Event): """ transaction_hash: int - finality_status: TransactionFinalityStatus block_hash: Optional[int] = None block_number: Optional[int] = None @@ -422,6 +411,16 @@ class TransactionExecutionStatus(Enum): REVERTED = "REVERTED" +class TransactionFinalityStatus(Enum): + """ + Enum representing transaction finality statuses. + """ + + PRE_CONFIRMED = "PRE_CONFIRMED" + ACCEPTED_ON_L2 = "ACCEPTED_ON_L2" + ACCEPTED_ON_L1 = "ACCEPTED_ON_L1" + + @dataclass class InnerCallExecutionResources: """ From 8182a90ae75e5b92404e07d571a40ea6eba83a82 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Fri, 29 Aug 2025 16:17:38 +0200 Subject: [PATCH 05/32] Add EmittedEventWithFinalityStatus --- starknet_py/net/client_models.py | 31 +++++++++++++++++++---- starknet_py/net/schemas/rpc/event.py | 16 ++++++++++-- starknet_py/net/schemas/rpc/websockets.py | 6 +++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/starknet_py/net/client_models.py b/starknet_py/net/client_models.py index c05f35b0b..c6d17cc3e 100644 --- a/starknet_py/net/client_models.py +++ b/starknet_py/net/client_models.py @@ -62,16 +62,23 @@ class Event: @dataclass -class EmittedEvent(Event): - """ - Dataclass representing an event emitted by transaction. - """ - +class _EmittedEventBase(Event): transaction_hash: int + + +@dataclass +class _EmittedEventDefaultBase(Event): block_hash: Optional[int] = None block_number: Optional[int] = None +@dataclass +class EmittedEvent(_EmittedEventDefaultBase, _EmittedEventBase): + """ + Dataclass representing an event emitted by transaction. + """ + + @dataclass class EventsChunk: """ @@ -1251,3 +1258,17 @@ def to_abi_dict(self) -> Dict: for call in self.calls ], } + + +@dataclass +class _EmittedEventWithFinalityStatus: + finality_status: TransactionFinalityStatus + + +@dataclass +class EmittedEventWithFinalityStatus( + _EmittedEventDefaultBase, _EmittedEventWithFinalityStatus, _EmittedEventBase +): + """ + Dataclass representing an event emitted by transaction. + """ diff --git a/starknet_py/net/schemas/rpc/event.py b/starknet_py/net/schemas/rpc/event.py index ce0a36e35..627417dda 100644 --- a/starknet_py/net/schemas/rpc/event.py +++ b/starknet_py/net/schemas/rpc/event.py @@ -1,6 +1,11 @@ from marshmallow import fields, post_load -from starknet_py.net.client_models import EmittedEvent, Event, EventsChunk +from starknet_py.net.client_models import ( + EmittedEvent, + EmittedEventWithFinalityStatus, + Event, + EventsChunk, +) from starknet_py.net.schemas.common import Felt, FinalityStatusField from starknet_py.utils.schema import Schema @@ -17,7 +22,6 @@ def make_dataclass(self, data, **kwargs) -> Event: class EmittedEventSchema(EventSchema): transaction_hash = Felt(data_key="transaction_hash", required=True) - finality_status = FinalityStatusField(data_key="finality_status", required=True) block_hash = Felt(data_key="block_hash", load_default=None) block_number = fields.Integer(data_key="block_number", load_default=None) @@ -26,6 +30,14 @@ def make_dataclass(self, data, **kwargs) -> EmittedEvent: return EmittedEvent(**data) +class EmittedEventWithFinalitySchema(EmittedEventSchema): + finality_status = FinalityStatusField(data_key="finality_status", required=True) + + @post_load + def make_dataclass(self, data, **kwargs) -> EmittedEventWithFinalityStatus: + return EmittedEventWithFinalityStatus(**data) + + class EventsChunkSchema(Schema): events = fields.List( fields.Nested(EmittedEventSchema()), diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index 896293d40..8cf0ac42a 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -6,7 +6,7 @@ from starknet_py.net.client_utils import _to_rpc_felt from starknet_py.net.schemas.common import Felt, TransactionFinalityStatusField from starknet_py.net.schemas.rpc.block import BlockHeaderSchema -from starknet_py.net.schemas.rpc.event import EmittedEventSchema +from starknet_py.net.schemas.rpc.event import EmittedEventWithFinalitySchema from starknet_py.net.schemas.rpc.transactions import ( TransactionReceiptSchema, TransactionStatusResponseSchema, @@ -36,7 +36,9 @@ def make_dataclass(self, data, **kwargs) -> NewHeadsNotification: class NewEventsNotificationSchema(Schema): subscription_id = fields.Str(data_key="subscription_id", required=True) - result = fields.Nested(EmittedEventSchema(), data_key="result", required=True) + result = fields.Nested( + EmittedEventWithFinalitySchema(), data_key="result", required=True + ) @post_load def make_dataclass(self, data, **kwargs) -> NewEventsNotification: From af22c2bb873e3c84ec9b695c471245119d6e2b9a Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Fri, 29 Aug 2025 17:42:12 +0200 Subject: [PATCH 06/32] Use correct type for finality status for new transaction subscription and notification --- starknet_py/net/client_models.py | 11 ++++++++++ starknet_py/net/schemas/common.py | 22 +++++++++++++++++++ starknet_py/net/schemas/rpc/websockets.py | 8 +++++-- starknet_py/net/websockets/models.py | 4 ++-- .../net/websockets/websocket_client.py | 8 ++----- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/starknet_py/net/client_models.py b/starknet_py/net/client_models.py index c6d17cc3e..e0dd0f36d 100644 --- a/starknet_py/net/client_models.py +++ b/starknet_py/net/client_models.py @@ -409,6 +409,17 @@ class TransactionStatus(Enum): ACCEPTED_ON_L1 = "ACCEPTED_ON_L1" +class TransactionStatusWithoutL1(Enum): + """ + Enum representing transaction statuses. + """ + + RECEIVED = "RECEIVED" + CANDIDATE = "CANDIDATE" + PRE_CONFIRMED = "PRE_CONFIRMED" + ACCEPTED_ON_L2 = "ACCEPTED_ON_L2" + + class TransactionExecutionStatus(Enum): """ Enum representing transaction execution statuses. diff --git a/starknet_py/net/schemas/common.py b/starknet_py/net/schemas/common.py index 55854b142..7f296a847 100644 --- a/starknet_py/net/schemas/common.py +++ b/starknet_py/net/schemas/common.py @@ -16,6 +16,7 @@ TransactionExecutionStatus, TransactionFinalityStatus, TransactionStatus, + TransactionStatusWithoutL1, TransactionType, ) @@ -205,6 +206,27 @@ def _deserialize( return TransactionFinalityStatus(value) +class TransactionStatusWithoutL1Field(fields.Field): + def _serialize(self, value: Any, attr: Optional[str], obj: Any, **kwargs): + return value.name if value is not None else "" + + def _deserialize( + self, + value: Any, + attr: Optional[str], + data: Optional[Mapping[str, Any]], + **kwargs, + ) -> TransactionStatusWithoutL1: + values = [v.value for v in TransactionStatusWithoutL1] + + if value not in values: + raise ValidationError( + f"Invalid value provided for TransactionStatusWithoutL1: {value}." + ) + + return TransactionStatusWithoutL1(value) + + class BlockStatusField(fields.Field): def _serialize(self, value: Any, attr: Optional[str], obj: Any, **kwargs): return value.name if value is not None else "" diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index 8cf0ac42a..723b5f84f 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -4,7 +4,11 @@ from starknet_py.net.client_models import Transaction from starknet_py.net.client_utils import _to_rpc_felt -from starknet_py.net.schemas.common import Felt, TransactionFinalityStatusField +from starknet_py.net.schemas.common import ( + Felt, + TransactionFinalityStatusField, + TransactionStatusWithoutL1Field, +) from starknet_py.net.schemas.rpc.block import BlockHeaderSchema from starknet_py.net.schemas.rpc.event import EmittedEventWithFinalitySchema from starknet_py.net.schemas.rpc.transactions import ( @@ -134,7 +138,7 @@ def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): "Invalid payload for transaction with finality_status." ) - finality_status_field = TransactionFinalityStatusField( + finality_status_field = TransactionStatusWithoutL1Field( data_key="finality_status", required=True ) finality_status = finality_status_field.deserialize( diff --git a/starknet_py/net/websockets/models.py b/starknet_py/net/websockets/models.py index 11d8ac2f6..2c59a1657 100644 --- a/starknet_py/net/websockets/models.py +++ b/starknet_py/net/websockets/models.py @@ -9,9 +9,9 @@ BlockHeader, EmittedEvent, Transaction, - TransactionFinalityStatus, TransactionReceipt, TransactionStatusResponse, + TransactionStatusWithoutL1, ) T = TypeVar("T") @@ -87,7 +87,7 @@ class NewTransactionReceiptsNotification(Notification[TransactionReceipt]): @dataclass class NewTransactionNotificationResult: transaction: Transaction - finality_status: TransactionFinalityStatus + finality_status: TransactionStatusWithoutL1 @dataclass diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index ae319ce4f..1fd663d68 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -9,7 +9,7 @@ Hash, LatestTag, TransactionFinalityStatus, - TransactionStatus, + TransactionStatusWithoutL1, ) from starknet_py.net.client_utils import _to_rpc_felt, get_block_identifier from starknet_py.net.schemas.rpc.websockets import ( @@ -197,7 +197,7 @@ async def subscribe_new_transactions( self, handler: Callable[[NewTransactionNotification], Any], sender_address: Optional[List[int]] = None, - finality_status: Optional[TransactionStatus] = None, + finality_status: Optional[TransactionStatusWithoutL1] = None, ) -> str: """ Creates a WebSocket stream which will fire events when a new pending transaction is added. @@ -215,10 +215,6 @@ async def subscribe_new_transactions( _to_rpc_felt(address) for address in sender_address ] if finality_status is not None: - if finality_status == "L1_ACCEPTED": - raise ValueError( - f"{finality_status} is not allowed to be used for new transactions subscription." - ) params["finality_status"] = finality_status.value subscription_id = await self._subscribe( From 3b64b4ebee53c69d123d902e6a2a058c7dd067b8 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Fri, 29 Aug 2025 18:44:43 +0200 Subject: [PATCH 07/32] fixup! Add EmittedEventWithFinalityStatus --- starknet_py/net/websockets/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starknet_py/net/websockets/models.py b/starknet_py/net/websockets/models.py index 2c59a1657..2bec2a69d 100644 --- a/starknet_py/net/websockets/models.py +++ b/starknet_py/net/websockets/models.py @@ -7,7 +7,7 @@ from starknet_py.net.client_models import ( BlockHeader, - EmittedEvent, + EmittedEventWithFinalityStatus, Transaction, TransactionReceipt, TransactionStatusResponse, @@ -35,7 +35,7 @@ class NewHeadsNotification(Notification[BlockHeader]): @dataclass -class NewEventsNotification(Notification[EmittedEvent]): +class NewEventsNotification(Notification[EmittedEventWithFinalityStatus]): """ Notification to the client of a new event. """ From 22990d814293adb96cf38aa1348aac67b506bf19 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Fri, 29 Aug 2025 18:55:44 +0200 Subject: [PATCH 08/32] Remove print --- starknet_py/net/websockets/websocket_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 1fd663d68..3a0e0f886 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -392,7 +392,6 @@ def _handle_notification(self, data: Dict): """ method: NotificationMethod = data["method"] schema = _NOTIFICATION_SCHEMA_MAPPING[method] - print(data["params"]) notification: Notification = schema().load(data["params"]) if notification.subscription_id not in self._subscriptions: From 64201666019089331b92d09449418d0ea9cf4692 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Fri, 29 Aug 2025 18:55:48 +0200 Subject: [PATCH 09/32] Add migration guide --- docs/migration_guide.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index d013e564f..1440f891f 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -1,6 +1,18 @@ Migration guide =============== +*************************** +0.28.0 Migration guide +*************************** + +Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. + +.. py:currentmodule:: starknet_py.net.websockets.websocket_client + +1. Removed ``subscribe_pending_transactions`` method and respective notification. +2. Added :meth:`WebsocketClient.subscribe_new_transactions` and :meth:`WebsocketClient.subscribe_new_transaction_receipts` and respective notifications. +3. Added field ``finality_status`` to :meth:`WebsocketClient.subscribe_events`, changed ``NewEventsNotification`` that is used in the handler inner type to contain finalty status. + *************************** 0.28.0-rc.4 Migration guide *************************** From 0ef0c5acf2a710d5388fbcf85329a9274c6a2401 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Mon, 1 Sep 2025 11:13:26 +0200 Subject: [PATCH 10/32] Fix methods not following the spec --- starknet_py/net/client_models.py | 9 ++++++ .../net/websockets/websocket_client.py | 31 +++++++----------- .../code_examples/test_websocket_client.py | 32 +++++++++++++++++-- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/starknet_py/net/client_models.py b/starknet_py/net/client_models.py index e0dd0f36d..7de116130 100644 --- a/starknet_py/net/client_models.py +++ b/starknet_py/net/client_models.py @@ -439,6 +439,15 @@ class TransactionFinalityStatus(Enum): ACCEPTED_ON_L1 = "ACCEPTED_ON_L1" +class TransactionFinalityStatusWithoutL1(Enum): + """ + Enum representing transaction finality statuses, without ACCEPTED_ON_L1 status. + """ + + PRE_CONFIRMED = "PRE_CONFIRMED" + ACCEPTED_ON_L2 = "ACCEPTED_ON_L2" + + @dataclass class InnerCallExecutionResources: """ diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 3a0e0f886..2a1aadd9b 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -8,7 +8,7 @@ from starknet_py.net.client_models import ( Hash, LatestTag, - TransactionFinalityStatus, + TransactionFinalityStatusWithoutL1, TransactionStatusWithoutL1, ) from starknet_py.net.client_utils import _to_rpc_felt, get_block_identifier @@ -134,7 +134,7 @@ async def subscribe_events( keys: Optional[List[List[int]]] = None, block_hash: Optional[Union[Hash, LatestTag]] = None, block_number: Optional[Union[int, LatestTag]] = None, - finality_status: Optional[TransactionFinalityStatus] = None, + finality_status: Optional[TransactionFinalityStatusWithoutL1] = None, ) -> str: """ Creates a WebSocket stream which will fire events for new Starknet events with applied filters. @@ -145,6 +145,9 @@ async def subscribe_events( :param block_hash: Hash of the block to get notifications from or literal `"latest"`. Mutually exclusive with ``block_number`` parameter. If not provided, queries block `"latest"`. :param block_number: Number (height) of the block to get notifications from or literal `"latest"`. + :param finality_status: The finality status of the most recent events to include, default is ACCEPTED_ON_L2. + If PRE_CONFIRMED finality is selected, events might appear multiple times, + once for each finality status update. :return: The subscription ID. """ params = {} @@ -155,10 +158,6 @@ async def subscribe_events( [_to_rpc_felt(key) for key in key_group] for key_group in keys ] if finality_status is not None: - if finality_status == "ACCEPTED_ON_L1": - raise ValueError( - f"{finality_status} is not allowed to be used for events subscription." - ) params["finality_status"] = finality_status.value block_id = get_block_identifier(block_hash, block_number, "latest") @@ -197,7 +196,7 @@ async def subscribe_new_transactions( self, handler: Callable[[NewTransactionNotification], Any], sender_address: Optional[List[int]] = None, - finality_status: Optional[TransactionStatusWithoutL1] = None, + finality_status: Optional[List[TransactionStatusWithoutL1]] = None, ) -> str: """ Creates a WebSocket stream which will fire events when a new pending transaction is added. @@ -205,7 +204,7 @@ async def subscribe_new_transactions( :param handler: The function to call when a new pending transaction is received. :param sender_address: List of sender addresses to filter transactions by. - :param finality_status: The finality statuses to filter transaction receipts by. + :param finality_status: The finality statuses to filter transaction receipts by, default is [ACCEPTED_ON_L2]. :return: The subscription ID. """ @@ -215,7 +214,7 @@ async def subscribe_new_transactions( _to_rpc_felt(address) for address in sender_address ] if finality_status is not None: - params["finality_status"] = finality_status.value + params["finality_status"] = [status.value for status in finality_status] subscription_id = await self._subscribe( handler, "starknet_subscribeNewTransactions", params @@ -225,7 +224,7 @@ async def subscribe_new_transactions( async def subscribe_new_transaction_receipts( self, handler: Callable[[NewTransactionReceiptsNotification], Any], - finality_status: Optional[List[TransactionFinalityStatus]] = None, + finality_status: Optional[List[TransactionFinalityStatusWithoutL1]] = None, sender_address: Optional[List[int]] = None, ) -> str: """ @@ -234,7 +233,7 @@ async def subscribe_new_transaction_receipts( to be received multiple times, or not at all. :param handler: The function to call when a new pending transaction is received. - :param finality_status: The finality statuses to filter transaction receipts by. + :param finality_status: The finality statuses to filter transaction receipts by, default is [ACCEPTED_ON_L2]. :param sender_address: List of addresses to filter transactions by. :return: The subscription ID. @@ -246,15 +245,7 @@ async def subscribe_new_transaction_receipts( _to_rpc_felt(address) for address in sender_address ] if finality_status is not None: - if finality_status == "pre_confirmed": - params["finality_status"] = "PRE_CONFIRMED" - elif finality_status == "accepted_on_l2": - params["finality_status"] = "ACCEPTED_ON_L2" - else: - raise ValueError( - f"{finality_status} is not allowed to be used for new transaction receipts " - f"subscription." - ) + params["finality_status"] = [status.value for status in finality_status] subscription_id = await self._subscribe( handler, "starknet_subscribeNewTransactionReceipts", params diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index b3ad4aa9a..01acadca7 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -15,7 +15,11 @@ TransactionReceipt, TransactionStatus, ) -from starknet_py.net.websockets.models import NewTransactionStatus, ReorgData +from starknet_py.net.websockets.models import ( + NewTransactionNotification, + NewTransactionStatus, + ReorgData, +) from starknet_py.net.websockets.websocket_client import WebsocketClient from starknet_py.tests.e2e.fixtures.constants import MAX_RESOURCE_BOUNDS @@ -211,6 +215,7 @@ async def test_subscribe_new_transaction_receipts( ): account = argent_account_v040 transaction_receipts: List[TransactionReceipt] = [] + transactions = [] # docs-start: subscribe_new_transaction_receipts from starknet_py.net.websockets.models import NewTransactionReceiptsNotification @@ -225,16 +230,32 @@ def handler( nonlocal transaction_receipts transaction_receipts.append(new_transaction_receipts_notification.result) + def handler2( + new_transaction_notification: NewTransactionNotification, + ): + # Perform the necessary actions with the new transaction receipts... + + # docs-end: subscribe_new_transaction_receipts + nonlocal transactions + transactions.append(new_transaction_notification.result) + # docs-start: subscribe_new_transaction_receipts # Subscribe to new transaction receipts notifications subscription_id = await websocket_client.subscribe_new_transaction_receipts( handler=handler, sender_address=[account.address], ) + subscription_id2 = await websocket_client.subscribe_new_transactions( + handler=handler2, + sender_address=[account.address], + ) # Here you can put code which will keep the application running (e.g. using loop and `asyncio.sleep`) # ... # docs-end: subscribe_new_transaction_receipts + # assert len(transaction_receipts) == 0 + print("transactions before ", transactions) + increase_balance_call = Call( to_addr=deployed_balance_contract.address, selector=get_selector_from_name("increase_balance"), @@ -244,19 +265,26 @@ def handler( calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS ) - await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) + rec = await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) await argent_account_v040.client.get_transaction_receipt( tx_hash=execute.transaction_hash ) + print("transaction hash", execute.transaction_hash) + print("transaction receipt", rec) + + await asyncio.sleep(5) + print("transactions after", transactions) assert len(transaction_receipts) == 1 transaction_receipt = transaction_receipts[0] + assert increase_balance_call.to_addr == transaction_receipt.contract_address assert execute.transaction_hash == transaction_receipt.transaction_hash # docs-start: subscribe_new_transaction_receipts # Unsubscribe from the notifications unsubscribe_result = await websocket_client.unsubscribe(subscription_id) + await websocket_client.unsubscribe(subscription_id2) # docs-end: subscribe_new_transaction_receipts assert unsubscribe_result is True From 8a31e4bfb02466bd1ba9922dbc9c4420f4f3816e Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Mon, 1 Sep 2025 12:25:21 +0200 Subject: [PATCH 11/32] Fix `NewTransactionNotificationSchema` return type --- starknet_py/net/schemas/rpc/websockets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index 723b5f84f..82dc8eade 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -6,7 +6,6 @@ from starknet_py.net.client_utils import _to_rpc_felt from starknet_py.net.schemas.common import ( Felt, - TransactionFinalityStatusField, TransactionStatusWithoutL1Field, ) from starknet_py.net.schemas.rpc.block import BlockHeaderSchema @@ -25,6 +24,7 @@ ReorgData, ReorgNotification, TransactionStatusNotification, + NewTransactionNotificationResult, ) from starknet_py.utils.schema import Schema @@ -151,7 +151,9 @@ def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): tx_payload.pop("finality_status", None) tx_obj = super().load(tx_payload, many=many, partial=partial, unknown=unknown) - return {"transaction": tx_obj, "finality_status": finality_status} + return NewTransactionNotificationResult( + transaction=tx_obj, finality_status=finality_status + ) class NewTransactionNotificationSchema(Schema): From 2ff5ab30bb6cdbe5589e2ec28b885f9f4fe761da Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Mon, 1 Sep 2025 12:26:23 +0200 Subject: [PATCH 12/32] Add tests for new ws methods --- .../tests/e2e/client/websocket_client_test.py | 141 +++++++++++++++++- 1 file changed, 138 insertions(+), 3 deletions(-) diff --git a/starknet_py/tests/e2e/client/websocket_client_test.py b/starknet_py/tests/e2e/client/websocket_client_test.py index 2c62de20f..c64d9a0a3 100644 --- a/starknet_py/tests/e2e/client/websocket_client_test.py +++ b/starknet_py/tests/e2e/client/websocket_client_test.py @@ -1,13 +1,31 @@ -from typing import Optional +from typing import Optional, List +import asyncio import pytest from starknet_py.devnet_utils.devnet_client import DevnetClient -from starknet_py.net.client_models import BlockHeader, StarknetBlock +from starknet_py.hash.selector import get_selector_from_name +from starknet_py.net.account.base_account import BaseAccount +from starknet_py.net.client_models import ( + BlockHeader, + StarknetBlock, + Call, + TransactionFinalityStatus, + TransactionFinalityStatusWithoutL1, + TransactionStatusWithoutL1, + TransactionReceipt, +) from starknet_py.net.full_node_client import FullNodeClient from starknet_py.net.websockets.errors import WebsocketClientError -from starknet_py.net.websockets.models import NewHeadsNotification +from starknet_py.net.websockets.models import ( + NewHeadsNotification, + NewEventsNotification, + NewTransactionNotification, + NewTransactionReceiptsNotification, + NewTransactionNotificationResult, +) from starknet_py.net.websockets.websocket_client import WebsocketClient +from starknet_py.tests.e2e.fixtures.constants import MAX_RESOURCE_BOUNDS @pytest.mark.asyncio @@ -107,3 +125,120 @@ async def test_unsubscribe_with_non_existing_id( ): unsubscribe_result = await websocket_client.unsubscribe("123") assert unsubscribe_result is False + + +@pytest.mark.asyncio +async def test_subscribe_events_with_finality_status( + websocket_client: WebsocketClient, + deployed_balance_contract, + argent_account_v040: BaseAccount, +): + received_events: List = [] + + def handler(new_events_notification: NewEventsNotification): + nonlocal received_events + received_events.append(new_events_notification.result) + + subscription_id = await websocket_client.subscribe_events( + handler=handler, + from_address=deployed_balance_contract.address, + finality_status=TransactionFinalityStatusWithoutL1.ACCEPTED_ON_L2, + ) + + increase_balance_call = Call( + to_addr=deployed_balance_contract.address, + selector=get_selector_from_name("increase_balance"), + calldata=[100], + ) + execute = await argent_account_v040.execute_v3( + calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS + ) + + await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) + await asyncio.sleep(3) + + assert len(received_events) >= 1 + # Ensure we received at least one event with the requested finality status + assert all( + ev.finality_status == TransactionFinalityStatus.ACCEPTED_ON_L2 + for ev in received_events + ) + + unsubscribe_result = await websocket_client.unsubscribe(subscription_id) + assert unsubscribe_result is True + + +@pytest.mark.asyncio +async def test_subscribe_new_transactions_with_finality_status( + websocket_client: WebsocketClient, + deployed_balance_contract, + argent_account_v040: BaseAccount, +): + received: List[NewTransactionNotificationResult] = [] + + def handler(new_tx_notification: NewTransactionNotification): + nonlocal received + received.append(new_tx_notification.result) + + subscription_id = await websocket_client.subscribe_new_transactions( + handler=handler, + sender_address=[argent_account_v040.address], + finality_status=[TransactionStatusWithoutL1.ACCEPTED_ON_L2], + ) + + increase_balance_call = Call( + to_addr=deployed_balance_contract.address, + selector=get_selector_from_name("increase_balance"), + calldata=[100], + ) + execute = await argent_account_v040.execute_v3( + calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS + ) + + await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) + await asyncio.sleep(3) + + assert len(received) >= 1 + assert all(r.finality_status == TransactionStatusWithoutL1.ACCEPTED_ON_L2 for r in received) + + unsubscribe_result = await websocket_client.unsubscribe(subscription_id) + assert unsubscribe_result is True + + +@pytest.mark.asyncio +async def test_subscribe_new_transaction_receipts_with_finality_status( + websocket_client: WebsocketClient, + deployed_balance_contract, + argent_account_v040: BaseAccount, +): + receipts: List[TransactionReceipt] = [] + + def handler(new_tx_receipt: NewTransactionReceiptsNotification): + nonlocal receipts + receipts.append(new_tx_receipt.result) + + subscription_id = await websocket_client.subscribe_new_transaction_receipts( + handler=handler, + sender_address=[argent_account_v040.address], + finality_status=[TransactionFinalityStatusWithoutL1.ACCEPTED_ON_L2], + ) + + increase_balance_call = Call( + to_addr=deployed_balance_contract.address, + selector=get_selector_from_name("increase_balance"), + calldata=[100], + ) + execute = await argent_account_v040.execute_v3( + calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS + ) + + await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) + await asyncio.sleep(3) + + assert len(receipts) >= 1 + assert all( + r.finality_status == TransactionFinalityStatus.ACCEPTED_ON_L2 for r in receipts + ) + + unsubscribe_result = await websocket_client.unsubscribe(subscription_id) + assert unsubscribe_result is True From 4de8c1e83ae9b8aad167fde42cfec5caf310144e Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 2 Sep 2025 16:29:48 +0200 Subject: [PATCH 13/32] Add handling of listener errors to WebsocketClient --- .../net/websockets/websocket_client.py | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 2a1aadd9b..1ce55c12b 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -57,6 +57,7 @@ } +# pylint: disable=too-many-instance-attributes class WebsocketClient: """ Starknet client for WebSocket API. @@ -74,12 +75,24 @@ def __init__(self, node_url: str): self._pending_responses: Dict[int, asyncio.Future] = {} self._on_chain_reorg: Optional[Callable[[ReorgNotification], Any]] = None + # New: future that completes with an exception if listen loop dies + self._listen_failed: Optional[asyncio.Future] = None + async def connect(self): """ Establishes the WebSocket connection. """ - self.connection = await connect(self.node_url) + self.connection = await connect( + self.node_url, ping_interval=None, ping_timeout=None + ) + + # Create/reset the failure future for this connection + loop = asyncio.get_running_loop() + self._listen_failed = loop.create_future() + + # Start listener and attach a synchronous done-callback self._listen_task = asyncio.create_task(self._listen()) + self._listen_task.add_done_callback(self._fail_fast) async def disconnect(self): """ @@ -92,6 +105,11 @@ async def disconnect(self): self._listen_task.cancel() await asyncio.gather(self._listen_task, return_exceptions=True) self._listen_task = None + + # Make sure waiters on _listen_failed don't hang forever + if self._listen_failed and not self._listen_failed.done(): + self._listen_failed.cancel() + await self.connection.close() self.connection = None @@ -320,9 +338,29 @@ async def _listen(self): if self.connection is None: raise InvalidState("Connection is not established.") - async for message in self.connection: + while True: + message = await self.connection.recv() self._handle_received_message(message) + def _fail_fast(self, task: asyncio.Task) -> None: + if task.cancelled(): + # Propagate cancellation to waiters + if self._listen_failed and not self._listen_failed.done(): + self._listen_failed.cancel() + return + + exc = task.exception() + if exc is not None: + # Signal failure to any waiter + if self._listen_failed and not self._listen_failed.done(): + self._listen_failed.set_exception(exc) + + # Also fail any pending RPC futures to unblock callers + for fut in self._pending_responses.values(): + if not fut.done(): + fut.set_exception(exc) + self._pending_responses.clear() + async def _send_message( self, method: str, @@ -337,6 +375,10 @@ async def _send_message( if self.connection is None: raise InvalidState("Connection is not established.") + # If the listener already failed, raise immediately + if self._listen_failed and self._listen_failed.done(): + await self._listen_failed + message_id = self._message_id self._message_id += 1 @@ -347,11 +389,22 @@ async def _send_message( "params": params if params else [], } - future = asyncio.Future() + future: asyncio.Future = asyncio.get_running_loop().create_future() self._pending_responses[message_id] = future await self.connection.send(json.dumps(payload)) - response = await future + + if self._listen_failed is not None: + # Get the first future that completes + done, _ = await asyncio.wait( + {future, self._listen_failed}, + return_when=asyncio.FIRST_COMPLETED, + ) + done_future = next(iter(done)) + # If the first completed future was `_listen_failed`, an exception will be raised on awaiting + response = await done_future + else: + response = await future if "error" in response: self._handle_error(response) @@ -369,9 +422,10 @@ def _handle_received_message(self, message: Union[str, bytes]): # case when the message is a response to `subscribe_{method}` if "id" in data and data["id"] in self._pending_responses: future = self._pending_responses.pop(data["id"]) - future.set_result(data) + if not future.done(): + future.set_result(data) - # case when the message is a notification + # Case when the message is a notification elif "method" in data: self._handle_notification(data) From af5c9d1d8b21da5fa29f8c3d1c79e6bc0e48569a Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 2 Sep 2025 16:30:10 +0200 Subject: [PATCH 14/32] Fix `NewTransactionReceiptsNotification` inner type --- starknet_py/net/schemas/rpc/websockets.py | 17 ++++++++--------- starknet_py/net/websockets/models.py | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index 82dc8eade..ba3584568 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -1,30 +1,27 @@ -from typing import Any, Optional +from typing import Any, Optional, cast from marshmallow import ValidationError, fields, post_load -from starknet_py.net.client_models import Transaction +from starknet_py.net.client_models import Transaction, TransactionStatusWithoutL1 from starknet_py.net.client_utils import _to_rpc_felt -from starknet_py.net.schemas.common import ( - Felt, - TransactionStatusWithoutL1Field, -) +from starknet_py.net.schemas.common import Felt, TransactionStatusWithoutL1Field from starknet_py.net.schemas.rpc.block import BlockHeaderSchema from starknet_py.net.schemas.rpc.event import EmittedEventWithFinalitySchema from starknet_py.net.schemas.rpc.transactions import ( - TransactionReceiptSchema, TransactionStatusResponseSchema, TypesOfTransactionsSchema, + TransactionReceiptWithBlockInfoSchema, ) from starknet_py.net.websockets.models import ( NewEventsNotification, NewHeadsNotification, NewTransactionNotification, + NewTransactionNotificationResult, NewTransactionReceiptsNotification, NewTransactionStatus, ReorgData, ReorgNotification, TransactionStatusNotification, - NewTransactionNotificationResult, ) from starknet_py.utils.schema import Schema @@ -123,7 +120,7 @@ def make_dataclass(self, data, **kwargs) -> ReorgNotification: class NewTransactionReceiptsNotificationSchema(Schema): subscription_id = fields.Str(data_key="subscription_id", required=True) - result = fields.Nested(TransactionReceiptSchema(), data_key="result", required=True) + result = fields.Nested(TransactionReceiptWithBlockInfoSchema(), data_key="result", required=True) @post_load def make_dataclass(self, data, **kwargs) -> NewTransactionReceiptsNotification: @@ -146,10 +143,12 @@ def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): attr="finality_status", data=data, ) + finality_status = cast(TransactionStatusWithoutL1, finality_status) tx_payload = dict(data) tx_payload.pop("finality_status", None) tx_obj = super().load(tx_payload, many=many, partial=partial, unknown=unknown) + tx_obj = cast(Transaction, tx_obj) return NewTransactionNotificationResult( transaction=tx_obj, finality_status=finality_status diff --git a/starknet_py/net/websockets/models.py b/starknet_py/net/websockets/models.py index 2bec2a69d..441b89123 100644 --- a/starknet_py/net/websockets/models.py +++ b/starknet_py/net/websockets/models.py @@ -9,9 +9,9 @@ BlockHeader, EmittedEventWithFinalityStatus, Transaction, - TransactionReceipt, TransactionStatusResponse, TransactionStatusWithoutL1, + TransactionReceiptWithBlockInfo, ) T = TypeVar("T") @@ -78,7 +78,7 @@ class ReorgNotification(Notification[ReorgData]): @dataclass -class NewTransactionReceiptsNotification(Notification[TransactionReceipt]): +class NewTransactionReceiptsNotification(Notification[TransactionReceiptWithBlockInfo]): """ Notification of a new transaction receipt """ From 50865006ddb0d5120a888ebc5cf987a60316047b Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 2 Sep 2025 16:50:02 +0200 Subject: [PATCH 15/32] Add more tests --- .../tests/e2e/client/websocket_client_test.py | 67 ++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/starknet_py/tests/e2e/client/websocket_client_test.py b/starknet_py/tests/e2e/client/websocket_client_test.py index c64d9a0a3..8ac515ecd 100644 --- a/starknet_py/tests/e2e/client/websocket_client_test.py +++ b/starknet_py/tests/e2e/client/websocket_client_test.py @@ -1,6 +1,6 @@ -from typing import Optional, List - import asyncio +from typing import List, Optional + import pytest from starknet_py.devnet_utils.devnet_client import DevnetClient @@ -8,21 +8,21 @@ from starknet_py.net.account.base_account import BaseAccount from starknet_py.net.client_models import ( BlockHeader, - StarknetBlock, Call, + StarknetBlock, TransactionFinalityStatus, TransactionFinalityStatusWithoutL1, - TransactionStatusWithoutL1, TransactionReceipt, + TransactionStatusWithoutL1, ) from starknet_py.net.full_node_client import FullNodeClient from starknet_py.net.websockets.errors import WebsocketClientError from starknet_py.net.websockets.models import ( - NewHeadsNotification, NewEventsNotification, + NewHeadsNotification, NewTransactionNotification, - NewTransactionReceiptsNotification, NewTransactionNotificationResult, + NewTransactionReceiptsNotification, ) from starknet_py.net.websockets.websocket_client import WebsocketClient from starknet_py.tests.e2e.fixtures.constants import MAX_RESOURCE_BOUNDS @@ -141,7 +141,9 @@ def handler(new_events_notification: NewEventsNotification): subscription_id = await websocket_client.subscribe_events( handler=handler, - from_address=deployed_balance_contract.address, + # We subscribe to the events from the account because it emits them for each executed transaction. + # Balance contract doesn't emit anything. + from_address=argent_account_v040.address, finality_status=TransactionFinalityStatusWithoutL1.ACCEPTED_ON_L2, ) @@ -158,7 +160,6 @@ def handler(new_events_notification: NewEventsNotification): await asyncio.sleep(3) assert len(received_events) >= 1 - # Ensure we received at least one event with the requested finality status assert all( ev.finality_status == TransactionFinalityStatus.ACCEPTED_ON_L2 for ev in received_events @@ -199,7 +200,9 @@ def handler(new_tx_notification: NewTransactionNotification): await asyncio.sleep(3) assert len(received) >= 1 - assert all(r.finality_status == TransactionStatusWithoutL1.ACCEPTED_ON_L2 for r in received) + assert all( + r.finality_status == TransactionStatusWithoutL1.ACCEPTED_ON_L2 for r in received + ) unsubscribe_result = await websocket_client.unsubscribe(subscription_id) assert unsubscribe_result is True @@ -242,3 +245,49 @@ def handler(new_tx_receipt: NewTransactionReceiptsNotification): unsubscribe_result = await websocket_client.unsubscribe(subscription_id) assert unsubscribe_result is True + + +@pytest.mark.asyncio +async def test_subscribe_events_with_all_filters( + client: FullNodeClient, + websocket_client: WebsocketClient, + deployed_balance_contract, + argent_account_v040: BaseAccount, +): + received_events: List = [] + + def handler(new_events_notification: NewEventsNotification): + nonlocal received_events + received_events.append(new_events_notification.result) + + latest_block = await client.get_block(block_hash="latest") + assert isinstance(latest_block, StarknetBlock) + + subscription_id = await websocket_client.subscribe_events( + handler=handler, + from_address=argent_account_v040.address, + keys=[[]], + block_number=latest_block.block_number, + finality_status=TransactionFinalityStatusWithoutL1.ACCEPTED_ON_L2, + ) + + increase_balance_call = Call( + to_addr=deployed_balance_contract.address, + selector=get_selector_from_name("increase_balance"), + calldata=[100], + ) + execute = await argent_account_v040.execute_v3( + calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS + ) + + await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) + await asyncio.sleep(3) + + assert len(received_events) >= 1 + assert all( + ev.finality_status == TransactionFinalityStatus.ACCEPTED_ON_L2 + for ev in received_events + ) + + unsubscribe_result = await websocket_client.unsubscribe(subscription_id) + assert unsubscribe_result is True From e1c3a5da30dc5459e1087ef0c2390decc6e6ac94 Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Tue, 2 Sep 2025 18:24:08 +0200 Subject: [PATCH 16/32] Improve error handling for listener --- .../net/websockets/websocket_client.py | 15 ++++- .../tests/e2e/client/websocket_client_test.py | 60 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 1ce55c12b..135656dd8 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -424,10 +424,11 @@ def _handle_received_message(self, message: Union[str, bytes]): future = self._pending_responses.pop(data["id"]) if not future.done(): future.set_result(data) - # Case when the message is a notification elif "method" in data: self._handle_notification(data) + else: + raise ValueError(f"Unexpected message: {data}") def _handle_notification(self, data: Dict): """ @@ -456,3 +457,15 @@ def _handle_error(self, result: dict): message=result["error"]["message"], data=result["error"].get("data"), ) + + # Optional: public awaitable for callers that want to detect failure or closure explicitly + async def wait_closed_or_failed(self) -> None: + """ + Awaits until the listener is canceled (on caling .disconnect() method) + or WebsocketClient fails with an exception. + If .connect() was never called or failure happened, this method returns immediately. + Raises the original exception on failure. + """ + if self._listen_failed is None: + return + await self._listen_failed diff --git a/starknet_py/tests/e2e/client/websocket_client_test.py b/starknet_py/tests/e2e/client/websocket_client_test.py index 8ac515ecd..e6f350e6c 100644 --- a/starknet_py/tests/e2e/client/websocket_client_test.py +++ b/starknet_py/tests/e2e/client/websocket_client_test.py @@ -1,6 +1,9 @@ import asyncio +from json import JSONDecodeError from typing import List, Optional +from unittest.mock import patch +import marshmallow import pytest from starknet_py.devnet_utils.devnet_client import DevnetClient @@ -234,6 +237,7 @@ def handler(new_tx_receipt: NewTransactionReceiptsNotification): execute = await argent_account_v040.execute_v3( calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS ) + print("increase balance hash", execute.transaction_hash) await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) await asyncio.sleep(3) @@ -291,3 +295,59 @@ def handler(new_events_notification: NewEventsNotification): unsubscribe_result = await websocket_client.unsubscribe(subscription_id) assert unsubscribe_result is True + + +@pytest.mark.asyncio +async def test_subscribe_failure(): + class FakeConnection: + async def recv(self): + return "xyz" + + async def send(self, *args, **kwargs): + return + + async def close(self, *args, **kwargs): + return + + async def fake_connect(*_args, **_kwargs): + return FakeConnection() + + with patch( + "starknet_py.net.websockets.websocket_client.connect", side_effect=fake_connect + ): + ws = WebsocketClient("wss://example.invalid") + await ws.connect() + + with pytest.raises(JSONDecodeError): + await ws.subscribe_events(lambda _: None) + + await ws.disconnect() + + +@pytest.mark.asyncio +async def test_listener_failure(): + class FakeConnection: + async def recv(self): + return '{"method": "starknet_subscriptionNewTransactionReceipts", "params": {"subscription_id": "1234", "result": {"unknown_key": 12345}}}' + + async def send(self, *args, **kwargs): + return + + async def close(self, *args, **kwargs): + return + + async def fake_connect(*_args, **_kwargs): + return FakeConnection() + + with patch( + "starknet_py.net.websockets.websocket_client.connect", side_effect=fake_connect + ): + ws = WebsocketClient("wss://example.invalid") + await ws.connect() + + ws._subscriptions["1234"] = lambda _: None + + with pytest.raises(marshmallow.ValidationError): + await ws.wait_closed_or_failed() + + await ws.disconnect() From 088d98365ccbd5fe5dd5eb345b800586a8f9d39f Mon Sep 17 00:00:00 2001 From: Artur Michalek Date: Thu, 4 Sep 2025 11:26:57 +0200 Subject: [PATCH 17/32] Add printing --- starknet_py/net/websockets/websocket_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 135656dd8..5228520b7 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -417,6 +417,7 @@ def _handle_received_message(self, message: Union[str, bytes]): :param message: The message received from the WebSocket server. """ + print("pure response", message) data = cast(Dict, json.loads(message)) # case when the message is a response to `subscribe_{method}` @@ -436,6 +437,8 @@ def _handle_notification(self, data: Dict): :param data: The notification data. """ + print("data ", data) + method: NotificationMethod = data["method"] schema = _NOTIFICATION_SCHEMA_MAPPING[method] notification: Notification = schema().load(data["params"]) From d75cd931731375639ed85418030d6785cdbef910 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 09:54:12 +0200 Subject: [PATCH 18/32] Cleanups --- starknet_py/constants.py | 2 +- starknet_py/net/websockets/websocket_client.py | 13 ++++++------- .../tests/e2e/client/websocket_client_test.py | 1 - .../e2e/docs/code_examples/test_websocket_client.py | 7 +------ 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/starknet_py/constants.py b/starknet_py/constants.py index 07336e498..9fea2a101 100644 --- a/starknet_py/constants.py +++ b/starknet_py/constants.py @@ -53,7 +53,7 @@ class OutsideExecutionInterfaceID(IntEnum): V2 = 0x1D1144BB2138366FF28D8E9AB57456B1D332AC42196230C3A602003C89872 -EXPECTED_RPC_VERSION = "0.9.0-rc.1" +EXPECTED_RPC_VERSION = "0.9.0" ARGENT_V040_CLASS_HASH = ( 0x036078334509B514626504EDC9FB252328D1A240E4E948BEF8D0C08DFF45927F diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 5228520b7..d7083bc79 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -389,7 +389,7 @@ async def _send_message( "params": params if params else [], } - future: asyncio.Future = asyncio.get_running_loop().create_future() + future = asyncio.get_running_loop().create_future() self._pending_responses[message_id] = future await self.connection.send(json.dumps(payload)) @@ -417,7 +417,6 @@ def _handle_received_message(self, message: Union[str, bytes]): :param message: The message received from the WebSocket server. """ - print("pure response", message) data = cast(Dict, json.loads(message)) # case when the message is a response to `subscribe_{method}` @@ -437,8 +436,6 @@ def _handle_notification(self, data: Dict): :param data: The notification data. """ - print("data ", data) - method: NotificationMethod = data["method"] schema = _NOTIFICATION_SCHEMA_MAPPING[method] notification: Notification = schema().load(data["params"]) @@ -464,9 +461,11 @@ def _handle_error(self, result: dict): # Optional: public awaitable for callers that want to detect failure or closure explicitly async def wait_closed_or_failed(self) -> None: """ - Awaits until the listener is canceled (on caling .disconnect() method) - or WebsocketClient fails with an exception. - If .connect() was never called or failure happened, this method returns immediately. + Awaits until the listener is canceled (on calling + :meth:`~net.websockets.websocket_client.WebsocketClient.disconnect` method) or WebsocketClient fails with an + exception. + If :meth:`~net.websockets.websocket_client.WebsocketClient.connect` was never called or failure happened, + this method returns immediately. Raises the original exception on failure. """ if self._listen_failed is None: diff --git a/starknet_py/tests/e2e/client/websocket_client_test.py b/starknet_py/tests/e2e/client/websocket_client_test.py index e6f350e6c..743b1b876 100644 --- a/starknet_py/tests/e2e/client/websocket_client_test.py +++ b/starknet_py/tests/e2e/client/websocket_client_test.py @@ -237,7 +237,6 @@ def handler(new_tx_receipt: NewTransactionReceiptsNotification): execute = await argent_account_v040.execute_v3( calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS ) - print("increase balance hash", execute.transaction_hash) await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) await asyncio.sleep(3) diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index 01acadca7..4462ee8da 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -206,7 +206,6 @@ def handler(transaction_status_notification: TransactionStatusNotification): assert unsubscribe_result is True -@pytest.mark.skip(reason="TODO(cptartur): Investigate why tx hashes assertion fails") @pytest.mark.asyncio async def test_subscribe_new_transaction_receipts( websocket_client: WebsocketClient, @@ -254,7 +253,6 @@ def handler2( # ... # docs-end: subscribe_new_transaction_receipts # assert len(transaction_receipts) == 0 - print("transactions before ", transactions) increase_balance_call = Call( to_addr=deployed_balance_contract.address, @@ -269,15 +267,12 @@ def handler2( await argent_account_v040.client.get_transaction_receipt( tx_hash=execute.transaction_hash ) - print("transaction hash", execute.transaction_hash) - print("transaction receipt", rec) await asyncio.sleep(5) - print("transactions after", transactions) assert len(transaction_receipts) == 1 transaction_receipt = transaction_receipts[0] - assert increase_balance_call.to_addr == transaction_receipt.contract_address + assert execute.transaction_hash == transaction_receipt.transaction_hash # docs-start: subscribe_new_transaction_receipts From 129d5de85ed7fae9d743758f854a95f09fb2dccb Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 10:20:59 +0200 Subject: [PATCH 19/32] Bump devnet to v0.5.1 --- .github/workflows/checks.yml | 2 +- starknet_py/tests/install_devnet.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 816eab876..b07d0a684 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -3,7 +3,7 @@ name: Checks env: CAIRO_LANG_VERSION: "0.13.1" # TODO(#1611) - DEVNET_SHA: 4798b6d516d7559aae02773721ff5fcb50ffccb2 # v0.5.0-rc.4 + DEVNET_SHA: e7ae78e59aaed289c081ab602328349d296ea4db # v0.5.1 LEDGER_APP_SHA: 768a7b47b0da681b28112342edd76e2c9b292c4e # v2.3.1 LEDGER_APP_DEV_TOOLS_SHA: a845b2ab0b5dd824133f73858f6f373edea85ec1bd828245bf50ce9700f33bcb # v4.5.0 diff --git a/starknet_py/tests/install_devnet.sh b/starknet_py/tests/install_devnet.sh index bfea12ba9..b20ebaaaa 100755 --- a/starknet_py/tests/install_devnet.sh +++ b/starknet_py/tests/install_devnet.sh @@ -3,7 +3,7 @@ set -e DEVNET_INSTALL_DIR="$(git rev-parse --show-toplevel)/starknet_py/tests/e2e/devnet/bin" DEVNET_REPO="https://github.com/0xSpaceShard/starknet-devnet-rs" -DEVNET_VERSION="v0.5.0" +DEVNET_VERSION="v0.5.1" require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then From 470a45ef649fe505730fea44fb359249cc02c2d3 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 10:21:29 +0200 Subject: [PATCH 20/32] Fix linting --- starknet_py/net/schemas/rpc/websockets.py | 6 ++++-- starknet_py/net/websockets/models.py | 2 +- .../tests/e2e/client/websocket_client_test.py | 15 ++++++++++----- .../docs/code_examples/test_websocket_client.py | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index ba3584568..9bc430488 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -8,9 +8,9 @@ from starknet_py.net.schemas.rpc.block import BlockHeaderSchema from starknet_py.net.schemas.rpc.event import EmittedEventWithFinalitySchema from starknet_py.net.schemas.rpc.transactions import ( + TransactionReceiptWithBlockInfoSchema, TransactionStatusResponseSchema, TypesOfTransactionsSchema, - TransactionReceiptWithBlockInfoSchema, ) from starknet_py.net.websockets.models import ( NewEventsNotification, @@ -120,7 +120,9 @@ def make_dataclass(self, data, **kwargs) -> ReorgNotification: class NewTransactionReceiptsNotificationSchema(Schema): subscription_id = fields.Str(data_key="subscription_id", required=True) - result = fields.Nested(TransactionReceiptWithBlockInfoSchema(), data_key="result", required=True) + result = fields.Nested( + TransactionReceiptWithBlockInfoSchema(), data_key="result", required=True + ) @post_load def make_dataclass(self, data, **kwargs) -> NewTransactionReceiptsNotification: diff --git a/starknet_py/net/websockets/models.py b/starknet_py/net/websockets/models.py index 441b89123..28508f5a2 100644 --- a/starknet_py/net/websockets/models.py +++ b/starknet_py/net/websockets/models.py @@ -9,9 +9,9 @@ BlockHeader, EmittedEventWithFinalityStatus, Transaction, + TransactionReceiptWithBlockInfo, TransactionStatusResponse, TransactionStatusWithoutL1, - TransactionReceiptWithBlockInfo, ) T = TypeVar("T") diff --git a/starknet_py/tests/e2e/client/websocket_client_test.py b/starknet_py/tests/e2e/client/websocket_client_test.py index 743b1b876..03bf20752 100644 --- a/starknet_py/tests/e2e/client/websocket_client_test.py +++ b/starknet_py/tests/e2e/client/websocket_client_test.py @@ -239,7 +239,7 @@ def handler(new_tx_receipt: NewTransactionReceiptsNotification): ) await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) - await asyncio.sleep(3) + await asyncio.sleep(10) assert len(receipts) >= 1 assert all( @@ -298,14 +298,16 @@ def handler(new_events_notification: NewEventsNotification): @pytest.mark.asyncio async def test_subscribe_failure(): + # pylint: disable=no-self-use class FakeConnection: async def recv(self): return "xyz" + # pylint: disable=unused-argument async def send(self, *args, **kwargs): return - async def close(self, *args, **kwargs): + async def close(self): return async def fake_connect(*_args, **_kwargs): @@ -325,14 +327,16 @@ async def fake_connect(*_args, **_kwargs): @pytest.mark.asyncio async def test_listener_failure(): + # pylint: disable=no-self-use class FakeConnection: async def recv(self): - return '{"method": "starknet_subscriptionNewTransactionReceipts", "params": {"subscription_id": "1234", "result": {"unknown_key": 12345}}}' + # pylint: disable=line-too-long + return '{"method": "starknet_subscriptionNewTransactionReceipts","params": {"subscription_id": "1234", "result": {"unknown_key": 12345}}}' - async def send(self, *args, **kwargs): + async def send(self): return - async def close(self, *args, **kwargs): + async def close(self): return async def fake_connect(*_args, **_kwargs): @@ -344,6 +348,7 @@ async def fake_connect(*_args, **_kwargs): ws = WebsocketClient("wss://example.invalid") await ws.connect() + # pylint: disable=protected-access ws._subscriptions["1234"] = lambda _: None with pytest.raises(marshmallow.ValidationError): diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index 4462ee8da..a4f16b200 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -263,7 +263,7 @@ def handler2( calls=increase_balance_call, resource_bounds=MAX_RESOURCE_BOUNDS ) - rec = await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) + await argent_account_v040.client.wait_for_tx(tx_hash=execute.transaction_hash) await argent_account_v040.client.get_transaction_receipt( tx_hash=execute.transaction_hash ) From c0fe46b452c58fb1f4bdf7b65dc2a1a07e1757e0 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 11:18:34 +0200 Subject: [PATCH 21/32] Fix tests --- starknet_py/net/websockets/websocket_client.py | 7 +++++++ .../tests/e2e/docs/code_examples/test_websocket_client.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index d7083bc79..5cb389895 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -73,6 +73,7 @@ def __init__(self, node_url: str): self._subscriptions: Dict[str, NotificationHandler] = {} self._message_id = 0 self._pending_responses: Dict[int, asyncio.Future] = {} + self._pending_notifications: Dict[str, List[Notification]] = {} self._on_chain_reorg: Optional[Callable[[ReorgNotification], Any]] = None # New: future that completes with an exception if listen loop dies @@ -329,6 +330,10 @@ async def _subscribe( subscription_id = response_message["result"] self._subscriptions[subscription_id] = handler + pending = self._pending_notifications.pop(subscription_id, []) + for notification in pending: + handler(notification) + return subscription_id async def _listen(self): @@ -440,7 +445,9 @@ def _handle_notification(self, data: Dict): schema = _NOTIFICATION_SCHEMA_MAPPING[method] notification: Notification = schema().load(data["params"]) + # Notification may arrive before the subscription is registered if notification.subscription_id not in self._subscriptions: + self._pending_notifications.setdefault(notification.subscription_id, []).append(notification) return if isinstance(notification, ReorgNotification): diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index a4f16b200..a490ba616 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -184,7 +184,7 @@ def handler(transaction_status_notification: TransactionStatusNotification): tx_hash=execute.transaction_hash ) - await asyncio.sleep(10) + await asyncio.sleep(5) assert new_transaction_status is not None assert new_transaction_status.transaction_hash == execute.transaction_hash From 640d0a6d18dc91e02f1f1d8181a0280600ec36c5 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 11:18:51 +0200 Subject: [PATCH 22/32] Formatting --- starknet_py/net/websockets/websocket_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/starknet_py/net/websockets/websocket_client.py b/starknet_py/net/websockets/websocket_client.py index 5cb389895..28ed26fda 100644 --- a/starknet_py/net/websockets/websocket_client.py +++ b/starknet_py/net/websockets/websocket_client.py @@ -447,7 +447,9 @@ def _handle_notification(self, data: Dict): # Notification may arrive before the subscription is registered if notification.subscription_id not in self._subscriptions: - self._pending_notifications.setdefault(notification.subscription_id, []).append(notification) + self._pending_notifications.setdefault( + notification.subscription_id, [] + ).append(notification) return if isinstance(notification, ReorgNotification): From 11a21e1ec90f327ae5fd868c2cb07499686b07b0 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 11:22:53 +0200 Subject: [PATCH 23/32] Update migration guide --- docs/migration_guide.rst | 5 +++++ .../tests/e2e/docs/code_examples/test_websocket_client.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index 1440f891f..e331dc885 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -7,6 +7,11 @@ Migration guide Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. +0.28.0 Targeted versions +------------------------ +- Starknet - `0.14.0 `_ +- RPC - `0.9.0 `_ + .. py:currentmodule:: starknet_py.net.websockets.websocket_client 1. Removed ``subscribe_pending_transactions`` method and respective notification. diff --git a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py index a490ba616..7904a8127 100644 --- a/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py +++ b/starknet_py/tests/e2e/docs/code_examples/test_websocket_client.py @@ -220,7 +220,7 @@ async def test_subscribe_new_transaction_receipts( from starknet_py.net.websockets.models import NewTransactionReceiptsNotification # Create a handler function that will be called when a new transaction is emitted - def handler( + def handler_a( new_transaction_receipts_notification: NewTransactionReceiptsNotification, ): # Perform the necessary actions with the new transaction receipts... @@ -229,7 +229,7 @@ def handler( nonlocal transaction_receipts transaction_receipts.append(new_transaction_receipts_notification.result) - def handler2( + def handler_b( new_transaction_notification: NewTransactionNotification, ): # Perform the necessary actions with the new transaction receipts... @@ -241,11 +241,11 @@ def handler2( # docs-start: subscribe_new_transaction_receipts # Subscribe to new transaction receipts notifications subscription_id = await websocket_client.subscribe_new_transaction_receipts( - handler=handler, + handler=handler_a, sender_address=[account.address], ) subscription_id2 = await websocket_client.subscribe_new_transactions( - handler=handler2, + handler=handler_b, sender_address=[account.address], ) From 90ad43f7ebc8837b9e15491b566313abd49fc114 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 11:38:51 +0200 Subject: [PATCH 24/32] Formatting --- starknet_py/net/schemas/rpc/websockets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/starknet_py/net/schemas/rpc/websockets.py b/starknet_py/net/schemas/rpc/websockets.py index 9bc430488..ec0d1ed57 100644 --- a/starknet_py/net/schemas/rpc/websockets.py +++ b/starknet_py/net/schemas/rpc/websockets.py @@ -130,7 +130,6 @@ def make_dataclass(self, data, **kwargs) -> NewTransactionReceiptsNotification: class TypesOfTransactionWithFinalitySchema(TypesOfTransactionsSchema): - def load(self, data, *, many=None, partial=None, unknown=None, **kwargs): if not isinstance(data, dict): raise ValidationError( From 5bebcab9fd95bab46f4c1fa4a3dcfcc13a4ae333 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 12:59:33 +0200 Subject: [PATCH 25/32] Add missing params in `ContractFunction.invoke_v3` --- docs/migration_guide.rst | 1 + starknet_py/contract.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index e331dc885..666e41aa1 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -17,6 +17,7 @@ Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. 1. Removed ``subscribe_pending_transactions`` method and respective notification. 2. Added :meth:`WebsocketClient.subscribe_new_transactions` and :meth:`WebsocketClient.subscribe_new_transaction_receipts` and respective notifications. 3. Added field ``finality_status`` to :meth:`WebsocketClient.subscribe_events`, changed ``NewEventsNotification`` that is used in the handler inner type to contain finalty status. +4. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`starknet_py.net.contract.ContractFunction.invoke_v3`. *************************** 0.28.0-rc.4 Migration guide diff --git a/starknet_py/contract.py b/starknet_py/contract.py index 25decfead..39532f606 100644 --- a/starknet_py/contract.py +++ b/starknet_py/contract.py @@ -558,6 +558,8 @@ async def invoke_v3( *args, resource_bounds: Optional[ResourceBoundsMapping] = None, auto_estimate: bool = False, + tip: Optional[int] = None, + auto_estimate_tip: bool = False, nonce: Optional[int] = None, **kwargs, ) -> InvokeResult: @@ -567,6 +569,8 @@ async def invoke_v3( :param resource_bounds: Resource limits (L1 and L2) used when executing this transaction. :param auto_estimate: Use automatic fee estimation (not recommended, as it may lead to high costs). + :param tip: The tip amount to be added to the transaction fee. + :param auto_estimate_tip: Use automatic tip estimation. Using this option may lead to higher costs. :param nonce: Nonce of the transaction. :return: InvokeResult. """ @@ -575,6 +579,8 @@ async def invoke_v3( resource_bounds=resource_bounds, nonce=nonce, auto_estimate=auto_estimate, + tip=tip, + auto_estimate_tip=auto_estimate_tip, ) @staticmethod From 56f0484606298402ddcbb0832ac7eb9faa49adcc Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Tue, 9 Sep 2025 13:02:23 +0200 Subject: [PATCH 26/32] Update migration guide --- docs/migration_guide.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index 666e41aa1..449c90ba4 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -17,7 +17,10 @@ Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. 1. Removed ``subscribe_pending_transactions`` method and respective notification. 2. Added :meth:`WebsocketClient.subscribe_new_transactions` and :meth:`WebsocketClient.subscribe_new_transaction_receipts` and respective notifications. 3. Added field ``finality_status`` to :meth:`WebsocketClient.subscribe_events`, changed ``NewEventsNotification`` that is used in the handler inner type to contain finalty status. -4. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`starknet_py.net.contract.ContractFunction.invoke_v3`. + +.. py:currentmodule:: starknet_py.contract + +4. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`ContractFunction.invoke_v3`. *************************** 0.28.0-rc.4 Migration guide From df46fad440d8d6ecc36182efc5859d82d2e935fc Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Wed, 10 Sep 2025 13:39:27 +0200 Subject: [PATCH 27/32] Update migration guide --- docs/migration_guide.rst | 65 +++++----------------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index 9e80fc191..a2950a1a2 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -59,6 +59,13 @@ Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. .. py:currentmodule:: starknet_py.net.account.account +``starknet_py.contract`` Changes +------------------------------------------- + +.. py:currentmodule:: starknet_py.contract + +4. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`ContractFunction.invoke_v3`. + 1. When no ``token_address`` is specified in the :meth:`Account.get_balance` method, the default token address is now the STRK fee contract instead of ETH. 2. Rename ``FEE_CONTRACT_ADDRESS`` to ``ETH_FEE_CONTRACT_ADDRESS``. @@ -68,64 +75,6 @@ Transaction Tip Support Ability to pass tip for the transaction has been added to following methods. If ``tip`` is not provided, a default value of ``0`` will be used -.. py:currentmodule:: starknet_py.contract - -- :meth:`DeclareResult.deploy_v3` -- :meth:`PreparedFunctionInvokeV3.invoke` -- :meth:`Contract.declare_v3` -- :meth:`Contract.deploy_contract_v3` - -.. py:currentmodule:: starknet_py.net.account.account - -- :meth:`Account.sign_invoke_v3` -- :meth:`Account.sign_declare_v3` -- :meth:`Account.sign_deploy_account_v3` -- :meth:`Account.execute_v3` -- :meth:`Account.deploy_account_v3` - -Additionally, dataclasses representing transactions now require passing a tip. -No default value is used for tip and it is a required parameter. - -.. py:currentmodule:: starknet_py.net.models.transaction - -- :class:`InvokeV3`, tip is now required -- :class:`DeclareV3`, tip is now required -- :class:`DeployAccountV3`, tip is now required - -Transaction Tip Estimation --------------------------- - -.. py:currentmodule:: starknet_py.net.tip - -1. Added :func:`estimate_tip` for automatic transaction tip estimation. -2. Added ``auto_estimate_tip`` param to :class:`~starknet_py.net.account.account.Account` and :class:`~starknet_py.contract.Contract` methods that accept a ``tip`` argument. If set to ``True``, median of tips from the ``pre_confirmed`` block will be used to estimate select at tip. - -Deployment with UDC -------------------- - -.. py:currentmodule:: starknet_py.net.udc_deployer.deployer - -1. Default deployer address in :class:`Deployer` is now the new UDC (``0x02ceed65a4bd731034c01113685c831b01c15d7d432f71afb1cf1634b53a2125``). - -Other Changes -------------- - -.. currentmodule:: starknet_py.devnet_utils.devnet_client - -1. ``unit`` param in :meth:`DevnetClient.mint` now defaults to ``PriceUnit.FRI``. - -.. py:currentmodule:: starknet_py.net.signer.eth_signer - -2. :class:`EthSigner` implementation has been added. - -0.28.0 Bugfixes ---------------- - -.. py:currentmodule:: starknet_py.contract - -1. Contracts which include fixed sized array type are now correctly serialized (e.g. when using :meth:`Contract.deploy_contract_v3`) - - *************************** 0.28.0-rc.4 Migration guide *************************** From 5d77ba4bb53c79519c21cf42556a087d38562938 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Wed, 10 Sep 2025 13:43:55 +0200 Subject: [PATCH 28/32] Update migration guide --- docs/migration_guide.rst | 65 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index a2950a1a2..7ec426d84 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -59,22 +59,79 @@ Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. .. py:currentmodule:: starknet_py.net.account.account +1. When no ``token_address`` is specified in the :meth:`Account.get_balance` method, the default token address is now the STRK fee contract instead of ETH. +2. Rename ``FEE_CONTRACT_ADDRESS`` to ``ETH_FEE_CONTRACT_ADDRESS``. + ``starknet_py.contract`` Changes ------------------------------------------- .. py:currentmodule:: starknet_py.contract - 4. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`ContractFunction.invoke_v3`. -1. When no ``token_address`` is specified in the :meth:`Account.get_balance` method, the default token address is now the STRK fee contract instead of ETH. -2. Rename ``FEE_CONTRACT_ADDRESS`` to ``ETH_FEE_CONTRACT_ADDRESS``. - Transaction Tip Support ----------------------- Ability to pass tip for the transaction has been added to following methods. If ``tip`` is not provided, a default value of ``0`` will be used +.. py:currentmodule:: starknet_py.contract + +- :meth:`DeclareResult.deploy_v3` +- :meth:`PreparedFunctionInvokeV3.invoke` +- :meth:`Contract.declare_v3` +- :meth:`Contract.deploy_contract_v3` + +.. py:currentmodule:: starknet_py.net.account.account + +- :meth:`Account.sign_invoke_v3` +- :meth:`Account.sign_declare_v3` +- :meth:`Account.sign_deploy_account_v3` +- :meth:`Account.execute_v3` +- :meth:`Account.deploy_account_v3` + +Additionally, dataclasses representing transactions now require passing a tip. +No default value is used for tip and it is a required parameter. + +.. py:currentmodule:: starknet_py.net.models.transaction + +- :class:`InvokeV3`, tip is now required +- :class:`DeclareV3`, tip is now required +- :class:`DeployAccountV3`, tip is now required + +Transaction Tip Estimation +-------------------------- + +.. py:currentmodule:: starknet_py.net.tip + +1. Added :func:`estimate_tip` for automatic transaction tip estimation. +2. Added ``auto_estimate_tip`` param to :class:`~starknet_py.net.account.account.Account` and :class:`~starknet_py.contract.Contract` methods that accept a ``tip`` argument. If set to ``True``, median of tips from the ``pre_confirmed`` block will be used to estimate select at tip. + +Deployment with UDC +------------------- + +.. py:currentmodule:: starknet_py.net.udc_deployer.deployer + +1. Default deployer address in :class:`Deployer` is now the new UDC (``0x02ceed65a4bd731034c01113685c831b01c15d7d432f71afb1cf1634b53a2125``). + +Other Changes +------------- + +.. currentmodule:: starknet_py.devnet_utils.devnet_client + +1. ``unit`` param in :meth:`DevnetClient.mint` now defaults to ``PriceUnit.FRI``. + +.. py:currentmodule:: starknet_py.net.signer.eth_signer + +2. :class:`EthSigner` implementation has been added. + +0.28.0 Bugfixes +--------------- + +.. py:currentmodule:: starknet_py.contract + +1. Contracts which include fixed sized array type are now correctly serialized (e.g. when using :meth:`Contract.deploy_contract_v3`) + + *************************** 0.28.0-rc.4 Migration guide *************************** From 192d5a01289505c1d82b56e8351bf87e71014219 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Wed, 10 Sep 2025 13:48:46 +0200 Subject: [PATCH 29/32] Fix docs --- docs/migration_guide.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/migration_guide.rst b/docs/migration_guide.rst index 7ec426d84..f0797da81 100644 --- a/docs/migration_guide.rst +++ b/docs/migration_guide.rst @@ -66,7 +66,8 @@ Version 0.28.0 of **starknet.py** comes with full support for RPC 0.9.0. ------------------------------------------- .. py:currentmodule:: starknet_py.contract -4. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`ContractFunction.invoke_v3`. + +1. Added missing ``tip`` and ``auto_estimate_tip`` to :meth:`ContractFunction.invoke_v3`. Transaction Tip Support ----------------------- From 46c31c06cd14c020ec59e0a6a8b008defb261190 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Wed, 10 Sep 2025 13:50:26 +0200 Subject: [PATCH 30/32] Bump version to `0.28.0` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8cf851e19..22eee7cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "starknet-py" -version = "0.28.0-rc.4" +version = "0.28.0" description = "A python SDK for Starknet" authors = [ { name = "Tomasz Rejowski", email = "tomasz.rejowski@swmansion.com" }, From 8a64cc0f390b186d7b15bb7ecd7e55515a095262 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Wed, 10 Sep 2025 13:52:17 +0200 Subject: [PATCH 31/32] Revert "Bump version to `0.28.0`" This reverts commit 46c31c06cd14c020ec59e0a6a8b008defb261190. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 22eee7cb0..8cf851e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "starknet-py" -version = "0.28.0" +version = "0.28.0-rc.4" description = "A python SDK for Starknet" authors = [ { name = "Tomasz Rejowski", email = "tomasz.rejowski@swmansion.com" }, From 4d576ac9be0b68f827d15b62207a3633abb41078 Mon Sep 17 00:00:00 2001 From: Fiiranek Date: Wed, 10 Sep 2025 13:54:12 +0200 Subject: [PATCH 32/32] Bump version to 0.28.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8cf851e19..22eee7cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "starknet-py" -version = "0.28.0-rc.4" +version = "0.28.0" description = "A python SDK for Starknet" authors = [ { name = "Tomasz Rejowski", email = "tomasz.rejowski@swmansion.com" },