From 942bed371e287c98c9d0518a3d1879bbe1294f76 Mon Sep 17 00:00:00 2001 From: amalcaraz Date: Mon, 16 Feb 2026 18:30:29 +0100 Subject: [PATCH] feat: support credit payment in store messages --- pyproject.toml | 3 +- src/aleph/sdk/client/abstract.py | 2 + src/aleph/sdk/client/authenticated_http.py | 11 +++- tests/unit/test_asynchronous.py | 69 ++++++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 46adc715..2bf21974 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dynamic = [ "version" ] dependencies = [ "aiohttp>=3.8.3", "aioresponses>=0.7.6", - "aleph-message>=1.0.5", + "aleph-message>=1.1", "aleph-superfluid>=0.3", "base58==2.1.1", # Needed now as default with _load_account changement "coincurve; python_version>='3.9'", @@ -115,6 +115,7 @@ include = [ python = [ "3.9", "3.10", "3.11" ] [tool.hatch.envs.testing] +python = "3.13" features = [ "cosmos", "dns", diff --git a/src/aleph/sdk/client/abstract.py b/src/aleph/sdk/client/abstract.py index 894717a2..3daa198e 100644 --- a/src/aleph/sdk/client/abstract.py +++ b/src/aleph/sdk/client/abstract.py @@ -388,6 +388,7 @@ async def create_store( extra_fields: Optional[dict] = None, channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, + payment: Optional[Payment] = None, ) -> Tuple[AlephMessage, MessageStatus]: """ Create a STORE message to store a file on the aleph.im network. @@ -404,6 +405,7 @@ async def create_store( :param extra_fields: Extra fields to add to the STORE message (Default: None) :param channel: Channel to post the message to (Default: "TEST") :param sync: If true, waits for the message to be processed by the API server (Default: False) + :param payment: Payment method used to pay for storage (Default: hold on ETH) """ raise NotImplementedError( "Did you mean to import `AuthenticatedAlephHttpClient`?" diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 4528a5b7..11aa08f0 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -12,6 +12,7 @@ AggregateContent, AggregateMessage, AlephMessage, + Chain, ForgetContent, ForgetMessage, InstanceMessage, @@ -24,7 +25,7 @@ StoreContent, StoreMessage, ) -from aleph_message.models.execution.base import Encoding, Payment +from aleph_message.models.execution.base import Encoding, Payment, PaymentType from aleph_message.models.execution.environment import ( HostRequirements, HypervisorType, @@ -350,8 +351,12 @@ async def create_store( extra_fields: Optional[dict] = None, channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, + payment: Optional[Payment] = None, ) -> Tuple[StoreMessage, MessageStatus]: address = address or settings.ADDRESS_TO_USE or self.account.get_address() + payment = payment or Payment( + chain=Chain.ETH, type=PaymentType.hold, receiver=None + ) extra_fields = extra_fields or {} @@ -374,6 +379,7 @@ async def create_store( extra_fields=extra_fields, channel=channel, sync=sync, + payment=payment, ) elif storage_engine == StorageEnum.ipfs: # We do not support authenticated upload for IPFS yet. Use the legacy method @@ -397,6 +403,7 @@ async def create_store( "item_type": storage_engine, "item_hash": file_hash, "time": time.time(), + "payment": payment, } if extra_fields is not None: values.update(extra_fields) @@ -660,6 +667,7 @@ async def _upload_file_native( extra_fields: Optional[dict] = None, channel: Optional[str] = settings.DEFAULT_CHANNEL, sync: bool = False, + payment: Optional[Payment] = None, ) -> Tuple[StoreMessage, MessageStatus]: file_hash = hashlib.sha256(file_content).hexdigest() if magic and guess_mime_type: @@ -674,6 +682,7 @@ async def _upload_file_native( item_hash=ItemHash(file_hash), mime_type=mime_type, # type: ignore time=time.time(), + payment=payment, **(extra_fields or {}), ) message, _ = await self._storage_push_file_with_message( diff --git a/tests/unit/test_asynchronous.py b/tests/unit/test_asynchronous.py index e2647590..1221a9b0 100644 --- a/tests/unit/test_asynchronous.py +++ b/tests/unit/test_asynchronous.py @@ -293,3 +293,72 @@ async def test_create_instance_insufficient_funds_error( receiver=None, ), ) + + +@pytest.mark.asyncio +async def test_create_instance_with_credit_payment(mock_session_with_post_success): + """Test that an instance can be created with credit payment.""" + async with mock_session_with_post_success as session: + instance_message, message_status = await session.create_instance( + rootfs="cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe", + rootfs_size=1, + channel="TEST", + metadata={"tags": ["test"]}, + payment=Payment( + chain=Chain.ETH, + receiver=None, + type=PaymentType.credit, + ), + ) + + assert instance_message.content.payment.type == PaymentType.credit + assert instance_message.content.payment.chain == Chain.ETH + assert instance_message.content.payment.receiver is None + + assert mock_session_with_post_success.http_session.post.assert_called_once + assert isinstance(instance_message, InstanceMessage) + + +@pytest.mark.asyncio +async def test_create_store_with_credit_payment(mock_session_with_post_success): + """Test that a store message can be created with credit payment.""" + mock_ipfs_push_file = AsyncMock() + mock_ipfs_push_file.return_value = "QmRTV3h1jLcACW4FRfdisokkQAk4E4qDhUzGpgdrd4JAFy" + + mock_session_with_post_success.ipfs_push_file = mock_ipfs_push_file + + async with mock_session_with_post_success as session: + store_message, message_status = await session.create_store( + file_content=b"HELLO", + channel="TEST", + storage_engine=StorageEnum.ipfs, + payment=Payment( + chain=Chain.ETH, + receiver=None, + type=PaymentType.credit, + ), + ) + + assert store_message.content.payment.type == PaymentType.credit + assert store_message.content.payment.chain == Chain.ETH + assert isinstance(store_message, StoreMessage) + + +@pytest.mark.asyncio +async def test_create_store_default_payment(mock_session_with_post_success): + """Test that a store message defaults to hold payment on ETH.""" + mock_ipfs_push_file = AsyncMock() + mock_ipfs_push_file.return_value = "QmRTV3h1jLcACW4FRfdisokkQAk4E4qDhUzGpgdrd4JAFy" + + mock_session_with_post_success.ipfs_push_file = mock_ipfs_push_file + + async with mock_session_with_post_success as session: + store_message, message_status = await session.create_store( + file_content=b"HELLO", + channel="TEST", + storage_engine=StorageEnum.ipfs, + ) + + assert store_message.content.payment.type == PaymentType.hold + assert store_message.content.payment.chain == Chain.ETH + assert isinstance(store_message, StoreMessage)