diff --git a/hathor/client.py b/hathor/client.py index f6b1ece41..eda6edf5a 100644 --- a/hathor/client.py +++ b/hathor/client.py @@ -14,7 +14,6 @@ """ Module that contains a Python API for interacting with a portion of the HTTP/WS APIs """ - import asyncio import random import string @@ -32,6 +31,7 @@ from hathor.mining import BlockTemplate, BlockTemplates from hathor.pubsub import EventArguments, HathorEvents from hathor.transaction import BaseTransaction, Block, TransactionMetadata +from hathor.transaction.base_transaction import get_cls_from_tx_version from hathor.transaction.storage import TransactionStorage logger = get_logger() @@ -390,7 +390,7 @@ def create_tx_from_dict(data: dict[str, Any], update_hash: bool = False, if storage: data['storage'] = storage - cls = TxVersion(data['version']).get_cls() + cls = get_cls_from_tx_version(TxVersion(data['version'])) metadata = data.pop('metadata', None) tx = cls(**data) if update_hash: diff --git a/hathor/mining/block_template.py b/hathor/mining/block_template.py index 6ab4ba776..a6dc80b70 100644 --- a/hathor/mining/block_template.py +++ b/hathor/mining/block_template.py @@ -15,10 +15,10 @@ """ Module for abstractions around generating mining templates. """ - from typing import Iterable, NamedTuple, Optional, TypeVar, cast from hathor.transaction import BaseTransaction, Block, MergeMinedBlock +from hathor.transaction.base_transaction import get_cls_from_tx_version from hathor.transaction.poa import PoaBlock from hathor.transaction.storage import TransactionStorage from hathor.util import Random @@ -42,7 +42,7 @@ class BlockTemplate(NamedTuple): def generate_minimally_valid_block(self) -> BaseTransaction: """ Generates a block, without any extra information that is valid for this template. No random choices.""" from hathor.transaction import TxOutput, TxVersion - return TxVersion(min(self.versions)).get_cls()( + return get_cls_from_tx_version(TxVersion(min(self.versions)))( timestamp=self.timestamp_min, parents=self.parents[:] + sorted(self.parents_any)[:(3 - len(self.parents))], outputs=[TxOutput(self.reward, b'')], diff --git a/hathor/nanocontracts/blueprint.py b/hathor/nanocontracts/blueprint.py index 297363831..00021a7a5 100644 --- a/hathor/nanocontracts/blueprint.py +++ b/hathor/nanocontracts/blueprint.py @@ -12,140 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, final - -from hathor.nanocontracts.blueprint_env import BlueprintEnvironment -from hathor.nanocontracts.exception import BlueprintSyntaxError -from hathor.nanocontracts.nc_types.utils import pretty_type -from hathor.nanocontracts.types import NC_FALLBACK_METHOD, NC_INITIALIZE_METHOD, NC_METHOD_TYPE_ATTR, NCMethodType - -if TYPE_CHECKING: - from hathor.nanocontracts.nc_exec_logs import NCLogger - -FORBIDDEN_NAMES = { - 'syscall', - 'log', -} - -NC_FIELDS_ATTR: str = '__fields' - - -class _BlueprintBase(type): - """Metaclass for blueprints. - - This metaclass will modify the attributes and set Fields to them according to their types. - """ - - def __new__( - cls: type[_BlueprintBase], - name: str, - bases: tuple[type, ...], - attrs: dict[str, Any], - /, - **kwargs: Any - ) -> _BlueprintBase: - from hathor.nanocontracts.fields import make_field_for_type - - # Initialize only subclasses of Blueprint. - parents = [b for b in bases if isinstance(b, _BlueprintBase)] - if not parents: - return super().__new__(cls, name, bases, attrs, **kwargs) - - cls._validate_initialize_method(attrs) - cls._validate_fallback_method(attrs) - nc_fields = attrs.get('__annotations__', {}) - - # Check for forbidden names. - for field_name in nc_fields: - if field_name in FORBIDDEN_NAMES: - raise BlueprintSyntaxError(f'field name is forbidden: `{field_name}`') - - if field_name.startswith('_'): - raise BlueprintSyntaxError(f'field name cannot start with underscore: `{field_name}`') - - # Create the fields attribute with the type for each field. - attrs[NC_FIELDS_ATTR] = nc_fields - - # Use an empty __slots__ to prevent storing any attributes directly on instances. - # The declared attributes are stored as fields on the class, so they still work despite the empty slots. - attrs['__slots__'] = tuple() - - # Finally, create class! - new_class = super().__new__(cls, name, bases, attrs, **kwargs) - - # Create the Field instance according to each type. - for field_name, field_type in attrs[NC_FIELDS_ATTR].items(): - value = getattr(new_class, field_name, None) - if value is None: - # This is the case when a type is specified but not a value. - # Example: - # name: str - # age: int - try: - field = make_field_for_type(field_name, field_type) - except TypeError: - raise BlueprintSyntaxError( - f'unsupported field type: `{field_name}: {pretty_type(field_type)}`' - ) - setattr(new_class, field_name, field) - else: - # This is the case when a value is specified. - # Example: - # name: str = StrField() - # - # This was not implemented yet and will be extended later. - raise BlueprintSyntaxError(f'fields with default values are currently not supported: `{field_name}`') - - return new_class - - @staticmethod - def _validate_initialize_method(attrs: Any) -> None: - if NC_INITIALIZE_METHOD not in attrs: - raise BlueprintSyntaxError(f'blueprints require a method called `{NC_INITIALIZE_METHOD}`') - - method = attrs[NC_INITIALIZE_METHOD] - method_type = getattr(method, NC_METHOD_TYPE_ATTR, None) - - if method_type is not NCMethodType.PUBLIC: - raise BlueprintSyntaxError(f'`{NC_INITIALIZE_METHOD}` method must be annotated with @public') - - @staticmethod - def _validate_fallback_method(attrs: Any) -> None: - if NC_FALLBACK_METHOD not in attrs: - return - - method = attrs[NC_FALLBACK_METHOD] - method_type = getattr(method, NC_METHOD_TYPE_ATTR, None) - - if method_type is not NCMethodType.FALLBACK: - raise BlueprintSyntaxError(f'`{NC_FALLBACK_METHOD}` method must be annotated with @fallback') - - -class Blueprint(metaclass=_BlueprintBase): - """Base class for all blueprints. - - Example: - - class MyBlueprint(Blueprint): - name: str - age: int - """ - - __slots__ = ('__env',) - - def __init__(self, env: BlueprintEnvironment) -> None: - self.__env = env - - @final - @property - def syscall(self) -> BlueprintEnvironment: - """Return the syscall provider for the current contract.""" - return self.__env - - @final - @property - def log(self) -> NCLogger: - """Return the logger for the current contract.""" - return self.syscall.__log__ +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.blueprint import * # noqa: F401,F403 +from hathorlib.nanocontracts.blueprint import NC_FIELDS_ATTR, Blueprint, _BlueprintBase # noqa: F401 diff --git a/hathor/nanocontracts/blueprint_env.py b/hathor/nanocontracts/blueprint_env.py index bcf0e68db..2f0274d2f 100644 --- a/hathor/nanocontracts/blueprint_env.py +++ b/hathor/nanocontracts/blueprint_env.py @@ -12,265 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Collection, Sequence, TypeAlias, final - -from hathor.conf.settings import HATHOR_TOKEN_UID -from hathor.nanocontracts.nano_settings import NanoSettings -from hathor.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, NCFee, TokenUid - -if TYPE_CHECKING: - from hathor.nanocontracts.contract_accessor import ContractAccessor - from hathor.nanocontracts.initialize_method_accessor import InitializeMethodAccessor - from hathor.nanocontracts.nc_exec_logs import NCLogger - from hathor.nanocontracts.proxy_accessor import ProxyAccessor - from hathor.nanocontracts.rng import NanoRNG - from hathor.nanocontracts.runner import Runner - from hathor.nanocontracts.storage import NCContractStorage - - -NCAttrCache: TypeAlias = dict[bytes, Any] | None - - -@final -class BlueprintEnvironment: - """A class that holds all possible interactions a blueprint may have with the system.""" - - __slots__ = ('__runner', '__log__', '__storage__', '__cache__') - - def __init__( - self, - runner: Runner, - nc_logger: NCLogger, - storage: NCContractStorage, - *, - disable_cache: bool = False, - ) -> None: - self.__log__ = nc_logger - self.__runner = runner - self.__storage__ = storage - # XXX: we could replace dict|None with a cache class that can be disabled, cleared, limited, etc - self.__cache__: NCAttrCache = None if disable_cache else {} - - @property - def rng(self) -> NanoRNG: - """Return an RNG for the current contract.""" - return self.__runner.syscall_get_rng() - - def get_contract_id(self) -> ContractId: - """Return the ContractId of the current nano contract.""" - return self.__runner.get_current_contract_id() - - def get_blueprint_id(self) -> BlueprintId: - """ - Return the BlueprintId of the current nano contract. - - This means that during a proxy call, this method will return the BlueprintId of the caller's blueprint, - NOT the BlueprintId of the Blueprint that owns the running code. - """ - contract_id = self.get_contract_id() - return self.__runner.get_blueprint_id(contract_id) - - def get_current_code_blueprint_id(self) -> BlueprintId: - """ - Return the BlueprintId of the Blueprint that owns the currently running code. - - This means that during a proxy call, this method will return the BlueprintId of the Blueprint that owns the - running code, NOT the BlueprintId of the current nano contract. - """ - return self.__runner.get_current_code_blueprint_id() - - def get_balance_before_current_call(self, token_uid: TokenUid | None = None) -> Amount: - """ - Return the balance for a given token before the current call, that is, - excluding any actions and changes in the current call. - - For instance, if a contract has 50 HTR and the call is requesting to withdraw 3 HTR, - then this method will return 50 HTR.""" - contract_id = self.get_contract_id() - balance = self.__runner.get_balance_before_current_call(contract_id, token_uid) - return Amount(balance.value) - - def get_current_balance(self, token_uid: TokenUid | None = None) -> Amount: - """ - Return the current balance for a given token, which includes all actions and changes in the current call. - - For instance, if a contract has 50 HTR and the call is requesting to withdraw 3 HTR, - then this method will return 47 HTR. - """ - contract_id = self.get_contract_id() - balance = self.__runner.get_current_balance(contract_id, token_uid) - return Amount(balance.value) - - def can_mint_before_current_call(self, token_uid: TokenUid) -> bool: - """ - Return whether a given token could be minted before the current call, that is, - excluding any actions and changes in the current call. - - For instance, if a contract has a mint authority and a call is revoking it, - then this method will return `True`. - """ - contract_id = self.get_contract_id() - balance = self.__runner.get_balance_before_current_call(contract_id, token_uid) - return balance.can_mint - - def can_mint(self, token_uid: TokenUid) -> bool: - """ - Return whether a given token can currently be minted, - which includes all actions and changes in the current call. - - For instance, if a contract has a mint authority and a call is revoking it, - then this method will return `False`. - """ - contract_id = self.get_contract_id() - balance = self.__runner.get_current_balance(contract_id, token_uid) - return balance.can_mint - - def can_melt_before_current_call(self, token_uid: TokenUid) -> bool: - """ - Return whether a given token could be melted before the current call, that is, - excluding any actions and changes in the current call. - - For instance, if a contract has a melt authority and a call is revoking it, - then this method will return `True`. - """ - contract_id = self.get_contract_id() - balance = self.__runner.get_balance_before_current_call(contract_id, token_uid) - return balance.can_melt - - def can_melt(self, token_uid: TokenUid) -> bool: - """ - Return whether a given token can currently be melted, - which includes all actions and changes in the current call. - - For instance, if a contract has a melt authority and a transaction is revoking it, - then this method will return `False`. - """ - contract_id = self.get_contract_id() - balance = self.__runner.get_current_balance(contract_id, token_uid) - return balance.can_melt - - def revoke_authorities(self, token_uid: TokenUid, *, revoke_mint: bool, revoke_melt: bool) -> None: - """Revoke authorities from this nano contract.""" - self.__runner.syscall_revoke_authorities(token_uid=token_uid, revoke_mint=revoke_mint, revoke_melt=revoke_melt) - - def mint_tokens( - self, - token_uid: TokenUid, - amount: int, - *, - fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) - ) -> None: - """Mint tokens and add them to the balance of this nano contract.""" - self.__runner.syscall_mint_tokens(token_uid=token_uid, amount=amount, fee_payment_token=fee_payment_token) - - def melt_tokens( - self, - token_uid: TokenUid, - amount: int, - *, - fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) - ) -> None: - """Melt tokens by removing them from the balance of this nano contract.""" - self.__runner.syscall_melt_tokens(token_uid=token_uid, amount=amount, fee_payment_token=fee_payment_token) - - def emit_event(self, data: bytes) -> None: - """Emit a custom event from a Nano Contract.""" - self.__runner.syscall_emit_event(data) - - def create_deposit_token( - self, - *, - token_name: str, - token_symbol: str, - amount: int, - mint_authority: bool = True, - melt_authority: bool = True, - salt: bytes = b'', - ) -> TokenUid: - """Create a new deposit-based token.""" - return self.__runner.syscall_create_child_deposit_token( - salt=salt, - token_name=token_name, - token_symbol=token_symbol, - amount=amount, - mint_authority=mint_authority, - melt_authority=melt_authority, - ) - - def create_fee_token( - self, - *, - token_name: str, - token_symbol: str, - amount: int, - mint_authority: bool = True, - melt_authority: bool = True, - salt: bytes = b'', - fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) - ) -> TokenUid: - """Create a new fee-based token.""" - return self.__runner.syscall_create_child_fee_token( - salt=salt, - token_name=token_name, - token_symbol=token_symbol, - amount=amount, - mint_authority=mint_authority, - melt_authority=melt_authority, - fee_payment_token=fee_payment_token - ) - - def change_blueprint(self, blueprint_id: BlueprintId) -> None: - """Change the blueprint of this contract.""" - self.__runner.syscall_change_blueprint(blueprint_id) - - def get_contract( - self, - contract_id: ContractId, - *, - blueprint_id: BlueprintId | Collection[BlueprintId] | None, - ) -> ContractAccessor: - """ - Get a contract accessor for the given contract ID. Use this for interacting with another contract. - - Args: - contract_id: the ID of the contract. - blueprint_id: the expected blueprint ID of the contract, or a collection of accepted blueprints, - or None if any blueprint is accepted. - - """ - from hathor.nanocontracts.contract_accessor import ContractAccessor - return ContractAccessor(runner=self.__runner, contract_id=contract_id, blueprint_id=blueprint_id) - - def get_proxy(self, blueprint_id: BlueprintId) -> ProxyAccessor: - """ - Get a proxy accessor for the given blueprint ID. Use this for interacting with another blueprint via a proxy. - """ - from hathor.nanocontracts.proxy_accessor import ProxyAccessor - return ProxyAccessor(runner=self.__runner, blueprint_id=blueprint_id) - - def setup_new_contract( - self, - blueprint_id: BlueprintId, - *actions: NCAction, - fees: Sequence[NCFee] | None = None, - salt: bytes, - ) -> InitializeMethodAccessor: - """Setup creation of a new contract.""" - from hathor.nanocontracts.initialize_method_accessor import InitializeMethodAccessor - self.__runner.forbid_call_on_view('setup_new_contract') - return InitializeMethodAccessor( - runner=self.__runner, - blueprint_id=blueprint_id, - salt=salt, - actions=actions, - fees=fees or (), - ) - - def get_settings(self) -> NanoSettings: - """ - Return the settings for the current Nano runtime. - Settings are not constant, they may be changed over time. - """ - return self.__runner.syscall_get_nano_settings() +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.blueprint_env import * # noqa: F401,F403 +from hathorlib.nanocontracts.blueprint_env import BlueprintEnvironment, NCAttrCache # noqa: F401 diff --git a/hathor/nanocontracts/context.py b/hathor/nanocontracts/context.py index 7ed05df81..69921328d 100644 --- a/hathor/nanocontracts/context.py +++ b/hathor/nanocontracts/context.py @@ -14,16 +14,17 @@ from __future__ import annotations -from collections import defaultdict -from itertools import chain from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Sequence, assert_never, final +from typing import TYPE_CHECKING, Sequence -from hathor.crypto.util import get_address_b58_from_bytes -from hathor.nanocontracts.exception import NCFail, NCInvalidContext -from hathor.nanocontracts.types import Address, CallerId, ContractId, NCAction, TokenUid -from hathor.nanocontracts.vertex_data import BlockData, VertexData +from hathor.nanocontracts.exception import NCInvalidContext +from hathor.nanocontracts.types import CallerId, NCAction, TokenUid +from hathor.nanocontracts.vertex_data import create_block_data_from_block, create_vertex_data_from_vertex from hathor.transaction.exceptions import TxValidationError +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.context import * # noqa: F401,F403 +from hathorlib.nanocontracts.context import Context # noqa: F401 +from hathorlib.nanocontracts.vertex_data import BlockData if TYPE_CHECKING: from hathor.transaction import Vertex @@ -31,165 +32,37 @@ _EMPTY_MAP: MappingProxyType[TokenUid, tuple[NCAction, ...]] = MappingProxyType({}) -@final -class Context: - """Context passed to a method call. An empty list of actions means the - method is being called with no deposits and withdrawals. - - Deposits and withdrawals are grouped by token. Note that it is impossible - to have both a deposit and a withdrawal for the same token. - """ - __slots__ = ('__actions', '__caller_id', '__vertex', '__block', '__all_actions__') - __caller_id: CallerId - __vertex: VertexData - __block: BlockData | None - __actions: MappingProxyType[TokenUid, tuple[NCAction, ...]] - - @staticmethod - def __group_actions__(actions: Sequence[NCAction]) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]: - actions_map: defaultdict[TokenUid, tuple[NCAction, ...]] = defaultdict(tuple) - for action in actions: - actions_map[action.token_uid] = (*actions_map[action.token_uid], action) - return MappingProxyType(actions_map) - - @classmethod - def create_from_vertex( - cls, - *, - caller_id: CallerId, - vertex: Vertex, - actions: Sequence[NCAction], - ) -> Context: - # Dict of action where the key is the token_uid. - # If empty, it is a method call without any actions. - actions_map: MappingProxyType[TokenUid, tuple[NCAction, ...]] - if not actions: - actions_map = _EMPTY_MAP - else: - from hathor.verification.nano_header_verifier import NanoHeaderVerifier - try: - NanoHeaderVerifier.verify_action_list(actions) - except TxValidationError as e: - raise NCInvalidContext('invalid nano context') from e - - actions_map = cls.__group_actions__(actions) - - vertex_data = VertexData.create_from_vertex(vertex) - vertex_meta = vertex.get_metadata() - - block_data: BlockData | None = None - if vertex_meta.first_block is not None: - # The context is also created when getting the tx's `to_json` for example, - # and in this case it might not be confirmed yet. - assert vertex.storage is not None - block = vertex.storage.get_block(vertex_meta.first_block) - block_data = BlockData.create_from_block(block) - - return Context( - caller_id=caller_id, - vertex_data=vertex_data, - block_data=block_data, - actions=actions_map, - ) - - def __init__( - self, - *, - caller_id: CallerId, - vertex_data: VertexData, - block_data: BlockData | None, - actions: MappingProxyType[TokenUid, tuple[NCAction, ...]], - ) -> None: - # Dict of action where the key is the token_uid. - # If empty, it is a method call without any actions. - self.__actions = actions - - self.__all_actions__: tuple[NCAction, ...] = tuple(chain(*self.__actions.values())) - - # Vertex calling the method. - self.__vertex = vertex_data - - # Block executing the vertex. - self.__block = block_data - - # Address calling the method. - self.__caller_id = caller_id - - @property - def vertex(self) -> VertexData: - return self.__vertex - - @property - def block(self) -> BlockData: - assert self.__block is not None - return self.__block - - @property - def caller_id(self) -> CallerId: - """Get the caller ID which can be either an Address or a ContractId.""" - return self.__caller_id - - def get_caller_address(self) -> Address | None: - """Get the caller address if the caller is an address, None if it's a contract.""" - match self.caller_id: - case Address(): - return self.caller_id - case ContractId(): - return None - case _: # pragma: no cover - assert_never(self.caller_id) - - def get_caller_contract_id(self) -> ContractId | None: - """Get the caller contract ID if the caller is a contract, None if it's an address.""" - match self.caller_id: - case Address(): - return None - case ContractId(): - return self.caller_id - case _: # pragma: no cover - assert_never(self.caller_id) - - @property - def actions(self) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]: - """Get a mapping of actions per token.""" - return self.__actions - - @property - def actions_list(self) -> Sequence[NCAction]: - """Get a list of all actions.""" - return tuple(self.__all_actions__) - - def get_single_action(self, token_uid: TokenUid) -> NCAction: - """Get exactly one action for the provided token, and fail otherwise.""" - actions = self.actions.get(token_uid) - if actions is None or len(actions) != 1: - raise NCFail(f'expected exactly 1 action for token {token_uid.hex()}') - return actions[0] - - def copy(self) -> Context: - """Return a copy of the context.""" - return Context( - caller_id=self.caller_id, - vertex_data=self.vertex, - block_data=self.block, # We only copy during execution, so we know the block must not be `None`. - actions=self.actions, - ) - - def to_json(self) -> dict[str, Any]: - """Return a JSON representation of the context.""" - caller_id: str - match self.caller_id: - case Address(): - caller_id = get_address_b58_from_bytes(self.caller_id) - case ContractId(): - caller_id = self.caller_id.hex() - case _: # pragma: no cover - assert_never(self.caller_id) - - return { - 'actions': [action.to_json() for action in self.__all_actions__], - 'caller_id': caller_id, - 'timestamp': self.__block.timestamp if self.__block is not None else None, - # XXX: Deprecated attribute - 'address': caller_id, - } +def create_context_from_vertex( + *, + caller_id: CallerId, + vertex: Vertex, + actions: Sequence[NCAction], +) -> Context: + """Create a Context from a transaction vertex. This is a hathor-specific factory function.""" + actions_map: MappingProxyType[TokenUid, tuple[NCAction, ...]] + if not actions: + actions_map = _EMPTY_MAP + else: + from hathor.verification.nano_header_verifier import NanoHeaderVerifier + try: + NanoHeaderVerifier.verify_action_list(actions) + except TxValidationError as e: + raise NCInvalidContext('invalid nano context') from e + + actions_map = Context.__group_actions__(actions) + + vertex_data = create_vertex_data_from_vertex(vertex) + vertex_meta = vertex.get_metadata() + + block_data: BlockData | None = None + if vertex_meta.first_block is not None: + assert vertex.storage is not None + block = vertex.storage.get_block(vertex_meta.first_block) + block_data = create_block_data_from_block(block) + + return Context( + caller_id=caller_id, + vertex_data=vertex_data, + block_data=block_data, + actions=actions_map, + ) diff --git a/hathor/nanocontracts/contract_accessor.py b/hathor/nanocontracts/contract_accessor.py index d69fe9c25..eeaaa2468 100644 --- a/hathor/nanocontracts/contract_accessor.py +++ b/hathor/nanocontracts/contract_accessor.py @@ -12,366 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Collection, Sequence, final - -from hathor.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ -from hathor.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, NCFee, TokenUid - -if TYPE_CHECKING: - from hathor.nanocontracts import Runner - - -@final -class ContractAccessor(FauxImmutable): - """ - This class represents a "contract instance", or a contract accessor, during a blueprint method execution. - Calling custom blueprint methods on this class will forward the call to the actual wrapped blueprint via syscalls. - """ - __slots__ = ('__runner', '__contract_id', '__blueprint_ids') - - def __init__( - self, - *, - runner: Runner, - contract_id: ContractId, - blueprint_id: BlueprintId | Collection[BlueprintId] | None, - ) -> None: - self.__runner: Runner - self.__contract_id: ContractId - self.__blueprint_ids: frozenset[BlueprintId] | None - - blueprint_ids: frozenset[BlueprintId] | None - match blueprint_id: - case None: - blueprint_ids = None - case bytes(): - blueprint_ids = frozenset({blueprint_id}) - case _: - blueprint_ids = frozenset(blueprint_id) - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__contract_id', contract_id) - __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) - - def get_contract_id(self) -> ContractId: - """Return the contract id of this nano contract.""" - return self.__contract_id - - def get_blueprint_id(self) -> BlueprintId: - """Return the blueprint id of this nano contract.""" - return self.__runner.get_blueprint_id(self.__contract_id) - - def get_current_balance(self, token_uid: TokenUid | None = None) -> Amount: - """ - Return the current balance for a given token, which includes all actions and changes in the current call. - - For instance, if a contract has 50 HTR and the call is requesting to withdraw 3 HTR, - then this method will return 47 HTR. - """ - balance = self.__runner.get_current_balance(self.__contract_id, token_uid) - return Amount(balance.value) - - def can_mint(self, token_uid: TokenUid) -> bool: - """ - Return whether a given token can currently be minted, - which includes all actions and changes in the current call. - - For instance, if a contract has a mint authority and a call is revoking it, - then this method will return `False`. - """ - balance = self.__runner.get_current_balance(self.__contract_id, token_uid) - return balance.can_mint - - def can_melt(self, token_uid: TokenUid) -> bool: - """ - Return whether a given token can currently be melted, - which includes all actions and changes in the current call. - - For instance, if a contract has a melt authority and a transaction is revoking it, - then this method will return `False`. - """ - balance = self.__runner.get_current_balance(self.__contract_id, token_uid) - return balance.can_melt - - def view(self) -> Any: - """Prepare a call to a view method.""" - return PreparedViewCall( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - ) - - def public(self, *actions: NCAction, fees: Sequence[NCFee] | None = None, forbid_fallback: bool = False) -> Any: - """Prepare a call to a public method.""" - return PreparedPublicCall( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - actions=actions, - fees=fees or (), - forbid_fallback=forbid_fallback, - ) - - def get_view_method(self, method_name: str) -> ViewMethodAccessor: - """Get a view method.""" - return ViewMethodAccessor( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - method_name=method_name, - ) - - def get_public_method( - self, - method_name: str, - *actions: NCAction, - fees: Sequence[NCFee] | None = None, - forbid_fallback: bool = False, - ) -> PublicMethodAccessor: - """Get a public method.""" - return PublicMethodAccessor( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - method_name=method_name, - actions=actions, - fees=fees or (), - forbid_fallback=forbid_fallback, - ) - - -@final -class PreparedViewCall(FauxImmutable): - __slots__ = ('__runner', '__contract_id', '__blueprint_ids') - __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ - - def __init__( - self, - *, - runner: Runner, - contract_id: ContractId, - blueprint_ids: frozenset[BlueprintId] | None, - ) -> None: - self.__runner: Runner - self.__contract_id: ContractId - self.__blueprint_ids: frozenset[BlueprintId] | None - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__contract_id', contract_id) - __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) - - def __getattr__(self, method_name: str) -> ViewMethodAccessor: - return ViewMethodAccessor( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - method_name=method_name, - ) - - -@final -class PreparedPublicCall(FauxImmutable): - __slots__ = ( - '__runner', - '__contract_id', - '__blueprint_ids', - '__actions', - '__fees', - '__forbid_fallback', - '__is_dirty', - ) - __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ - - def __init__( - self, - *, - runner: Runner, - contract_id: ContractId, - blueprint_ids: frozenset[BlueprintId] | None, - actions: Sequence[NCAction], - fees: Sequence[NCFee], - forbid_fallback: bool, - ) -> None: - self.__runner: Runner - self.__contract_id: ContractId - self.__blueprint_ids: frozenset[BlueprintId] | None - self.__actions: Sequence[NCAction] - self.__fees: Sequence[NCFee] - self.__forbid_fallback: bool - self.__is_dirty: bool - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__contract_id', contract_id) - __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) - __set_faux_immutable__(self, '__actions', actions) - __set_faux_immutable__(self, '__fees', fees) - __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) - __set_faux_immutable__(self, '__is_dirty', False) - - def __getattr__(self, method_name: str) -> PublicMethodAccessor: - from hathor.nanocontracts import NCFail - if self.__is_dirty: - raise NCFail( - f'prepared public method for contract `{self.__contract_id.hex()}` was already used, ' - f'you must use `public` on the contract to call it again' - ) - - __set_faux_immutable__(self, '__is_dirty', True) - - return PublicMethodAccessor( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - method_name=method_name, - actions=self.__actions, - fees=self.__fees, - forbid_fallback=self.__forbid_fallback, - ) - - -@final -class ViewMethodAccessor(FauxImmutable): - """ - This class represents a "view method", or a view method accessor, during a blueprint method execution. - It's a callable that will forward the call to the actual wrapped blueprint via syscall. - It may be used multiple times to call the same method with different arguments. - """ - __slots__ = ('__runner', '__contract_id', '__blueprint_ids', '__method_name') - - def __init__( - self, - *, - runner: Runner, - contract_id: ContractId, - blueprint_ids: frozenset[BlueprintId] | None, - method_name: str, - ) -> None: - self.__runner: Runner - self.__contract_id: ContractId - self.__blueprint_ids: frozenset[BlueprintId] | None - self.__method_name: str - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__contract_id', contract_id) - __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) - __set_faux_immutable__(self, '__method_name', method_name) - - def call(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments. This is just an alias for calling the object directly.""" - return self(*args, **kwargs) - - def __call__(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments.""" - validate_blueprint_id( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - ) - - return self.__runner.syscall_call_another_contract_view_method( - contract_id=self.__contract_id, - method_name=self.__method_name, - args=args, - kwargs=kwargs, - ) - - -@final -class PublicMethodAccessor(FauxImmutable): - """ - This class represents a "public method", or a public method accessor, during a blueprint method execution. - It's a callable that will forward the call to the actual wrapped blueprint via syscall. - It can only be used once because it consumes the provided actions after a single use. - """ - __slots__ = ( - '__runner', - '__contract_id', - '__blueprint_ids', - '__method_name', - '__actions', - '__fees', - '__forbid_fallback', - '__is_dirty', - ) - - def __init__( - self, - *, - runner: Runner, - contract_id: ContractId, - blueprint_ids: frozenset[BlueprintId] | None, - method_name: str, - actions: Sequence[NCAction], - fees: Sequence[NCFee], - forbid_fallback: bool, - ) -> None: - self.__runner: Runner - self.__contract_id: ContractId - self.__blueprint_ids: frozenset[BlueprintId] | None - self.__method_name: str - self.__actions: Sequence[NCAction] - self.__fees: Sequence[NCFee] - self.__forbid_fallback: bool - self.__is_dirty: bool - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__contract_id', contract_id) - __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) - __set_faux_immutable__(self, '__method_name', method_name) - __set_faux_immutable__(self, '__actions', actions) - __set_faux_immutable__(self, '__fees', fees) - __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) - __set_faux_immutable__(self, '__is_dirty', False) - - def call(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments. This is just an alias for calling the object directly.""" - return self(*args, **kwargs) - - def __call__(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments.""" - from hathor.nanocontracts import NCFail - if self.__is_dirty: - raise NCFail( - f'accessor for public method `{self.__method_name}` was already used, ' - f'you must use `public`/`public_method` on the contract to call it again' - ) - - __set_faux_immutable__(self, '__is_dirty', True) - - validate_blueprint_id( - runner=self.__runner, - contract_id=self.__contract_id, - blueprint_ids=self.__blueprint_ids, - ) - - return self.__runner.syscall_call_another_contract_public_method( - contract_id=self.__contract_id, - method_name=self.__method_name, - actions=self.__actions, - fees=self.__fees, - args=args, - kwargs=kwargs, - forbid_fallback=self.__forbid_fallback, - ) - - -def validate_blueprint_id( - *, - runner: Runner, - contract_id: ContractId, - blueprint_ids: frozenset[BlueprintId] | None, -) -> None: - """Check whether the blueprint id of a contract matches the expected id(s), raise an exception otherwise.""" - if blueprint_ids is None: - return - - blueprint_id = runner.get_blueprint_id(contract_id) - if blueprint_id not in blueprint_ids: - from hathor.nanocontracts import NCFail - expected = tuple(sorted(bp.hex() for bp in blueprint_ids)) - raise NCFail( - f'expected blueprint to be one of `{expected}`, ' - f'got `{blueprint_id.hex()}` for contract `{contract_id.hex()}`' - ) +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.contract_accessor import * # noqa: F401,F403 +from hathorlib.nanocontracts.contract_accessor import ( # noqa: F401 + ContractAccessor, + PreparedPublicCall, + PreparedViewCall, + PublicMethodAccessor, + ViewMethodAccessor, +) diff --git a/hathor/nanocontracts/initialize_method_accessor.py b/hathor/nanocontracts/initialize_method_accessor.py index 48edd1fb7..edd09e590 100644 --- a/hathor/nanocontracts/initialize_method_accessor.py +++ b/hathor/nanocontracts/initialize_method_accessor.py @@ -12,71 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from typing import Any, Sequence, final - -from hathor import BlueprintId, ContractId, NCAction, NCFee -from hathor.nanocontracts import Runner -from hathor.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ - - -@final -class InitializeMethodAccessor(FauxImmutable): - """ - This class represents an "initialize method", or an initialize method accessor, during a blueprint method - execution. - Calling `initialize()` on it will forward the call to the actual wrapped blueprint via syscall. - It can only be used once because it consumes the provided actions after a single use. - """ - __slots__ = ( - '__runner', - '__blueprint_id', - '__salt', - '__actions', - '__fees', - '__is_dirty', - ) - - def __init__( - self, - *, - runner: Runner, - blueprint_id: BlueprintId, - salt: bytes, - actions: Sequence[NCAction], - fees: Sequence[NCFee], - ) -> None: - self.__runner: Runner - self.__blueprint_id: BlueprintId - self.__salt: bytes - self.__actions: Sequence[NCAction] - self.__fees: Sequence[NCFee] - self.__is_dirty: bool - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__blueprint_id', blueprint_id) - __set_faux_immutable__(self, '__salt', salt) - __set_faux_immutable__(self, '__actions', actions) - __set_faux_immutable__(self, '__fees', fees) - __set_faux_immutable__(self, '__is_dirty', False) - - def initialize(self, *args: Any, **kwargs: Any) -> tuple[ContractId, object]: - """Initialize a new contract.""" - from hathor.nanocontracts import NCFail - if self.__is_dirty: - raise NCFail( - 'accessor for initialize method was already used, ' - 'you must use `setup_new_contract` to call it again' - ) - - __set_faux_immutable__(self, '__is_dirty', True) - - return self.__runner.syscall_create_another_contract( - blueprint_id=self.__blueprint_id, - salt=self.__salt, - actions=self.__actions, - fees=self.__fees, - args=args, - kwargs=kwargs, - ) +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.initialize_method_accessor import * # noqa: F401,F403 +from hathorlib.nanocontracts.initialize_method_accessor import InitializeMethodAccessor # noqa: F401 diff --git a/hathor/nanocontracts/method.py b/hathor/nanocontracts/method.py index c403fc6ee..618acd80b 100644 --- a/hathor/nanocontracts/method.py +++ b/hathor/nanocontracts/method.py @@ -12,342 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from collections.abc import Callable, Iterable -from inspect import Parameter, Signature, _empty as EMPTY, signature -from types import FunctionType, MethodType -from typing import Any, TypeVar - -from typing_extensions import Self, assert_never, override - -from hathor.nanocontracts import Context -from hathor.nanocontracts.exception import NCFail, NCSerializationArgTooLong, NCSerializationError -from hathor.nanocontracts.nc_types import ( - NCType, - VarUint32NCType, - make_nc_type_for_arg_type, - make_nc_type_for_return_type, -) -from hathor.nanocontracts.utils import is_nc_public_method -from hathor.serialization import Deserializer, SerializationError, Serializer -from hathor.serialization.adapters import MaxBytesExceededError - -_num_args_nc_type = VarUint32NCType() -T = TypeVar('T') - -MAX_BYTES_SERIALIZED_ARG: int = 1000 - - -def _deserialize_map_exception(nc_type: NCType[T], data: bytes) -> T: - """ Internal handy method to deserialize `bytes` to `T` while mapping the exceptions.""" - try: - deserializer = Deserializer.build_bytes_deserializer(data) - value = nc_type.deserialize(deserializer) - deserializer.finalize() - return value - except MaxBytesExceededError as e: - raise NCSerializationArgTooLong from e - except SerializationError as e: - raise NCSerializationError from e - except NCFail: - raise - except Exception as e: - raise NCFail from e - - -def _serialize_map_exception(nc_type: NCType[T], value: T) -> bytes: - """ Internal handy method to serialize `T` to `bytes` while mapping the exceptions.""" - try: - serializer = Serializer.build_bytes_serializer() - nc_type.serialize(serializer, value) - return bytes(serializer.finalize()) - except MaxBytesExceededError as e: - raise NCSerializationArgTooLong from e - except SerializationError as e: - raise NCSerializationError from e - except NCFail: - raise - except Exception as e: - raise NCFail from e - - -class _ArgsNCType(NCType): - """ Inner implementation of a callable "args" using the NCType model. - """ - - _args: tuple[NCType, ...] - _max_bytes: int - - def __init__(self, args_nc_types: Iterable[NCType], max_bytes: int) -> None: - self._args = tuple(args_nc_types) - self._max_bytes = max_bytes - - @override - def _check_value(self, value: Any, /, *, deep: bool) -> None: - # XXX: we take either a tuple or a list as input - if not isinstance(value, (tuple, list)): - raise TypeError('expected tuple or list') - if len(value) > len(self._args): - raise TypeError('too many arguments') - if deep: - for i, arg_nc_type in zip(value, self._args): - arg_nc_type._check_value(i, deep=deep) - - @override - def _serialize(self, serializer: Serializer, args: tuple[Any, ...] | list[Any], /) -> None: - with serializer.with_max_bytes(self._max_bytes) as serializer: - num_args = len(args) - if num_args > len(self._args): - raise TypeError('too many arguments') - # XXX: default arguments are currently not supported, thus we reject too few arguments too - if num_args < len(self._args): - raise TypeError('too few arguments') - _num_args_nc_type.serialize(serializer, num_args) - for value, arg in zip(self._args, args): - value.serialize(serializer, arg) - - @override - def _deserialize(self, deserializer: Deserializer, /) -> tuple[Any, ...]: - with deserializer.with_max_bytes(self._max_bytes) as deserializer: - # TODO: normalize exceptions - num_args = _num_args_nc_type.deserialize(deserializer) - if num_args > len(self._args): - raise TypeError('too many arguments') - # XXX: default arguments are currently not supported, thus we reject too few arguments too - if num_args < len(self._args): - raise TypeError('too few arguments') - args = [] - for value, _ in zip(self._args, range(num_args)): - args.append(value.deserialize(deserializer)) - return tuple(args) - - @override - def _json_to_value(self, json_value: NCType.Json, /) -> tuple[Any, ...]: - if not isinstance(json_value, list): - raise ValueError('expected list') - return tuple(v.json_to_value(i) for (i, v) in zip(json_value, self._args)) - - @override - def _value_to_json(self, value: tuple[Any, ...], /) -> NCType.Json: - return [v.value_to_json(i) for (i, v) in zip(value, self._args)] - - -class ArgsOnly: - """ This class is used to parse only arguments of a call, when all that is provided is a list of argument types. - - Its primary use is for implementing `NCRawArgs.try_parse_as`. - """ - args: _ArgsNCType - - def __init__(self, args_nc_type: _ArgsNCType) -> None: - """Do not build directly, use `ArgsOnly.from_arg_types`""" - self.args = args_nc_type - - @classmethod - def from_arg_types(cls, arg_types: tuple[type, ...]) -> Self: - args_nc_types: list[NCType] = [] - for arg_type in arg_types: - args_nc_types.append(make_nc_type_for_arg_type(arg_type)) - - return cls(_ArgsNCType(args_nc_types, max_bytes=MAX_BYTES_SERIALIZED_ARG)) - - def serialize_args_bytes(self, args: tuple[Any, ...] | list[Any]) -> bytes: - """ Shortcut to serialize args directly to a bytes instead of using a serializer. - """ - return _serialize_map_exception(self.args, args) - - def deserialize_args_bytes(self, data: bytes) -> tuple[Any, ...]: - """ Shortcut to deserialize args directly from bytes instead of using a deserializer. - """ - return _deserialize_map_exception(self.args, data) - - -class ReturnOnly: - """ - This class is used to parse only the return of a method. - - Its primary use is for validating the fallback method. - """ - return_nc_type: NCType - - def __init__(self, return_nc_type: NCType) -> None: - self.return_nc_type = return_nc_type - - @classmethod - def from_callable(cls, method: Callable) -> Self: - method_signature = _get_method_signature(method) - nc_type = make_nc_type_for_return_type(method_signature.return_annotation) - return cls(nc_type) - - def serialize_return_bytes(self, return_value: Any) -> bytes: - """Shortcut to serialize a return value directly to bytes instead of using a serializer.""" - return _serialize_map_exception(self.return_nc_type, return_value) - - def deserialize_return_bytes(self, data: bytes) -> Any: - """Shortcut to deserialize a return value directly from bytes instead of using a deserializer.""" - return _deserialize_map_exception(self.return_nc_type, data) - - -# XXX: currently the relationship between the method's signature's types and the `NCType`s type's cannot be described -# with Python/mypy's typing system -class Method: - """ This class abstracts a method's type signature in relation similarly to how NCType and Field abstract a loose - "value" or a classe's "field". - - This abstraction is used to (de)serialize the arguments of a method call, and (de)serialize the result of a method - call. It may also be used to transmit values when a nano-method calls another nano-method. - - For arguments, `make_nc_type_for_arg_type` is used, which tends to preserve original types as much as possible, but - for return types `make_nc_type_for_return_type` is used, which supports `None`. - """ - name: str - arg_names: tuple[str, ...] - args: _ArgsNCType - return_: NCType - - def __init__( - self, - *, - name: str, - arg_names: Iterable[str], - args_nc_type: _ArgsNCType, - return_nc_type: NCType, - ) -> None: - """Do not build directly, use `Method.from_callable`""" - self.name = name - self.arg_names = tuple(arg_names) - self.args = args_nc_type - self.return_ = return_nc_type - - @classmethod - def from_callable(cls, method: Callable) -> Self: - method_signature = _get_method_signature(method) - - # XXX: bound methods don't have the self argument - is_bound_method: bool - - match method: - case MethodType(): - is_bound_method = True - case FunctionType(): - is_bound_method = False - case _: - raise TypeError(f'{method!r} is neither a function or a bound method') - - for param in method_signature.parameters.values(): - if isinstance(param.annotation, str): - raise TypeError('string annotations (including `from __future__ import annotations`), ' - 'are not supported') - - arg_names = [] - args_nc_types = [] - iter_params = iter(method_signature.parameters.values()) - - # XXX: bound methods don't expose the self argument - if not is_bound_method: - try: - self_param = next(iter_params) - except StopIteration: - raise TypeError('missing self argument') - if self_param.name != 'self': - # XXX: self_param is not technically required to be named 'self', it can be named anything, but it - # should at least be a warning because it's possible the author forgot the 'self' argument - raise TypeError('first argument should be self') - - if is_nc_public_method(method): - try: - ctx_param = next(iter_params) - except StopIteration: - raise TypeError('missing ctx argument') - if ctx_param.annotation is not Context: - raise TypeError('context argument must be annotated as `ctx: Context`') - - for param in iter_params: - match param.kind: - case Parameter.POSITIONAL_ONLY: # these are arguments before / - # we accept these - pass - case Parameter.POSITIONAL_OR_KEYWORD: # there are normal arguments - # we accept these - pass - case Parameter.VAR_POSITIONAL: # these are *args kind of arguments - # XXX: we can technically support this, since these can be annotated - raise TypeError('variable *args arguments are not supported') - case Parameter.KEYWORD_ONLY: # these are arguments after * or *args, which are keyword-only - raise TypeError('keyword-only arguments are not supported') - case Parameter.VAR_KEYWORD: # these are **kwargs arguments - raise TypeError('variable **kwargs arguments are not supported') - case _ as impossible_kind: # no other type of argument exist - assert_never(impossible_kind) - # XXX: this can (and probably will) be implemented in the future - if param.default is not EMPTY: - raise TypeError('default values are not supported') - arg_names.append(param.name) - args_nc_types.append(make_nc_type_for_arg_type(param.annotation)) - - return cls( - name=method.__name__, - arg_names=arg_names, - args_nc_type=_ArgsNCType(args_nc_types, max_bytes=MAX_BYTES_SERIALIZED_ARG), - return_nc_type=make_nc_type_for_return_type(method_signature.return_annotation), - ) - - def serialize_args_bytes(self, args: tuple[Any, ...] | list[Any], kwargs: dict[str, Any] | None = None) -> bytes: - """ Shortcut to serialize args directly to a bytes instead of using a serializer. - """ - if len(args) > len(self.arg_names): - raise NCFail('too many arguments') - - merged: dict[str, Any] = {} - for index, arg in enumerate(args): - name = self.arg_names[index] - merged[name] = arg - - kwargs = kwargs or {} - for name, arg in kwargs.items(): - if name not in self.arg_names: - raise NCFail(f"{self.name}() got an unexpected keyword argument '{name}'") - if name in merged: - raise NCFail(f"{self.name}() got multiple values for argument '{name}'") - merged[name] = arg - - ordered_args = [] - for name in self.arg_names: - if name not in merged: - raise NCFail(f"{self.name}() missing required argument: '{name}'") - ordered_args.append(merged[name]) - - return _serialize_map_exception(self.args, tuple(ordered_args)) - - def deserialize_args_bytes(self, data: bytes) -> tuple[Any, ...]: - """ Shortcut to deserialize args directly from bytes instead of using a deserializer. - """ - return _deserialize_map_exception(self.args, data) - - def serialize_return_bytes(self, return_value: Any) -> bytes: - """ Shortcut to serialize a return value directly to a bytes instead of using a serializer. - """ - return _serialize_map_exception(self.return_, return_value) - - def deserialize_return_bytes(self, data: bytes) -> Any: - """ Shortcut to deserialize a return value directly from bytes instead of using a deserializer. - """ - return _deserialize_map_exception(self.return_, data) - - -def _get_method_signature(method: Callable) -> Signature: - if not callable(method): - raise TypeError(f'{method!r} is not a callable object') - - # XXX: explicit all arguments to explain the choices, even if default - return signature( - method, - follow_wrapped=True, # we're interested in the implementation's signature, so we follow wrappers - globals=None, # don't expose any global - locals=None, # don't expose any local - # XXX: do not evaluate strings, this means `from __future__ import annotations` is not supported, ideally - # we should support it because it's very convenient, but it must be done with care, otherwise we could - # run into cases that do `def foo(self, i: '2**100**100') -> None`, which is syntactically legal - eval_str=False, - ) +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.method import * # noqa: F401,F403 +from hathorlib.nanocontracts.method import ArgsOnly, Method, ReturnOnly # noqa: F401 diff --git a/hathor/nanocontracts/nano_runtime_version.py b/hathor/nanocontracts/nano_runtime_version.py index 5c68f7025..fa192ad86 100644 --- a/hathor/nanocontracts/nano_runtime_version.py +++ b/hathor/nanocontracts/nano_runtime_version.py @@ -12,19 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import IntEnum - - -class NanoRuntimeVersion(IntEnum): - """ - The runtime version of Nano Contracts. - It must be updated via Feature Activation and can be used to add new syscalls, for example. - - V1: - - Initial version - - V2: - - Added `get_settings` syscall - """ - V1 = 1 - V2 = 2 +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.nano_runtime_version import * # noqa: F401,F403 +from hathorlib.nanocontracts.nano_runtime_version import NanoRuntimeVersion # noqa: F401 diff --git a/hathor/nanocontracts/nano_settings.py b/hathor/nanocontracts/nano_settings.py index bcd76918c..80072573b 100644 --- a/hathor/nanocontracts/nano_settings.py +++ b/hathor/nanocontracts/nano_settings.py @@ -12,13 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import dataclass - - -@dataclass(slots=True, frozen=True, kw_only=True) -class NanoSettings: - """ - This dataclass contains information about the settings used by the current Nano runtime. - It is returned by the `get_settings` syscall. Note that settings are not constant, they may change over time. - """ - fee_per_output: int +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.nano_settings import * # noqa: F401,F403 +from hathorlib.nanocontracts.nano_settings import NanoSettings # noqa: F401 diff --git a/hathor/nanocontracts/proxy_accessor.py b/hathor/nanocontracts/proxy_accessor.py index 2317fc05d..d22c1bbee 100644 --- a/hathor/nanocontracts/proxy_accessor.py +++ b/hathor/nanocontracts/proxy_accessor.py @@ -12,258 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from typing import Any, Sequence, final - -from hathor import BlueprintId, NCAction, NCArgs, NCFee, NCParsedArgs -from hathor.nanocontracts import Runner -from hathor.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ - - -@final -class ProxyAccessor(FauxImmutable): - """ - This class represents a "proxy instance", or a proxy accessor, during a blueprint method execution. - Calling custom blueprint methods on this class will forward the call to the actual wrapped blueprint via syscalls. - """ - __slots__ = ('__runner', '__blueprint_id') - - def __init__( - self, - *, - runner: Runner, - blueprint_id: BlueprintId, - ) -> None: - self.__runner: Runner - self.__blueprint_id: BlueprintId - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__blueprint_id', blueprint_id) - - def get_blueprint_id(self) -> BlueprintId: - """Return the blueprint id of this proxy.""" - return self.__blueprint_id - - def view(self) -> Any: - """Prepare a call to a proxy view method.""" - return PreparedProxyViewCall( - runner=self.__runner, - blueprint_id=self.__blueprint_id, - ) - - def public(self, *actions: NCAction, fees: Sequence[NCFee] | None = None, forbid_fallback: bool = False) -> Any: - """Prepare a proxy call to a public method.""" - return PreparedProxyPublicCall( - runner=self.__runner, - blueprint_id=self.__blueprint_id, - actions=actions, - fees=fees or (), - forbid_fallback=forbid_fallback, - ) - - def get_view_method(self, method_name: str) -> ProxyViewMethodAccessor: - """Get a proxy view method.""" - return ProxyViewMethodAccessor( - runner=self.__runner, - blueprint_id=self.__blueprint_id, - method_name=method_name, - ) - - def get_public_method( - self, - method_name: str, - *actions: NCAction, - fees: Sequence[NCFee] | None = None, - forbid_fallback: bool = False, - ) -> ProxyPublicMethodAccessor: - """Get a proxy public method.""" - return ProxyPublicMethodAccessor( - runner=self.__runner, - blueprint_id=self.__blueprint_id, - method_name=method_name, - actions=actions, - fees=fees or (), - forbid_fallback=forbid_fallback, - ) - - -@final -class PreparedProxyViewCall(FauxImmutable): - __slots__ = ('__runner', '__blueprint_id') - __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ - - def __init__(self, *, runner: Runner, blueprint_id: BlueprintId) -> None: - self.__runner: Runner - self.__blueprint_id: BlueprintId - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__blueprint_id', blueprint_id) - - def __getattr__(self, method_name: str) -> ProxyViewMethodAccessor: - return ProxyViewMethodAccessor( - runner=self.__runner, - blueprint_id=self.__blueprint_id, - method_name=method_name, - ) - - -@final -class PreparedProxyPublicCall(FauxImmutable): - __slots__ = ( - '__runner', - '__blueprint_id', - '__actions', - '__fees', - '__forbid_fallback', - '__is_dirty', - ) - __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ - - def __init__( - self, - *, - runner: Runner, - blueprint_id: BlueprintId, - actions: Sequence[NCAction], - fees: Sequence[NCFee], - forbid_fallback: bool, - ) -> None: - self.__runner: Runner - self.__blueprint_id: BlueprintId - self.__actions: Sequence[NCAction] - self.__fees: Sequence[NCFee] - self.__forbid_fallback: bool - self.__is_dirty: bool - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__blueprint_id', blueprint_id) - __set_faux_immutable__(self, '__actions', actions) - __set_faux_immutable__(self, '__fees', fees) - __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) - __set_faux_immutable__(self, '__is_dirty', False) - - def __getattr__(self, method_name: str) -> ProxyPublicMethodAccessor: - from hathor.nanocontracts import NCFail - if self.__is_dirty: - raise NCFail( - f'prepared proxy public method for blueprint `{self.__blueprint_id.hex()}` was already used, ' - f'you must use `public` on the proxy to call it again' - ) - - __set_faux_immutable__(self, '__is_dirty', True) - - return ProxyPublicMethodAccessor( - runner=self.__runner, - blueprint_id=self.__blueprint_id, - method_name=method_name, - actions=self.__actions, - fees=self.__fees, - forbid_fallback=self.__forbid_fallback, - ) - - -@final -class ProxyViewMethodAccessor(FauxImmutable): - """ - This class represents a "proxy view method", or a proxy view method accessor, during a blueprint method execution. - It's a callable that will forward the call to the actual wrapped blueprint via syscall. - It may be used multiple times to call the same method with different arguments. - """ - __slots__ = ('__runner', '__blueprint_id', '__method_name') - - def __init__(self, *, runner: Runner, blueprint_id: BlueprintId, method_name: str) -> None: - self.__runner: Runner - self.__blueprint_id: BlueprintId - self.__method_name: str - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__blueprint_id', blueprint_id) - __set_faux_immutable__(self, '__method_name', method_name) - - def call(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments. This is just an alias for calling the object directly.""" - return self(*args, **kwargs) - - def __call__(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments.""" - return self.__runner.syscall_proxy_call_view_method( - blueprint_id=self.__blueprint_id, - method_name=self.__method_name, - args=args, - kwargs=kwargs, - ) - - -@final -class ProxyPublicMethodAccessor(FauxImmutable): - """ - This class represents a "proxy public method", or a proxy public method accessor, during a blueprint method - execution. - It's a callable that will forward the call to the actual wrapped blueprint via syscall. - It can only be used once because it consumes the provided actions after a single use. - """ - __slots__ = ( - '__runner', - '__blueprint_id', - '__method_name', - '__actions', - '__fees', - '__forbid_fallback', - '__is_dirty', - ) - - def __init__( - self, - *, - runner: Runner, - blueprint_id: BlueprintId, - method_name: str, - actions: Sequence[NCAction], - fees: Sequence[NCFee], - forbid_fallback: bool, - ) -> None: - self.__runner: Runner - self.__blueprint_id: BlueprintId - self.__method_name: str - self.__actions: Sequence[NCAction] - self.__fees: Sequence[NCFee] - self.__forbid_fallback: bool - self.__is_dirty: bool - - __set_faux_immutable__(self, '__runner', runner) - __set_faux_immutable__(self, '__blueprint_id', blueprint_id) - __set_faux_immutable__(self, '__method_name', method_name) - __set_faux_immutable__(self, '__actions', actions) - __set_faux_immutable__(self, '__fees', fees) - __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) - __set_faux_immutable__(self, '__is_dirty', False) - - def call(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments. This is just an alias for calling the object directly.""" - return self(*args, **kwargs) - - def __call__(self, *args: Any, **kwargs: Any) -> object: - """Call the method with the provided arguments.""" - nc_args = NCParsedArgs(args, kwargs) - return self.call_with_nc_args(nc_args) - - def call_with_nc_args(self, nc_args: NCArgs) -> object: - """Call the method with the provided NCArgs.""" - from hathor.nanocontracts import NCFail - if self.__is_dirty: - raise NCFail( - f'accessor for proxy public method `{self.__method_name}` was already used, ' - f'you must use `public`/`public_method` on the proxy to call it again' - ) - - __set_faux_immutable__(self, '__is_dirty', True) - - return self.__runner.syscall_proxy_call_public_method( - blueprint_id=self.__blueprint_id, - method_name=self.__method_name, - actions=self.__actions, - fees=self.__fees, - nc_args=nc_args, - forbid_fallback=self.__forbid_fallback, - ) +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.proxy_accessor import * # noqa: F401,F403 +from hathorlib.nanocontracts.proxy_accessor import ProxyAccessor # noqa: F401 diff --git a/hathor/nanocontracts/rng.py b/hathor/nanocontracts/rng.py index 71bda4c02..5418f1976 100644 --- a/hathor/nanocontracts/rng.py +++ b/hathor/nanocontracts/rng.py @@ -12,83 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations - -from typing import Sequence, TypeVar, final - -from cryptography.hazmat.primitives.ciphers import Cipher, CipherContext, algorithms - -from hathor.difficulty import Hash -from hathor.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ - -T = TypeVar('T') - - -@final -class NanoRNG(FauxImmutable): - """Implement a deterministic random number generator that will be used by the sorter. - - This implementation uses the ChaCha20 encryption as RNG. - """ - - __slots__ = ('__seed', '__encryptor') - - def __init__(self, seed: bytes) -> None: - self.__seed: Hash - self.__encryptor: CipherContext - __set_faux_immutable__(self, '__seed', Hash(seed)) - - key = self.__seed - nonce = self.__seed[:16] - - algorithm = algorithms.ChaCha20(key, nonce) - cipher = Cipher(algorithm, mode=None) - __set_faux_immutable__(self, '__encryptor', cipher.encryptor()) - - def randbytes(self, size: int) -> bytes: - """Return a random string of bytes.""" - assert size >= 1 - ciphertext = self.__encryptor.update(b'\0' * size) - assert len(ciphertext) == size - return ciphertext - - def randbits(self, bits: int) -> int: - """Return a random integer in the range [0, 2**bits).""" - assert bits >= 1 - size = (bits + 7) // 8 - ciphertext = self.randbytes(size) - x = int.from_bytes(ciphertext, byteorder='little', signed=False) - return x % (2**bits) - - def randbelow(self, n: int) -> int: - """Return a random integer in the range [0, n).""" - assert n >= 1 - k = n.bit_length() - r = self.randbits(k) # 0 <= r < 2**k - while r >= n: - r = self.randbits(k) - return r - - def randrange(self, start: int, stop: int, step: int = 1) -> int: - """Return a random integer in the range [start, stop) with a given step. - - Roughly equivalent to `choice(range(start, stop, step))` but supports arbitrarily large ranges.""" - assert stop > start - assert step >= 1 - qty = (stop - start + step - 1) // step - k = self.randbelow(qty) - return start + k * step - - def randint(self, a: int, b: int) -> int: - """Return a random integer in the range [a, b].""" - assert b >= a - return a + self.randbelow(b - a + 1) - - def choice(self, seq: Sequence[T]) -> T: - """Choose a random element from a non-empty sequence.""" - return seq[self.randbelow(len(seq))] - - def random(self) -> float: - """Return a random float in the range [0, 1).""" - # 2**53 is the maximum integer float can represent without loss of precision. - return self.randbits(53) / 2**53 +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.rng import * # noqa: F401,F403 +from hathorlib.nanocontracts.rng import NanoRNG # noqa: F401 diff --git a/hathor/nanocontracts/vertex_data.py b/hathor/nanocontracts/vertex_data.py index 22305955b..45484fa88 100644 --- a/hathor/nanocontracts/vertex_data.py +++ b/hathor/nanocontracts/vertex_data.py @@ -14,17 +14,26 @@ from __future__ import annotations -from dataclasses import dataclass -from enum import StrEnum, unique from typing import TYPE_CHECKING -from typing_extensions import Self - from hathor.transaction.scripts import P2PKH, MultiSig, parse_address_script -from hathor.types import TokenUid, VertexId +from hathorlib.nanocontracts.types import VertexId +# Re-export from hathorlib for backward compatibility +from hathorlib.nanocontracts.vertex_data import * # noqa: F401,F403 +from hathorlib.nanocontracts.vertex_data import ( # noqa: F401 + BlockData, + HeaderData, + NanoHeaderData, + ScriptInfo, + ScriptType, + TokenUid, + TxInputData, + TxOutputData, + VertexData, +) if TYPE_CHECKING: - from hathor.transaction import BaseTransaction, Block, TxInput, TxOutput, TxVersion + from hathor.transaction import BaseTransaction, Block, TxInput, TxOutput from hathor.transaction.headers.nano_header import NanoHeader @@ -46,147 +55,91 @@ def _get_txin_output(vertex: BaseTransaction, txin: TxInput) -> TxOutput | None: return txin_output -@dataclass(frozen=True, slots=True, kw_only=True) -class VertexData: - version: TxVersion - hash: bytes - nonce: int - signal_bits: int - weight: float - inputs: tuple[TxInputData, ...] - outputs: tuple[TxOutputData, ...] - tokens: tuple[TokenUid, ...] - parents: tuple[VertexId, ...] - headers: tuple[HeaderData, ...] - - @classmethod - def create_from_vertex(cls, vertex: BaseTransaction) -> Self: - from hathor.transaction import Transaction - from hathor.transaction.headers.nano_header import NanoHeader - - inputs = tuple( - TxInputData.create_from_txin(txin, _get_txin_output(vertex, txin)) - for txin in vertex.inputs - ) - outputs = tuple(TxOutputData.create_from_txout(txout) for txout in vertex.outputs) - parents = tuple(vertex.parents) - tokens: tuple[TokenUid, ...] = tuple() - - assert isinstance(vertex, Transaction) - headers_data: list[HeaderData] = [] - has_nano_header = False - for header in vertex.headers: - if isinstance(header, NanoHeader): - assert not has_nano_header, 'code should guarantee NanoHeader only appears once' - headers_data.append(NanoHeaderData.create_from_nano_header(header)) - has_nano_header = True - - original_tokens = getattr(vertex, 'tokens', None) - if original_tokens is not None: - # XXX Should we add HTR_TOKEN_ID as first token? - tokens = tuple(original_tokens) - - return cls( - version=vertex.version, - hash=vertex.hash, - nonce=vertex.nonce, - signal_bits=vertex.signal_bits, - weight=vertex.weight, - inputs=inputs, - outputs=outputs, - tokens=tokens, - parents=parents, - headers=tuple(headers_data), - ) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class TxInputData: - tx_id: VertexId - index: int - data: bytes - info: TxOutputData | None - - @classmethod - def create_from_txin(cls, txin: TxInput, txin_output: TxOutput | None) -> Self: - return cls( - tx_id=txin.tx_id, - index=txin.index, - data=txin.data, - info=TxOutputData.create_from_txout(txin_output) if txin_output else None, - ) - - -@unique -class ScriptType(StrEnum): - P2PKH = 'P2PKH' - MULTI_SIG = 'MultiSig' - - -@dataclass(slots=True, frozen=True, kw_only=True) -class ScriptInfo: - type: ScriptType - address: str - timelock: int | None - - @classmethod - def from_script(cls, script: P2PKH | MultiSig) -> Self: - return cls( - type=ScriptType(script.get_type()), - address=script.get_address(), - timelock=script.get_timelock(), - ) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class TxOutputData: - value: int - raw_script: bytes - parsed_script: ScriptInfo | None - token_data: int - - @classmethod - def create_from_txout(cls, txout: TxOutput) -> Self: - parsed = parse_address_script(txout.script) - return cls( - value=txout.value, - raw_script=txout.script, - parsed_script=ScriptInfo.from_script(parsed) if parsed is not None else None, - token_data=txout.token_data, - ) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class BlockData: - hash: VertexId - timestamp: int - height: int - - @classmethod - def create_from_block(cls, block: Block) -> Self: - return cls( - hash=block.hash, - timestamp=block.timestamp, - height=block.get_height(), - ) - - -class HeaderData: - """Marker class, represents an arbitrary vertex-header.""" - - -@dataclass(frozen=True, slots=True, kw_only=True) -class NanoHeaderData(HeaderData): - nc_seqnum: int - nc_id: VertexId - nc_method: str - nc_args_bytes: bytes - - @classmethod - def create_from_nano_header(cls, nc_header: NanoHeader) -> Self: - return cls( - nc_seqnum=nc_header.nc_seqnum, - nc_id=nc_header.nc_id, - nc_method=nc_header.nc_method, - nc_args_bytes=nc_header.nc_args_bytes, - ) +def create_script_info_from_script(script: P2PKH | MultiSig) -> ScriptInfo: + """Create a ScriptInfo from a parsed script object.""" + return ScriptInfo( + type=ScriptType(script.get_type()), + address=script.get_address(), + timelock=script.get_timelock(), + ) + + +def create_txoutput_data_from_txout(txout: TxOutput) -> TxOutputData: + """Create a TxOutputData from a TxOutput.""" + parsed = parse_address_script(txout.script) + return TxOutputData( + value=txout.value, + raw_script=txout.script, + parsed_script=create_script_info_from_script(parsed) if parsed is not None else None, + token_data=txout.token_data, + ) + + +def create_txinput_data_from_txin(txin: TxInput, txin_output: TxOutput | None) -> TxInputData: + """Create a TxInputData from a TxInput and its corresponding output.""" + return TxInputData( + tx_id=VertexId(txin.tx_id), + index=txin.index, + data=txin.data, + info=create_txoutput_data_from_txout(txin_output) if txin_output else None, + ) + + +def create_nano_header_data_from_nano_header(nc_header: NanoHeader) -> NanoHeaderData: + """Create a NanoHeaderData from a NanoHeader.""" + return NanoHeaderData( + nc_seqnum=nc_header.nc_seqnum, + nc_id=VertexId(nc_header.nc_id), + nc_method=nc_header.nc_method, + nc_args_bytes=nc_header.nc_args_bytes, + ) + + +def create_block_data_from_block(block: Block) -> BlockData: + """Create a BlockData from a Block.""" + return BlockData( + hash=VertexId(block.hash), + timestamp=block.timestamp, + height=block.get_height(), + ) + + +def create_vertex_data_from_vertex(vertex: BaseTransaction) -> VertexData: + """Create a VertexData from a transaction vertex.""" + from hathor.transaction import Transaction + from hathor.transaction.headers.nano_header import NanoHeader + + inputs = tuple( + create_txinput_data_from_txin(txin, _get_txin_output(vertex, txin)) + for txin in vertex.inputs + ) + outputs = tuple(create_txoutput_data_from_txout(txout) for txout in vertex.outputs) + parents = tuple([VertexId(p) for p in vertex.parents]) + tokens: tuple[TokenUid, ...] = tuple() + + assert isinstance(vertex, Transaction) + headers_data: list[HeaderData] = [] + has_nano_header = False + for header in vertex.headers: + if isinstance(header, NanoHeader): + assert not has_nano_header, 'code should guarantee NanoHeader only appears once' + headers_data.append(create_nano_header_data_from_nano_header(header)) + has_nano_header = True + + original_tokens = getattr(vertex, 'tokens', None) + if original_tokens is not None: + # XXX Should we add HTR_TOKEN_ID as first token? + tokens = tuple(original_tokens) + + return VertexData( + version=vertex.version, + hash=vertex.hash, + nonce=vertex.nonce, + signal_bits=vertex.signal_bits, + weight=vertex.weight, + inputs=inputs, + outputs=outputs, + tokens=tokens, + parents=parents, + headers=tuple(headers_data), + ) diff --git a/hathor/transaction/base_transaction.py b/hathor/transaction/base_transaction.py index 42346a145..9211a001a 100644 --- a/hathor/transaction/base_transaction.py +++ b/hathor/transaction/base_transaction.py @@ -20,7 +20,6 @@ import time import weakref from abc import ABC, abstractmethod -from enum import IntEnum from itertools import chain from math import isfinite, log from typing import TYPE_CHECKING, Any, ClassVar, Generic, Iterator, Optional, TypeAlias, TypeVar @@ -39,6 +38,7 @@ from hathor.types import TokenUid, TxOutputScript, VertexId from hathor.util import classproperty from hathor.utils.weight import weight_to_work +from hathorlib.base_transaction import TxVersion # noqa: F401 if TYPE_CHECKING: from _hashlib import HASH @@ -78,49 +78,32 @@ def aux_calc_weight(w1: float, w2: float, multiplier: int) -> float: return a + log(1 + 2**(b - a) * multiplier, 2) -# Versions are sequential for blocks and transactions -class TxVersion(IntEnum): - REGULAR_BLOCK = 0 - REGULAR_TRANSACTION = 1 - TOKEN_CREATION_TRANSACTION = 2 - MERGE_MINED_BLOCK = 3 - # DEPRECATED_NANO_CONTRACT = 4 # XXX: Temporary to keep compatibility - POA_BLOCK = 5 - ON_CHAIN_BLUEPRINT = 6 - - @classmethod - def _missing_(cls, value: Any) -> None: - assert isinstance(value, int), f"Value '{value}' must be an integer" - assert value <= _ONE_BYTE, f'Value {hex(value)} must not be larger than one byte' - - raise ValueError(f'Invalid version: {value}') - - def get_cls(self) -> type['BaseTransaction']: - from hathor.transaction.block import Block - from hathor.transaction.merge_mined_block import MergeMinedBlock - from hathor.transaction.poa import PoaBlock - from hathor.transaction.token_creation_tx import TokenCreationTransaction - from hathor.transaction.transaction import Transaction - - cls_map: dict[TxVersion, type[BaseTransaction]] = { - TxVersion.REGULAR_BLOCK: Block, - TxVersion.REGULAR_TRANSACTION: Transaction, - TxVersion.TOKEN_CREATION_TRANSACTION: TokenCreationTransaction, - TxVersion.MERGE_MINED_BLOCK: MergeMinedBlock, - TxVersion.POA_BLOCK: PoaBlock - } - - settings = get_global_settings() - if settings.ENABLE_NANO_CONTRACTS: - from hathor.nanocontracts.on_chain_blueprint import OnChainBlueprint - cls_map[TxVersion.ON_CHAIN_BLUEPRINT] = OnChainBlueprint - - cls = cls_map.get(self) - - if cls is None: - raise ValueError('Invalid version.') - else: - return cls +def get_cls_from_tx_version(tx_version: TxVersion) -> type['BaseTransaction']: + from hathor.transaction.block import Block + from hathor.transaction.merge_mined_block import MergeMinedBlock + from hathor.transaction.poa import PoaBlock + from hathor.transaction.token_creation_tx import TokenCreationTransaction + from hathor.transaction.transaction import Transaction + + cls_map: dict[TxVersion, type[BaseTransaction]] = { + TxVersion.REGULAR_BLOCK: Block, + TxVersion.REGULAR_TRANSACTION: Transaction, + TxVersion.TOKEN_CREATION_TRANSACTION: TokenCreationTransaction, + TxVersion.MERGE_MINED_BLOCK: MergeMinedBlock, + TxVersion.POA_BLOCK: PoaBlock + } + + settings = get_global_settings() + if settings.ENABLE_NANO_CONTRACTS: + from hathor.nanocontracts.on_chain_blueprint import OnChainBlueprint + cls_map[TxVersion.ON_CHAIN_BLUEPRINT] = OnChainBlueprint + + cls = cls_map.get(tx_version) + + if cls is None: + raise ValueError('Invalid version.') + else: + return cls _base_transaction_log = logger.new() diff --git a/hathor/transaction/headers/nano_header.py b/hathor/transaction/headers/nano_header.py index 8fb78f6fd..ee62117e5 100644 --- a/hathor/transaction/headers/nano_header.py +++ b/hathor/transaction/headers/nano_header.py @@ -225,9 +225,9 @@ def get_actions(self) -> list[NCAction]: def get_context(self) -> Context: """Return a context to be used in a method call.""" - from hathor.nanocontracts.context import Context + from hathor.nanocontracts.context import create_context_from_vertex from hathor.nanocontracts.types import Address - return Context.create_from_vertex( + return create_context_from_vertex( caller_id=Address(self.nc_address), vertex=self.tx, actions=self.get_actions(), diff --git a/hathor/transaction/vertex_parser/_vertex_parser.py b/hathor/transaction/vertex_parser/_vertex_parser.py index 7713c18fc..500fd40e8 100644 --- a/hathor/transaction/vertex_parser/_vertex_parser.py +++ b/hathor/transaction/vertex_parser/_vertex_parser.py @@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Type from hathor.serialization.exceptions import SerializationError +from hathor.transaction.base_transaction import get_cls_from_tx_version from hathor.transaction.headers import FeeHeader, NanoHeader, VertexBaseHeader, VertexHeaderId if TYPE_CHECKING: @@ -67,7 +68,7 @@ def deserialize(self, data: bytes, storage: TransactionStorage | None = None) -> if not is_valid: raise StructError(f"invalid vertex version: {tx_version}") - cls = tx_version.get_cls() + cls = get_cls_from_tx_version(tx_version) return cls.create_from_struct(data, storage=storage) except (ValueError, SerializationError) as e: raise StructError('Invalid bytes to create transaction subclass.') from e diff --git a/hathor_tests/nanocontracts/blueprints/unittest.py b/hathor_tests/nanocontracts/blueprints/unittest.py index e1f01f955..aa4c0713b 100644 --- a/hathor_tests/nanocontracts/blueprints/unittest.py +++ b/hathor_tests/nanocontracts/blueprints/unittest.py @@ -10,7 +10,7 @@ from hathor.nanocontracts.nc_exec_logs import NCLogConfig from hathor.nanocontracts.on_chain_blueprint import Code, OnChainBlueprint from hathor.nanocontracts.types import Address, BlueprintId, ContractId, NCAction, TokenUid, VertexId -from hathor.nanocontracts.vertex_data import BlockData, VertexData +from hathor.nanocontracts.vertex_data import BlockData, create_vertex_data_from_vertex from hathor.transaction import Transaction, Vertex from hathor.transaction.token_info import TokenVersion from hathor.util import not_none @@ -174,7 +174,7 @@ def create_context( """Create a Context instance with optional values or defaults.""" return Context( caller_id=caller_id or self.gen_random_address(), - vertex_data=VertexData.create_from_vertex(vertex or self.get_genesis_tx()), + vertex_data=create_vertex_data_from_vertex(vertex or self.get_genesis_tx()), block_data=BlockData(hash=VertexId(b''), timestamp=timestamp or 0, height=0), actions=Context.__group_actions__(actions or ()), ) diff --git a/hathor_tests/nanocontracts/test_exposed_properties.py b/hathor_tests/nanocontracts/test_exposed_properties.py index 394c984a2..b67dd199d 100644 --- a/hathor_tests/nanocontracts/test_exposed_properties.py +++ b/hathor_tests/nanocontracts/test_exposed_properties.py @@ -29,7 +29,6 @@ 'hathor.Context.block', 'hathor.Context.caller_id', 'hathor.Context.copy', - 'hathor.Context.create_from_vertex', 'hathor.Context.get_caller_address', 'hathor.Context.get_caller_contract_id', 'hathor.Context.get_single_action', diff --git a/hathor_tests/tx/test_tx.py b/hathor_tests/tx/test_tx.py index 888454ce7..57f3bd10c 100644 --- a/hathor_tests/tx/test_tx.py +++ b/hathor_tests/tx/test_tx.py @@ -13,6 +13,7 @@ from hathor.feature_activation.utils import Features from hathor.simulator.utils import add_new_blocks from hathor.transaction import MAX_OUTPUT_VALUE, Block, Transaction, TxInput, TxOutput, Vertex +from hathor.transaction.base_transaction import get_cls_from_tx_version from hathor.transaction.exceptions import ( BlockWithInputs, ConflictingInputs, @@ -925,9 +926,9 @@ def test_tx_version_and_signal_bits(self): # test get the correct class version = TxVersion(0x00) - self.assertEqual(version.get_cls(), Block) + self.assertEqual(get_cls_from_tx_version(version), Block) version = TxVersion(0x01) - self.assertEqual(version.get_cls(), Transaction) + self.assertEqual(get_cls_from_tx_version(version), Transaction) # test Block.__init__() fails with self.assertRaises(AssertionError) as cm: diff --git a/hathor_tests/tx/test_tx_deserialization.py b/hathor_tests/tx/test_tx_deserialization.py index deb12e101..42a480958 100644 --- a/hathor_tests/tx/test_tx_deserialization.py +++ b/hathor_tests/tx/test_tx_deserialization.py @@ -2,6 +2,7 @@ from hathor.daa import DifficultyAdjustmentAlgorithm from hathor.transaction import Block, MergeMinedBlock, Transaction, TxVersion +from hathor.transaction.base_transaction import get_cls_from_tx_version from hathor.transaction.token_creation_tx import TokenCreationTransaction from hathor.verification.verification_service import VerificationService from hathor.verification.vertex_verifiers import VertexVerifiers @@ -41,7 +42,7 @@ def verbose(key, value): self.assertEqual(key, 'version') tx_version = TxVersion(version) - self.assertEqual(tx_version.get_cls(), cls) + self.assertEqual(get_cls_from_tx_version(tx_version), cls) self.assertEqual(bytes(tx), self.tx_bytes) diff --git a/hathorlib/hathorlib/base_transaction.py b/hathorlib/hathorlib/base_transaction.py index 9865bf301..7a65a2b60 100644 --- a/hathorlib/hathorlib/base_transaction.py +++ b/hathorlib/hathorlib/base_transaction.py @@ -64,13 +64,13 @@ def aux_calc_weight(w1: float, w2: float, multiplier: int) -> float: class TxVersion(IntEnum): - """Versions are sequential for blocks and transactions""" - + """ Indicate transaction type when serialized. """ REGULAR_BLOCK = 0 REGULAR_TRANSACTION = 1 TOKEN_CREATION_TRANSACTION = 2 MERGE_MINED_BLOCK = 3 - NANO_CONTRACT = 4 + # DEPRECATED_NANO_CONTRACT = 4 + POA_BLOCK = 5 ON_CHAIN_BLUEPRINT = 6 @classmethod @@ -80,25 +80,24 @@ def _missing_(cls, value: Any) -> None: raise ValueError(f'Invalid version: {value}') - def get_cls(self) -> Type['BaseTransaction']: - from hathorlib import Block, TokenCreationTransaction, Transaction - from hathorlib.nanocontracts.nanocontract import DeprecatedNanoContract - from hathorlib.nanocontracts.on_chain_blueprint import OnChainBlueprint - - cls_map: Dict[TxVersion, Type[BaseTransaction]] = { - TxVersion.REGULAR_BLOCK: Block, - TxVersion.REGULAR_TRANSACTION: Transaction, - TxVersion.TOKEN_CREATION_TRANSACTION: TokenCreationTransaction, - TxVersion.NANO_CONTRACT: DeprecatedNanoContract, - TxVersion.ON_CHAIN_BLUEPRINT: OnChainBlueprint, - } - cls = cls_map.get(self) +def get_cls_from_tx_version(tx_version: TxVersion) -> Type['BaseTransaction']: + from hathorlib import Block, TokenCreationTransaction, Transaction + from hathorlib.nanocontracts.on_chain_blueprint import OnChainBlueprint - if cls is None: - raise ValueError('Invalid version.') - else: - return cls + cls_map: Dict[TxVersion, Type[BaseTransaction]] = { + TxVersion.REGULAR_BLOCK: Block, + TxVersion.REGULAR_TRANSACTION: Transaction, + TxVersion.TOKEN_CREATION_TRANSACTION: TokenCreationTransaction, + TxVersion.ON_CHAIN_BLUEPRINT: OnChainBlueprint, + } + + cls = cls_map.get(tx_version) + + if cls is None: + raise ValueError('Invalid version.') + else: + return cls class BaseTransaction(ABC): @@ -724,7 +723,7 @@ def tx_or_block_from_bytes(data: bytes) -> BaseTransaction: version = data[1] try: tx_version = TxVersion(version) - cls = tx_version.get_cls() + cls = get_cls_from_tx_version(tx_version) return cls.create_from_struct(data) except ValueError: raise StructError('Invalid bytes to create transaction subclass.') diff --git a/hathorlib/hathorlib/headers/__init__.py b/hathorlib/hathorlib/headers/__init__.py index c39de7ed1..c7efac639 100644 --- a/hathorlib/hathorlib/headers/__init__.py +++ b/hathorlib/hathorlib/headers/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. from hathorlib.headers.base import VertexBaseHeader -from hathorlib.headers.deprecated_nano_header import DeprecatedNanoHeader from hathorlib.headers.fee_header import FeeEntry, FeeHeader, FeeHeaderEntry from hathorlib.headers.nano_header import NC_INITIALIZE_METHOD, NanoHeader from hathorlib.headers.types import VertexHeaderId @@ -22,7 +21,6 @@ 'VertexBaseHeader', 'VertexHeaderId', 'NanoHeader', - 'DeprecatedNanoHeader', 'FeeHeader', 'FeeHeaderEntry', 'FeeEntry', diff --git a/hathorlib/hathorlib/headers/deprecated_nano_header.py b/hathorlib/hathorlib/headers/deprecated_nano_header.py deleted file mode 100644 index 5ad294d06..000000000 --- a/hathorlib/hathorlib/headers/deprecated_nano_header.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2023 Hathor Labs -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from collections import deque -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from hathorlib.headers.base import VertexBaseHeader -from hathorlib.headers.types import VertexHeaderId -from hathorlib.utils import int_to_bytes, unpack, unpack_len - -if TYPE_CHECKING: - from hathorlib.base_transaction import BaseTransaction - from hathorlib.headers.nano_header import NanoHeader, NanoHeaderAction - - -NC_VERSION = 1 - - -@dataclass(frozen=True) -class DeprecatedNanoHeader(VertexBaseHeader): - tx: BaseTransaction - - # nc_id equals to the blueprint_id when a Nano Contract is being created. - # nc_id equals to the nanocontract_id when a method is being called. - nc_id: bytes - - # Name of the method to be called. When creating a new Nano Contract, it must be equal to 'initialize'. - nc_method: str - - # Serialized arguments to nc_method. - nc_args_bytes: bytes - - nc_actions: list[NanoHeaderAction] - - # Pubkey and signature of the transaction owner / caller. - nc_pubkey: bytes - nc_signature: bytes - - nc_version: int = NC_VERSION - - @classmethod - def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[DeprecatedNanoHeader, bytes]: - header_id, buf = buf[:1], buf[1:] - assert header_id == VertexHeaderId.NANO_HEADER.value - (nc_version,), buf = unpack('!B', buf) - if nc_version != NC_VERSION: - raise ValueError('unknown nanocontract version: {}'.format(nc_version)) - - nc_id, buf = unpack_len(32, buf) - (nc_method_len,), buf = unpack('!B', buf) - nc_method, buf = unpack_len(nc_method_len, buf) - (nc_args_bytes_len,), buf = unpack('!H', buf) - nc_args_bytes, buf = unpack_len(nc_args_bytes_len, buf) - - nc_actions: list[NanoHeaderAction] = [] - from hathorlib.nanocontracts import DeprecatedNanoContract - if not isinstance(tx, DeprecatedNanoContract): - (nc_actions_len,), buf = unpack('!B', buf) - for _ in range(nc_actions_len): - action, buf = NanoHeader._deserialize_action(buf) - nc_actions.append(action) - - (nc_pubkey_len,), buf = unpack('!B', buf) - nc_pubkey, buf = unpack_len(nc_pubkey_len, buf) - (nc_signature_len,), buf = unpack('!B', buf) - nc_signature, buf = unpack_len(nc_signature_len, buf) - - decoded_nc_method = nc_method.decode('ascii') - - return cls( - tx=tx, - nc_version=nc_version, - nc_id=nc_id, - nc_method=decoded_nc_method, - nc_args_bytes=nc_args_bytes, - nc_actions=nc_actions, - nc_pubkey=nc_pubkey, - nc_signature=nc_signature, - ), bytes(buf) - - def _serialize_without_header_id(self, *, skip_signature: bool) -> deque[bytes]: - """Serialize the header with the option to skip the signature.""" - encoded_method = self.nc_method.encode('ascii') - - ret: deque[bytes] = deque() - ret.append(int_to_bytes(NC_VERSION, 1)) - ret.append(self.nc_id) - ret.append(int_to_bytes(len(encoded_method), 1)) - ret.append(encoded_method) - ret.append(int_to_bytes(len(self.nc_args_bytes), 2)) - ret.append(self.nc_args_bytes) - - from hathorlib.nanocontracts import DeprecatedNanoContract - if not isinstance(self.tx, DeprecatedNanoContract): - ret.append(int_to_bytes(len(self.nc_actions), 1)) - for action in self.nc_actions: - ret.append(NanoHeader._serialize_action(action)) - - ret.append(int_to_bytes(len(self.nc_pubkey), 1)) - ret.append(self.nc_pubkey) - if not skip_signature: - ret.append(int_to_bytes(len(self.nc_signature), 1)) - ret.append(self.nc_signature) - else: - ret.append(int_to_bytes(0, 1)) - return ret - - def serialize(self) -> bytes: - ret = self._serialize_without_header_id(skip_signature=False) - ret.appendleft(VertexHeaderId.NANO_HEADER.value) - return b''.join(ret) - - def get_sighash_bytes(self) -> bytes: - ret = self._serialize_without_header_id(skip_signature=True) - return b''.join(ret) diff --git a/hathorlib/hathorlib/headers/nano_header.py b/hathorlib/hathorlib/headers/nano_header.py index 7fb8c8e9f..628ed6e25 100644 --- a/hathorlib/hathorlib/headers/nano_header.py +++ b/hathorlib/hathorlib/headers/nano_header.py @@ -80,8 +80,6 @@ def _deserialize_action(cls, buf: bytes) -> tuple[NanoHeaderAction, bytes]: @classmethod def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[NanoHeader, bytes]: - from hathorlib.nanocontracts import DeprecatedNanoContract - header_id, buf = buf[:1], buf[1:] assert header_id == VertexHeaderId.NANO_HEADER.value @@ -93,11 +91,10 @@ def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[NanoHeader, bytes nc_args_bytes, buf = unpack_len(nc_args_bytes_len, buf) nc_actions: list[NanoHeaderAction] = [] - if not isinstance(tx, DeprecatedNanoContract): - (nc_actions_len,), buf = unpack('!B', buf) - for _ in range(nc_actions_len): - action, buf = cls._deserialize_action(buf) - nc_actions.append(action) + (nc_actions_len,), buf = unpack('!B', buf) + for _ in range(nc_actions_len): + action, buf = cls._deserialize_action(buf) + nc_actions.append(action) nc_address, buf = unpack_len(ADDRESS_LEN_BYTES, buf) nc_script_len, buf = decode_unsigned(buf, max_bytes=_NC_SCRIPT_LEN_MAX_BYTES) @@ -128,8 +125,6 @@ def _serialize_action(action: NanoHeaderAction) -> bytes: def _serialize_without_header_id(self, *, skip_signature: bool) -> deque[bytes]: """Serialize the header with the option to skip the signature.""" - from hathorlib.nanocontracts import DeprecatedNanoContract - encoded_method = self.nc_method.encode('ascii') ret: deque[bytes] = deque() @@ -140,10 +135,9 @@ def _serialize_without_header_id(self, *, skip_signature: bool) -> deque[bytes]: ret.append(int_to_bytes(len(self.nc_args_bytes), 2)) ret.append(self.nc_args_bytes) - if not isinstance(self.tx, DeprecatedNanoContract): - ret.append(int_to_bytes(len(self.nc_actions), 1)) - for action in self.nc_actions: - ret.append(self._serialize_action(action)) + ret.append(int_to_bytes(len(self.nc_actions), 1)) + for action in self.nc_actions: + ret.append(self._serialize_action(action)) ret.append(self.nc_address) if not skip_signature: diff --git a/hathorlib/hathorlib/nanocontracts/__init__.py b/hathorlib/hathorlib/nanocontracts/__init__.py index c7cf6a872..4235a3961 100644 --- a/hathorlib/hathorlib/nanocontracts/__init__.py +++ b/hathorlib/hathorlib/nanocontracts/__init__.py @@ -21,13 +21,11 @@ __set_faux_immutable__, create_with_shell, ) -from hathorlib.nanocontracts.nanocontract import DeprecatedNanoContract from hathorlib.nanocontracts.on_chain_blueprint import OnChainBlueprint __all__ = [ 'ALLOW_DUNDER_ATTR', 'ALLOW_INHERITANCE_ATTR', - 'DeprecatedNanoContract', 'FauxImmutable', 'FauxImmutableMeta', 'OnChainBlueprint', diff --git a/hathorlib/hathorlib/nanocontracts/blueprint.py b/hathorlib/hathorlib/nanocontracts/blueprint.py new file mode 100644 index 000000000..d007d6611 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/blueprint.py @@ -0,0 +1,151 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, final + +from hathorlib.nanocontracts.blueprint_env import BlueprintEnvironment +from hathorlib.nanocontracts.exception import BlueprintSyntaxError +from hathorlib.nanocontracts.nc_types.utils import pretty_type +from hathorlib.nanocontracts.types import NC_FALLBACK_METHOD, NC_INITIALIZE_METHOD, NC_METHOD_TYPE_ATTR, NCMethodType + +if TYPE_CHECKING: + from hathor.nanocontracts.nc_exec_logs import NCLogger # type: ignore[import-not-found] + +FORBIDDEN_NAMES = { + 'syscall', + 'log', +} + +NC_FIELDS_ATTR: str = '__fields' + + +class _BlueprintBase(type): + """Metaclass for blueprints. + + This metaclass will modify the attributes and set Fields to them according to their types. + """ + + def __new__( + cls: type[_BlueprintBase], + name: str, + bases: tuple[type, ...], + attrs: dict[str, Any], + /, + **kwargs: Any + ) -> _BlueprintBase: + from hathorlib.nanocontracts.fields import make_field_for_type + + # Initialize only subclasses of Blueprint. + parents = [b for b in bases if isinstance(b, _BlueprintBase)] + if not parents: + return super().__new__(cls, name, bases, attrs, **kwargs) + + cls._validate_initialize_method(attrs) + cls._validate_fallback_method(attrs) + nc_fields = attrs.get('__annotations__', {}) + + # Check for forbidden names. + for field_name in nc_fields: + if field_name in FORBIDDEN_NAMES: + raise BlueprintSyntaxError(f'field name is forbidden: `{field_name}`') + + if field_name.startswith('_'): + raise BlueprintSyntaxError(f'field name cannot start with underscore: `{field_name}`') + + # Create the fields attribute with the type for each field. + attrs[NC_FIELDS_ATTR] = nc_fields + + # Use an empty __slots__ to prevent storing any attributes directly on instances. + # The declared attributes are stored as fields on the class, so they still work despite the empty slots. + attrs['__slots__'] = tuple() + + # Finally, create class! + new_class = super().__new__(cls, name, bases, attrs, **kwargs) + + # Create the Field instance according to each type. + for field_name, field_type in attrs[NC_FIELDS_ATTR].items(): + value = getattr(new_class, field_name, None) + if value is None: + # This is the case when a type is specified but not a value. + # Example: + # name: str + # age: int + try: + field = make_field_for_type(field_name, field_type) + except TypeError: + raise BlueprintSyntaxError( + f'unsupported field type: `{field_name}: {pretty_type(field_type)}`' + ) + setattr(new_class, field_name, field) + else: + # This is the case when a value is specified. + # Example: + # name: str = StrField() + # + # This was not implemented yet and will be extended later. + raise BlueprintSyntaxError(f'fields with default values are currently not supported: `{field_name}`') + + return new_class + + @staticmethod + def _validate_initialize_method(attrs: Any) -> None: + if NC_INITIALIZE_METHOD not in attrs: + raise BlueprintSyntaxError(f'blueprints require a method called `{NC_INITIALIZE_METHOD}`') + + method = attrs[NC_INITIALIZE_METHOD] + method_type = getattr(method, NC_METHOD_TYPE_ATTR, None) + + if method_type is not NCMethodType.PUBLIC: + raise BlueprintSyntaxError(f'`{NC_INITIALIZE_METHOD}` method must be annotated with @public') + + @staticmethod + def _validate_fallback_method(attrs: Any) -> None: + if NC_FALLBACK_METHOD not in attrs: + return + + method = attrs[NC_FALLBACK_METHOD] + method_type = getattr(method, NC_METHOD_TYPE_ATTR, None) + + if method_type is not NCMethodType.FALLBACK: + raise BlueprintSyntaxError(f'`{NC_FALLBACK_METHOD}` method must be annotated with @fallback') + + +class Blueprint(metaclass=_BlueprintBase): + """Base class for all blueprints. + + Example: + + class MyBlueprint(Blueprint): + name: str + age: int + """ + + __slots__ = ('__env',) + + def __init__(self, env: BlueprintEnvironment) -> None: + self.__env = env + + @final + @property + def syscall(self) -> BlueprintEnvironment: + """Return the syscall provider for the current contract.""" + return self.__env + + @final + @property + def log(self) -> NCLogger: + """Return the logger for the current contract.""" + return self.syscall.__log__ diff --git a/hathorlib/hathorlib/nanocontracts/blueprint_env.py b/hathorlib/hathorlib/nanocontracts/blueprint_env.py new file mode 100644 index 000000000..b86b8c4d1 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/blueprint_env.py @@ -0,0 +1,278 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Collection, Sequence, TypeAlias, final + +from hathorlib.conf.settings import HATHOR_TOKEN_UID +from hathorlib.nanocontracts.nano_settings import NanoSettings +from hathorlib.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, NCFee, TokenUid + +if TYPE_CHECKING: + from hathor.nanocontracts.nc_exec_logs import NCLogger # type: ignore[import-not-found] + # Temporary lazy imports from hathor + from hathor.nanocontracts.runner import Runner # type: ignore[import-not-found] + + from hathorlib.nanocontracts.contract_accessor import ContractAccessor + from hathorlib.nanocontracts.initialize_method_accessor import InitializeMethodAccessor + from hathorlib.nanocontracts.proxy_accessor import ProxyAccessor + from hathorlib.nanocontracts.rng import NanoRNG + from hathorlib.nanocontracts.storage import NCContractStorage + + +NCAttrCache: TypeAlias = dict[bytes, Any] | None + + +@final +class BlueprintEnvironment: + """A class that holds all possible interactions a blueprint may have with the system.""" + + __slots__ = ('__runner', '__log__', '__storage__', '__cache__') + + def __init__( + self, + runner: Runner, + nc_logger: NCLogger, + storage: NCContractStorage, + *, + disable_cache: bool = False, + ) -> None: + self.__log__ = nc_logger + self.__runner = runner + self.__storage__ = storage + # XXX: we could replace dict|None with a cache class that can be disabled, cleared, limited, etc + self.__cache__: NCAttrCache = None if disable_cache else {} + + @property + def rng(self) -> NanoRNG: + """Return an RNG for the current contract.""" + return self.__runner.syscall_get_rng() + + def get_contract_id(self) -> ContractId: + """Return the ContractId of the current nano contract.""" + return self.__runner.get_current_contract_id() + + def get_blueprint_id(self) -> BlueprintId: + """ + Return the BlueprintId of the current nano contract. + + This means that during a proxy call, this method will return the BlueprintId of the caller's blueprint, + NOT the BlueprintId of the Blueprint that owns the running code. + """ + contract_id = self.get_contract_id() + return self.__runner.get_blueprint_id(contract_id) + + def get_current_code_blueprint_id(self) -> BlueprintId: + """ + Return the BlueprintId of the Blueprint that owns the currently running code. + + This means that during a proxy call, this method will return the BlueprintId of the Blueprint that owns the + running code, NOT the BlueprintId of the current nano contract. + """ + return self.__runner.get_current_code_blueprint_id() + + def get_balance_before_current_call(self, token_uid: TokenUid | None = None) -> Amount: + """ + Return the balance for a given token before the current call, that is, + excluding any actions and changes in the current call. + + For instance, if a contract has 50 HTR and the call is requesting to withdraw 3 HTR, + then this method will return 50 HTR.""" + contract_id = self.get_contract_id() + balance = self.__runner.get_balance_before_current_call(contract_id, token_uid) + return Amount(balance.value) + + def get_current_balance(self, token_uid: TokenUid | None = None) -> Amount: + """ + Return the current balance for a given token, which includes all actions and changes in the current call. + + For instance, if a contract has 50 HTR and the call is requesting to withdraw 3 HTR, + then this method will return 47 HTR. + """ + contract_id = self.get_contract_id() + balance = self.__runner.get_current_balance(contract_id, token_uid) + return Amount(balance.value) + + def can_mint_before_current_call(self, token_uid: TokenUid) -> bool: + """ + Return whether a given token could be minted before the current call, that is, + excluding any actions and changes in the current call. + + For instance, if a contract has a mint authority and a call is revoking it, + then this method will return `True`. + """ + contract_id = self.get_contract_id() + balance = self.__runner.get_balance_before_current_call(contract_id, token_uid) + return balance.can_mint + + def can_mint(self, token_uid: TokenUid) -> bool: + """ + Return whether a given token can currently be minted, + which includes all actions and changes in the current call. + + For instance, if a contract has a mint authority and a call is revoking it, + then this method will return `False`. + """ + contract_id = self.get_contract_id() + balance = self.__runner.get_current_balance(contract_id, token_uid) + return balance.can_mint + + def can_melt_before_current_call(self, token_uid: TokenUid) -> bool: + """ + Return whether a given token could be melted before the current call, that is, + excluding any actions and changes in the current call. + + For instance, if a contract has a melt authority and a call is revoking it, + then this method will return `True`. + """ + contract_id = self.get_contract_id() + balance = self.__runner.get_balance_before_current_call(contract_id, token_uid) + return balance.can_melt + + def can_melt(self, token_uid: TokenUid) -> bool: + """ + Return whether a given token can currently be melted, + which includes all actions and changes in the current call. + + For instance, if a contract has a melt authority and a transaction is revoking it, + then this method will return `False`. + """ + contract_id = self.get_contract_id() + balance = self.__runner.get_current_balance(contract_id, token_uid) + return balance.can_melt + + def revoke_authorities(self, token_uid: TokenUid, *, revoke_mint: bool, revoke_melt: bool) -> None: + """Revoke authorities from this nano contract.""" + self.__runner.syscall_revoke_authorities(token_uid=token_uid, revoke_mint=revoke_mint, revoke_melt=revoke_melt) + + def mint_tokens( + self, + token_uid: TokenUid, + amount: int, + *, + fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) + ) -> None: + """Mint tokens and add them to the balance of this nano contract.""" + self.__runner.syscall_mint_tokens(token_uid=token_uid, amount=amount, fee_payment_token=fee_payment_token) + + def melt_tokens( + self, + token_uid: TokenUid, + amount: int, + *, + fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) + ) -> None: + """Melt tokens by removing them from the balance of this nano contract.""" + self.__runner.syscall_melt_tokens(token_uid=token_uid, amount=amount, fee_payment_token=fee_payment_token) + + def emit_event(self, data: bytes) -> None: + """Emit a custom event from a Nano Contract.""" + self.__runner.syscall_emit_event(data) + + def create_deposit_token( + self, + *, + token_name: str, + token_symbol: str, + amount: int, + mint_authority: bool = True, + melt_authority: bool = True, + salt: bytes = b'', + ) -> TokenUid: + """Create a new deposit-based token.""" + return self.__runner.syscall_create_child_deposit_token( + salt=salt, + token_name=token_name, + token_symbol=token_symbol, + amount=amount, + mint_authority=mint_authority, + melt_authority=melt_authority, + ) + + def create_fee_token( + self, + *, + token_name: str, + token_symbol: str, + amount: int, + mint_authority: bool = True, + melt_authority: bool = True, + salt: bytes = b'', + fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) + ) -> TokenUid: + """Create a new fee-based token.""" + return self.__runner.syscall_create_child_fee_token( + salt=salt, + token_name=token_name, + token_symbol=token_symbol, + amount=amount, + mint_authority=mint_authority, + melt_authority=melt_authority, + fee_payment_token=fee_payment_token + ) + + def change_blueprint(self, blueprint_id: BlueprintId) -> None: + """Change the blueprint of this contract.""" + self.__runner.syscall_change_blueprint(blueprint_id) + + def get_contract( + self, + contract_id: ContractId, + *, + blueprint_id: BlueprintId | Collection[BlueprintId] | None, + ) -> ContractAccessor: + """ + Get a contract accessor for the given contract ID. Use this for interacting with another contract. + + Args: + contract_id: the ID of the contract. + blueprint_id: the expected blueprint ID of the contract, or a collection of accepted blueprints, + or None if any blueprint is accepted. + + """ + from hathorlib.nanocontracts.contract_accessor import ContractAccessor + return ContractAccessor(runner=self.__runner, contract_id=contract_id, blueprint_id=blueprint_id) + + def get_proxy(self, blueprint_id: BlueprintId) -> ProxyAccessor: + """ + Get a proxy accessor for the given blueprint ID. Use this for interacting with another blueprint via a proxy. + """ + from hathorlib.nanocontracts.proxy_accessor import ProxyAccessor + return ProxyAccessor(runner=self.__runner, blueprint_id=blueprint_id) + + def setup_new_contract( + self, + blueprint_id: BlueprintId, + *actions: NCAction, + fees: Sequence[NCFee] | None = None, + salt: bytes, + ) -> InitializeMethodAccessor: + """Setup creation of a new contract.""" + from hathorlib.nanocontracts.initialize_method_accessor import InitializeMethodAccessor + self.__runner.forbid_call_on_view('setup_new_contract') + return InitializeMethodAccessor( + runner=self.__runner, + blueprint_id=blueprint_id, + salt=salt, + actions=actions, + fees=fees or (), + ) + + def get_settings(self) -> NanoSettings: + """ + Return the settings for the current Nano runtime. + Settings are not constant, they may be changed over time. + """ + return self.__runner.syscall_get_nano_settings() diff --git a/hathorlib/hathorlib/nanocontracts/blueprint_syntax_validation.py b/hathorlib/hathorlib/nanocontracts/blueprint_syntax_validation.py index 637bc7288..5abcd239e 100644 --- a/hathorlib/hathorlib/nanocontracts/blueprint_syntax_validation.py +++ b/hathorlib/hathorlib/nanocontracts/blueprint_syntax_validation.py @@ -83,7 +83,7 @@ def validate_has_ctx_arg(fn: Callable, annotation_name: str) -> None: f'@{annotation_name} method must have `Context` argument: `{fn.__name__}()`' ) - from hathor.nanocontracts.context import Context # type: ignore[import-not-found] + from hathorlib.nanocontracts.context import Context second_arg = arg_spec.args[1] if arg_spec.annotations[second_arg] is not Context: raise BlueprintSyntaxError( @@ -94,7 +94,7 @@ def validate_has_ctx_arg(fn: Callable, annotation_name: str) -> None: def validate_has_not_ctx_arg(fn: Callable, annotation_name: str) -> None: """Validate that a callable doesn't have a `Context` arg.""" - from hathor.nanocontracts.context import Context + from hathorlib.nanocontracts.context import Context arg_spec = inspect.getfullargspec(fn) if Context in arg_spec.annotations.values(): raise BlueprintSyntaxError(f'@{annotation_name} method cannot have arg with type `Context`: `{fn.__name__}()`') diff --git a/hathorlib/hathorlib/nanocontracts/context.py b/hathorlib/hathorlib/nanocontracts/context.py new file mode 100644 index 000000000..f810f4770 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/context.py @@ -0,0 +1,149 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections import defaultdict +from itertools import chain +from types import MappingProxyType +from typing import Any, Sequence, assert_never, final + +from hathorlib.nanocontracts.exception import NCFail +from hathorlib.nanocontracts.types import Address, CallerId, ContractId, NCAction, TokenUid +from hathorlib.nanocontracts.vertex_data import BlockData, VertexData +from hathorlib.utils.address import get_address_b58_from_bytes + + +@final +class Context: + """Context passed to a method call. An empty list of actions means the + method is being called with no deposits and withdrawals. + + Deposits and withdrawals are grouped by token. Note that it is impossible + to have both a deposit and a withdrawal for the same token. + """ + __slots__ = ('__actions', '__caller_id', '__vertex', '__block', '__all_actions__') + __caller_id: CallerId + __vertex: VertexData + __block: BlockData | None + __actions: MappingProxyType[TokenUid, tuple[NCAction, ...]] + + @staticmethod + def __group_actions__(actions: Sequence[NCAction]) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]: + actions_map: defaultdict[TokenUid, tuple[NCAction, ...]] = defaultdict(tuple) + for action in actions: + actions_map[action.token_uid] = (*actions_map[action.token_uid], action) + return MappingProxyType(actions_map) + + def __init__( + self, + *, + caller_id: CallerId, + vertex_data: VertexData, + block_data: BlockData | None, + actions: MappingProxyType[TokenUid, tuple[NCAction, ...]], + ) -> None: + # Dict of action where the key is the token_uid. + # If empty, it is a method call without any actions. + self.__actions = actions + + self.__all_actions__: tuple[NCAction, ...] = tuple(chain(*self.__actions.values())) + + # Vertex calling the method. + self.__vertex = vertex_data + + # Block executing the vertex. + self.__block = block_data + + # Address calling the method. + self.__caller_id = caller_id + + @property + def vertex(self) -> VertexData: + return self.__vertex + + @property + def block(self) -> BlockData: + assert self.__block is not None + return self.__block + + @property + def caller_id(self) -> CallerId: + """Get the caller ID which can be either an Address or a ContractId.""" + return self.__caller_id + + def get_caller_address(self) -> Address | None: + """Get the caller address if the caller is an address, None if it's a contract.""" + match self.caller_id: + case Address(): + return self.caller_id + case ContractId(): + return None + case _: # pragma: no cover + assert_never(self.caller_id) + + def get_caller_contract_id(self) -> ContractId | None: + """Get the caller contract ID if the caller is a contract, None if it's an address.""" + match self.caller_id: + case Address(): + return None + case ContractId(): + return self.caller_id + case _: # pragma: no cover + assert_never(self.caller_id) + + @property + def actions(self) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]: + """Get a mapping of actions per token.""" + return self.__actions + + @property + def actions_list(self) -> Sequence[NCAction]: + """Get a list of all actions.""" + return tuple(self.__all_actions__) + + def get_single_action(self, token_uid: TokenUid) -> NCAction: + """Get exactly one action for the provided token, and fail otherwise.""" + actions = self.actions.get(token_uid) + if actions is None or len(actions) != 1: + raise NCFail(f'expected exactly 1 action for token {token_uid.hex()}') + return actions[0] + + def copy(self) -> Context: + """Return a copy of the context.""" + return Context( + caller_id=self.caller_id, + vertex_data=self.vertex, + block_data=self.block, # We only copy during execution, so we know the block must not be `None`. + actions=self.actions, + ) + + def to_json(self) -> dict[str, Any]: + """Return a JSON representation of the context.""" + caller_id: str + match self.caller_id: + case Address(): + caller_id = get_address_b58_from_bytes(self.caller_id) + case ContractId(): + caller_id = self.caller_id.hex() + case _: # pragma: no cover + assert_never(self.caller_id) + + return { + 'actions': [action.to_json() for action in self.__all_actions__], + 'caller_id': caller_id, + 'timestamp': self.__block.timestamp if self.__block is not None else None, + # XXX: Deprecated attribute + 'address': caller_id, + } diff --git a/hathorlib/hathorlib/nanocontracts/contract_accessor.py b/hathorlib/hathorlib/nanocontracts/contract_accessor.py new file mode 100644 index 000000000..fa6f31d65 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/contract_accessor.py @@ -0,0 +1,378 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Collection, Sequence, final + +from hathorlib.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ +from hathorlib.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, NCFee, TokenUid + +if TYPE_CHECKING: + # XXX: Temporary lazy import from hathor + from hathor.nanocontracts.runner import Runner # type: ignore[import-not-found] + + +@final +class ContractAccessor(FauxImmutable): + """ + This class represents a "contract instance", or a contract accessor, during a blueprint method execution. + Calling custom blueprint methods on this class will forward the call to the actual wrapped blueprint via syscalls. + """ + __slots__ = ('__runner', '__contract_id', '__blueprint_ids') + + def __init__( + self, + *, + runner: Runner, + contract_id: ContractId, + blueprint_id: BlueprintId | Collection[BlueprintId] | None, + ) -> None: + self.__runner: Runner + self.__contract_id: ContractId + self.__blueprint_ids: frozenset[BlueprintId] | None + + blueprint_ids: frozenset[BlueprintId] | None + match blueprint_id: + case None: + blueprint_ids = None + case bytes(): + blueprint_ids = frozenset({blueprint_id}) + case _: + blueprint_ids = frozenset(blueprint_id) + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__contract_id', contract_id) + __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) + + def get_contract_id(self) -> ContractId: + """Return the contract id of this nano contract.""" + return self.__contract_id + + def get_blueprint_id(self) -> BlueprintId: + """Return the blueprint id of this nano contract.""" + return self.__runner.get_blueprint_id(self.__contract_id) + + def get_current_balance(self, token_uid: TokenUid | None = None) -> Amount: + """ + Return the current balance for a given token, which includes all actions and changes in the current call. + + For instance, if a contract has 50 HTR and the call is requesting to withdraw 3 HTR, + then this method will return 47 HTR. + """ + balance = self.__runner.get_current_balance(self.__contract_id, token_uid) + return Amount(balance.value) + + def can_mint(self, token_uid: TokenUid) -> bool: + """ + Return whether a given token can currently be minted, + which includes all actions and changes in the current call. + + For instance, if a contract has a mint authority and a call is revoking it, + then this method will return `False`. + """ + balance = self.__runner.get_current_balance(self.__contract_id, token_uid) + return balance.can_mint + + def can_melt(self, token_uid: TokenUid) -> bool: + """ + Return whether a given token can currently be melted, + which includes all actions and changes in the current call. + + For instance, if a contract has a melt authority and a transaction is revoking it, + then this method will return `False`. + """ + balance = self.__runner.get_current_balance(self.__contract_id, token_uid) + return balance.can_melt + + def view(self) -> Any: + """Prepare a call to a view method.""" + return PreparedViewCall( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + ) + + def public(self, *actions: NCAction, fees: Sequence[NCFee] | None = None, forbid_fallback: bool = False) -> Any: + """Prepare a call to a public method.""" + return PreparedPublicCall( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + actions=actions, + fees=fees or (), + forbid_fallback=forbid_fallback, + ) + + def get_view_method(self, method_name: str) -> ViewMethodAccessor: + """Get a view method.""" + return ViewMethodAccessor( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + method_name=method_name, + ) + + def get_public_method( + self, + method_name: str, + *actions: NCAction, + fees: Sequence[NCFee] | None = None, + forbid_fallback: bool = False, + ) -> PublicMethodAccessor: + """Get a public method.""" + return PublicMethodAccessor( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + method_name=method_name, + actions=actions, + fees=fees or (), + forbid_fallback=forbid_fallback, + ) + + +@final +class PreparedViewCall(FauxImmutable): + __slots__ = ('__runner', '__contract_id', '__blueprint_ids') + __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ + + def __init__( + self, + *, + runner: Runner, + contract_id: ContractId, + blueprint_ids: frozenset[BlueprintId] | None, + ) -> None: + self.__runner: Runner + self.__contract_id: ContractId + self.__blueprint_ids: frozenset[BlueprintId] | None + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__contract_id', contract_id) + __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) + + def __getattr__(self, method_name: str) -> ViewMethodAccessor: + return ViewMethodAccessor( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + method_name=method_name, + ) + + +@final +class PreparedPublicCall(FauxImmutable): + __slots__ = ( + '__runner', + '__contract_id', + '__blueprint_ids', + '__actions', + '__fees', + '__forbid_fallback', + '__is_dirty', + ) + __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ + + def __init__( + self, + *, + runner: Runner, + contract_id: ContractId, + blueprint_ids: frozenset[BlueprintId] | None, + actions: Sequence[NCAction], + fees: Sequence[NCFee], + forbid_fallback: bool, + ) -> None: + self.__runner: Runner + self.__contract_id: ContractId + self.__blueprint_ids: frozenset[BlueprintId] | None + self.__actions: Sequence[NCAction] + self.__fees: Sequence[NCFee] + self.__forbid_fallback: bool + self.__is_dirty: bool + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__contract_id', contract_id) + __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) + __set_faux_immutable__(self, '__actions', actions) + __set_faux_immutable__(self, '__fees', fees) + __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) + __set_faux_immutable__(self, '__is_dirty', False) + + def __getattr__(self, method_name: str) -> PublicMethodAccessor: + from hathorlib.nanocontracts.exception import NCFail + if self.__is_dirty: + raise NCFail( + f'prepared public method for contract `{self.__contract_id.hex()}` was already used, ' + f'you must use `public` on the contract to call it again' + ) + + __set_faux_immutable__(self, '__is_dirty', True) + + return PublicMethodAccessor( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + method_name=method_name, + actions=self.__actions, + fees=self.__fees, + forbid_fallback=self.__forbid_fallback, + ) + + +@final +class ViewMethodAccessor(FauxImmutable): + """ + This class represents a "view method", or a view method accessor, during a blueprint method execution. + It's a callable that will forward the call to the actual wrapped blueprint via syscall. + It may be used multiple times to call the same method with different arguments. + """ + __slots__ = ('__runner', '__contract_id', '__blueprint_ids', '__method_name') + + def __init__( + self, + *, + runner: Runner, + contract_id: ContractId, + blueprint_ids: frozenset[BlueprintId] | None, + method_name: str, + ) -> None: + self.__runner: Runner + self.__contract_id: ContractId + self.__blueprint_ids: frozenset[BlueprintId] | None + self.__method_name: str + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__contract_id', contract_id) + __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) + __set_faux_immutable__(self, '__method_name', method_name) + + def call(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments. This is just an alias for calling the object directly.""" + return self(*args, **kwargs) + + def __call__(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments.""" + validate_blueprint_id( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + ) + + return self.__runner.syscall_call_another_contract_view_method( + contract_id=self.__contract_id, + method_name=self.__method_name, + args=args, + kwargs=kwargs, + ) + + +@final +class PublicMethodAccessor(FauxImmutable): + """ + This class represents a "public method", or a public method accessor, during a blueprint method execution. + It's a callable that will forward the call to the actual wrapped blueprint via syscall. + It can only be used once because it consumes the provided actions after a single use. + """ + __slots__ = ( + '__runner', + '__contract_id', + '__blueprint_ids', + '__method_name', + '__actions', + '__fees', + '__forbid_fallback', + '__is_dirty', + ) + + def __init__( + self, + *, + runner: Runner, + contract_id: ContractId, + blueprint_ids: frozenset[BlueprintId] | None, + method_name: str, + actions: Sequence[NCAction], + fees: Sequence[NCFee], + forbid_fallback: bool, + ) -> None: + self.__runner: Runner + self.__contract_id: ContractId + self.__blueprint_ids: frozenset[BlueprintId] | None + self.__method_name: str + self.__actions: Sequence[NCAction] + self.__fees: Sequence[NCFee] + self.__forbid_fallback: bool + self.__is_dirty: bool + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__contract_id', contract_id) + __set_faux_immutable__(self, '__blueprint_ids', blueprint_ids) + __set_faux_immutable__(self, '__method_name', method_name) + __set_faux_immutable__(self, '__actions', actions) + __set_faux_immutable__(self, '__fees', fees) + __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) + __set_faux_immutable__(self, '__is_dirty', False) + + def call(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments. This is just an alias for calling the object directly.""" + return self(*args, **kwargs) + + def __call__(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments.""" + from hathorlib.nanocontracts.exception import NCFail + if self.__is_dirty: + raise NCFail( + f'accessor for public method `{self.__method_name}` was already used, ' + f'you must use `public`/`public_method` on the contract to call it again' + ) + + __set_faux_immutable__(self, '__is_dirty', True) + + validate_blueprint_id( + runner=self.__runner, + contract_id=self.__contract_id, + blueprint_ids=self.__blueprint_ids, + ) + + return self.__runner.syscall_call_another_contract_public_method( + contract_id=self.__contract_id, + method_name=self.__method_name, + actions=self.__actions, + fees=self.__fees, + args=args, + kwargs=kwargs, + forbid_fallback=self.__forbid_fallback, + ) + + +def validate_blueprint_id( + *, + runner: Runner, + contract_id: ContractId, + blueprint_ids: frozenset[BlueprintId] | None, +) -> None: + """Check whether the blueprint id of a contract matches the expected id(s), raise an exception otherwise.""" + if blueprint_ids is None: + return + + blueprint_id = runner.get_blueprint_id(contract_id) + if blueprint_id not in blueprint_ids: + from hathorlib.nanocontracts.exception import NCFail + expected = tuple(sorted(bp.hex() for bp in blueprint_ids)) + raise NCFail( + f'expected blueprint to be one of `{expected}`, ' + f'got `{blueprint_id.hex()}` for contract `{contract_id.hex()}`' + ) diff --git a/hathorlib/hathorlib/nanocontracts/fields/container.py b/hathorlib/hathorlib/nanocontracts/fields/container.py index 49da1274a..ca0e528a0 100644 --- a/hathorlib/hathorlib/nanocontracts/fields/container.py +++ b/hathorlib/hathorlib/nanocontracts/fields/container.py @@ -16,17 +16,16 @@ from abc import ABC, abstractmethod from collections.abc import Container as ContainerAbc, Mapping -from typing import Any, ClassVar, Generic, TypeAlias, TypeVar +from typing import ClassVar, Generic, TypeAlias, TypeVar from typing_extensions import TYPE_CHECKING, Self, final, get_origin, override +from hathorlib.nanocontracts.blueprint_env import NCAttrCache from hathorlib.nanocontracts.nc_types import BoolNCType, NCType from hathorlib.nanocontracts.storage.contract_storage import NCContractStorage -NCAttrCache: TypeAlias = dict[bytes, Any] | None - if TYPE_CHECKING: - from hathorlib.nanocontracts.blueprint import Blueprint # type: ignore[import-not-found] + from hathorlib.nanocontracts.blueprint import Blueprint from hathorlib.nanocontracts.fields.field import Field T = TypeVar('T') diff --git a/hathorlib/hathorlib/nanocontracts/fields/field.py b/hathorlib/hathorlib/nanocontracts/fields/field.py index e2ba890eb..50ffc5a01 100644 --- a/hathorlib/hathorlib/nanocontracts/fields/field.py +++ b/hathorlib/hathorlib/nanocontracts/fields/field.py @@ -23,7 +23,7 @@ from hathorlib.nanocontracts.nc_types.utils import TypeAliasMap, TypeToNCTypeMap if TYPE_CHECKING: - from hathorlib.nanocontracts.blueprint import Blueprint # type: ignore[import-not-found] + from hathorlib.nanocontracts.blueprint import Blueprint T = TypeVar('T') diff --git a/hathorlib/hathorlib/nanocontracts/initialize_method_accessor.py b/hathorlib/hathorlib/nanocontracts/initialize_method_accessor.py new file mode 100644 index 000000000..f5bf58e38 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/initialize_method_accessor.py @@ -0,0 +1,85 @@ +# Copyright 2025 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Sequence, final + +from hathorlib.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ +from hathorlib.nanocontracts.types import BlueprintId, ContractId, NCAction, NCFee + +if TYPE_CHECKING: + # XXX: Temporary lazy import from hathor + from hathor.nanocontracts.runner import Runner # type: ignore[import-not-found] + + +@final +class InitializeMethodAccessor(FauxImmutable): + """ + This class represents an "initialize method", or an initialize method accessor, during a blueprint method + execution. + Calling `initialize()` on it will forward the call to the actual wrapped blueprint via syscall. + It can only be used once because it consumes the provided actions after a single use. + """ + __slots__ = ( + '__runner', + '__blueprint_id', + '__salt', + '__actions', + '__fees', + '__is_dirty', + ) + + def __init__( + self, + *, + runner: Runner, + blueprint_id: BlueprintId, + salt: bytes, + actions: Sequence[NCAction], + fees: Sequence[NCFee], + ) -> None: + self.__runner: Runner + self.__blueprint_id: BlueprintId + self.__salt: bytes + self.__actions: Sequence[NCAction] + self.__fees: Sequence[NCFee] + self.__is_dirty: bool + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__blueprint_id', blueprint_id) + __set_faux_immutable__(self, '__salt', salt) + __set_faux_immutable__(self, '__actions', actions) + __set_faux_immutable__(self, '__fees', fees) + __set_faux_immutable__(self, '__is_dirty', False) + + def initialize(self, *args: Any, **kwargs: Any) -> tuple[ContractId, object]: + """Initialize a new contract.""" + from hathorlib.nanocontracts.exception import NCFail + if self.__is_dirty: + raise NCFail( + 'accessor for initialize method was already used, ' + 'you must use `setup_new_contract` to call it again' + ) + + __set_faux_immutable__(self, '__is_dirty', True) + + return self.__runner.syscall_create_another_contract( + blueprint_id=self.__blueprint_id, + salt=self.__salt, + actions=self.__actions, + fees=self.__fees, + args=args, + kwargs=kwargs, + ) diff --git a/hathorlib/hathorlib/nanocontracts/method.py b/hathorlib/hathorlib/nanocontracts/method.py new file mode 100644 index 000000000..dd00123ee --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/method.py @@ -0,0 +1,353 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from inspect import Parameter, Signature, _empty as EMPTY, signature +from types import FunctionType, MethodType +from typing import Any, TypeVar + +from typing_extensions import Self, assert_never, override + +from hathorlib.nanocontracts.context import Context +from hathorlib.nanocontracts.exception import NCFail, NCSerializationArgTooLong, NCSerializationError +from hathorlib.nanocontracts.nc_types import ( + NCType, + VarUint32NCType, + make_nc_type_for_arg_type, + make_nc_type_for_return_type, +) +from hathorlib.nanocontracts.utils import is_nc_public_method +from hathorlib.serialization import Deserializer, SerializationError, Serializer +from hathorlib.serialization.adapters import MaxBytesExceededError + +_num_args_nc_type = VarUint32NCType() +T = TypeVar('T') + +MAX_BYTES_SERIALIZED_ARG: int = 1000 + + +def _deserialize_map_exception(nc_type: NCType[T], data: bytes) -> T: + """ Internal handy method to deserialize `bytes` to `T` while mapping the exceptions.""" + try: + deserializer = Deserializer.build_bytes_deserializer(data) + value = nc_type.deserialize(deserializer) + deserializer.finalize() + return value + except MaxBytesExceededError as e: + raise NCSerializationArgTooLong from e + except SerializationError as e: + raise NCSerializationError from e + except NCFail: + raise + except Exception as e: + raise NCFail from e + + +def _serialize_map_exception(nc_type: NCType[T], value: T) -> bytes: + """ Internal handy method to serialize `T` to `bytes` while mapping the exceptions.""" + try: + serializer = Serializer.build_bytes_serializer() + nc_type.serialize(serializer, value) + return bytes(serializer.finalize()) + except MaxBytesExceededError as e: + raise NCSerializationArgTooLong from e + except SerializationError as e: + raise NCSerializationError from e + except NCFail: + raise + except Exception as e: + raise NCFail from e + + +class _ArgsNCType(NCType): + """ Inner implementation of a callable "args" using the NCType model. + """ + + _args: tuple[NCType, ...] + _max_bytes: int + + def __init__(self, args_nc_types: Iterable[NCType], max_bytes: int) -> None: + self._args = tuple(args_nc_types) + self._max_bytes = max_bytes + + @override + def _check_value(self, value: Any, /, *, deep: bool) -> None: + # XXX: we take either a tuple or a list as input + if not isinstance(value, (tuple, list)): + raise TypeError('expected tuple or list') + if len(value) > len(self._args): + raise TypeError('too many arguments') + if deep: + for i, arg_nc_type in zip(value, self._args): + arg_nc_type._check_value(i, deep=deep) + + @override + def _serialize(self, serializer: Serializer, args: tuple[Any, ...] | list[Any], /) -> None: + with serializer.with_max_bytes(self._max_bytes) as serializer: + num_args = len(args) + if num_args > len(self._args): + raise TypeError('too many arguments') + # XXX: default arguments are currently not supported, thus we reject too few arguments too + if num_args < len(self._args): + raise TypeError('too few arguments') + _num_args_nc_type.serialize(serializer, num_args) + for value, arg in zip(self._args, args): + value.serialize(serializer, arg) + + @override + def _deserialize(self, deserializer: Deserializer, /) -> tuple[Any, ...]: + with deserializer.with_max_bytes(self._max_bytes) as deserializer: + # TODO: normalize exceptions + num_args = _num_args_nc_type.deserialize(deserializer) + if num_args > len(self._args): + raise TypeError('too many arguments') + # XXX: default arguments are currently not supported, thus we reject too few arguments too + if num_args < len(self._args): + raise TypeError('too few arguments') + args = [] + for value, _ in zip(self._args, range(num_args)): + args.append(value.deserialize(deserializer)) + return tuple(args) + + @override + def _json_to_value(self, json_value: NCType.Json, /) -> tuple[Any, ...]: + if not isinstance(json_value, list): + raise ValueError('expected list') + return tuple(v.json_to_value(i) for (i, v) in zip(json_value, self._args)) + + @override + def _value_to_json(self, value: tuple[Any, ...], /) -> NCType.Json: + return [v.value_to_json(i) for (i, v) in zip(value, self._args)] + + +class ArgsOnly: + """ This class is used to parse only arguments of a call, when all that is provided is a list of argument types. + + Its primary use is for implementing `NCRawArgs.try_parse_as`. + """ + args: _ArgsNCType + + def __init__(self, args_nc_type: _ArgsNCType) -> None: + """Do not build directly, use `ArgsOnly.from_arg_types`""" + self.args = args_nc_type + + @classmethod + def from_arg_types(cls, arg_types: tuple[type, ...]) -> Self: + args_nc_types: list[NCType] = [] + for arg_type in arg_types: + args_nc_types.append(make_nc_type_for_arg_type(arg_type)) + + return cls(_ArgsNCType(args_nc_types, max_bytes=MAX_BYTES_SERIALIZED_ARG)) + + def serialize_args_bytes(self, args: tuple[Any, ...] | list[Any]) -> bytes: + """ Shortcut to serialize args directly to a bytes instead of using a serializer. + """ + return _serialize_map_exception(self.args, args) + + def deserialize_args_bytes(self, data: bytes) -> tuple[Any, ...]: + """ Shortcut to deserialize args directly from bytes instead of using a deserializer. + """ + return _deserialize_map_exception(self.args, data) + + +class ReturnOnly: + """ + This class is used to parse only the return of a method. + + Its primary use is for validating the fallback method. + """ + return_nc_type: NCType + + def __init__(self, return_nc_type: NCType) -> None: + self.return_nc_type = return_nc_type + + @classmethod + def from_callable(cls, method: Callable) -> Self: + method_signature = _get_method_signature(method) + nc_type = make_nc_type_for_return_type(method_signature.return_annotation) + return cls(nc_type) + + def serialize_return_bytes(self, return_value: Any) -> bytes: + """Shortcut to serialize a return value directly to bytes instead of using a serializer.""" + return _serialize_map_exception(self.return_nc_type, return_value) + + def deserialize_return_bytes(self, data: bytes) -> Any: + """Shortcut to deserialize a return value directly from bytes instead of using a deserializer.""" + return _deserialize_map_exception(self.return_nc_type, data) + + +# XXX: currently the relationship between the method's signature's types and the `NCType`s type's cannot be described +# with Python/mypy's typing system +class Method: + """ This class abstracts a method's type signature in relation similarly to how NCType and Field abstract a loose + "value" or a classe's "field". + + This abstraction is used to (de)serialize the arguments of a method call, and (de)serialize the result of a method + call. It may also be used to transmit values when a nano-method calls another nano-method. + + For arguments, `make_nc_type_for_arg_type` is used, which tends to preserve original types as much as possible, but + for return types `make_nc_type_for_return_type` is used, which supports `None`. + """ + name: str + arg_names: tuple[str, ...] + args: _ArgsNCType + return_: NCType + + def __init__( + self, + *, + name: str, + arg_names: Iterable[str], + args_nc_type: _ArgsNCType, + return_nc_type: NCType, + ) -> None: + """Do not build directly, use `Method.from_callable`""" + self.name = name + self.arg_names = tuple(arg_names) + self.args = args_nc_type + self.return_ = return_nc_type + + @classmethod + def from_callable(cls, method: Callable) -> Self: + method_signature = _get_method_signature(method) + + # XXX: bound methods don't have the self argument + is_bound_method: bool + + match method: + case MethodType(): + is_bound_method = True + case FunctionType(): + is_bound_method = False + case _: + raise TypeError(f'{method!r} is neither a function or a bound method') + + for param in method_signature.parameters.values(): + if isinstance(param.annotation, str): + raise TypeError('string annotations (including `from __future__ import annotations`), ' + 'are not supported') + + arg_names = [] + args_nc_types = [] + iter_params = iter(method_signature.parameters.values()) + + # XXX: bound methods don't expose the self argument + if not is_bound_method: + try: + self_param = next(iter_params) + except StopIteration: + raise TypeError('missing self argument') + if self_param.name != 'self': + # XXX: self_param is not technically required to be named 'self', it can be named anything, but it + # should at least be a warning because it's possible the author forgot the 'self' argument + raise TypeError('first argument should be self') + + if is_nc_public_method(method): + try: + ctx_param = next(iter_params) + except StopIteration: + raise TypeError('missing ctx argument') + if ctx_param.annotation is not Context: + raise TypeError('context argument must be annotated as `ctx: Context`') + + for param in iter_params: + match param.kind: + case Parameter.POSITIONAL_ONLY: # these are arguments before / + # we accept these + pass + case Parameter.POSITIONAL_OR_KEYWORD: # there are normal arguments + # we accept these + pass + case Parameter.VAR_POSITIONAL: # these are *args kind of arguments + # XXX: we can technically support this, since these can be annotated + raise TypeError('variable *args arguments are not supported') + case Parameter.KEYWORD_ONLY: # these are arguments after * or *args, which are keyword-only + raise TypeError('keyword-only arguments are not supported') + case Parameter.VAR_KEYWORD: # these are **kwargs arguments + raise TypeError('variable **kwargs arguments are not supported') + case _ as impossible_kind: # no other type of argument exist + assert_never(impossible_kind) + # XXX: this can (and probably will) be implemented in the future + if param.default is not EMPTY: + raise TypeError('default values are not supported') + arg_names.append(param.name) + args_nc_types.append(make_nc_type_for_arg_type(param.annotation)) + + return cls( + name=method.__name__, + arg_names=arg_names, + args_nc_type=_ArgsNCType(args_nc_types, max_bytes=MAX_BYTES_SERIALIZED_ARG), + return_nc_type=make_nc_type_for_return_type(method_signature.return_annotation), + ) + + def serialize_args_bytes(self, args: tuple[Any, ...] | list[Any], kwargs: dict[str, Any] | None = None) -> bytes: + """ Shortcut to serialize args directly to a bytes instead of using a serializer. + """ + if len(args) > len(self.arg_names): + raise NCFail('too many arguments') + + merged: dict[str, Any] = {} + for index, arg in enumerate(args): + name = self.arg_names[index] + merged[name] = arg + + kwargs = kwargs or {} + for name, arg in kwargs.items(): + if name not in self.arg_names: + raise NCFail(f"{self.name}() got an unexpected keyword argument '{name}'") + if name in merged: + raise NCFail(f"{self.name}() got multiple values for argument '{name}'") + merged[name] = arg + + ordered_args = [] + for name in self.arg_names: + if name not in merged: + raise NCFail(f"{self.name}() missing required argument: '{name}'") + ordered_args.append(merged[name]) + + return _serialize_map_exception(self.args, tuple(ordered_args)) + + def deserialize_args_bytes(self, data: bytes) -> tuple[Any, ...]: + """ Shortcut to deserialize args directly from bytes instead of using a deserializer. + """ + return _deserialize_map_exception(self.args, data) + + def serialize_return_bytes(self, return_value: Any) -> bytes: + """ Shortcut to serialize a return value directly to a bytes instead of using a serializer. + """ + return _serialize_map_exception(self.return_, return_value) + + def deserialize_return_bytes(self, data: bytes) -> Any: + """ Shortcut to deserialize a return value directly from bytes instead of using a deserializer. + """ + return _deserialize_map_exception(self.return_, data) + + +def _get_method_signature(method: Callable) -> Signature: + if not callable(method): + raise TypeError(f'{method!r} is not a callable object') + + # XXX: explicit all arguments to explain the choices, even if default + return signature( + method, + follow_wrapped=True, # we're interested in the implementation's signature, so we follow wrappers + globals=None, # don't expose any global + locals=None, # don't expose any local + # XXX: do not evaluate strings, this means `from __future__ import annotations` is not supported, ideally + # we should support it because it's very convenient, but it must be done with care, otherwise we could + # run into cases that do `def foo(self, i: '2**100**100') -> None`, which is syntactically legal + eval_str=False, + ) diff --git a/hathorlib/hathorlib/nanocontracts/nano_runtime_version.py b/hathorlib/hathorlib/nanocontracts/nano_runtime_version.py new file mode 100644 index 000000000..5c68f7025 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/nano_runtime_version.py @@ -0,0 +1,30 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import IntEnum + + +class NanoRuntimeVersion(IntEnum): + """ + The runtime version of Nano Contracts. + It must be updated via Feature Activation and can be used to add new syscalls, for example. + + V1: + - Initial version + + V2: + - Added `get_settings` syscall + """ + V1 = 1 + V2 = 2 diff --git a/hathorlib/hathorlib/nanocontracts/nano_settings.py b/hathorlib/hathorlib/nanocontracts/nano_settings.py new file mode 100644 index 000000000..bcd76918c --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/nano_settings.py @@ -0,0 +1,24 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass + + +@dataclass(slots=True, frozen=True, kw_only=True) +class NanoSettings: + """ + This dataclass contains information about the settings used by the current Nano runtime. + It is returned by the `get_settings` syscall. Note that settings are not constant, they may change over time. + """ + fee_per_output: int diff --git a/hathorlib/hathorlib/nanocontracts/nanocontract.py b/hathorlib/hathorlib/nanocontracts/nanocontract.py deleted file mode 100644 index 3f6b0c5ec..000000000 --- a/hathorlib/hathorlib/nanocontracts/nanocontract.py +++ /dev/null @@ -1,62 +0,0 @@ -# Copyright 2023 Hathor Labs -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from hathorlib import Transaction, TxVersion - - -class DeprecatedNanoContract(Transaction): - """NanoContract vertex to be placed on the DAG of transactions.""" - - def __init__(self) -> None: - super().__init__() - - self.version = TxVersion.NANO_CONTRACT - - # nc_id equals to the blueprint_id when a Nano Contract is being created. - # nc_id equals to the nanocontract_id when a method is being called. - self.nc_id: bytes = b'' - - # Name of the method to be called. When creating a new Nano Contract, it must be equal to 'initialize'. - self.nc_method: str = '' - - # Serialized arguments to nc_method. - self.nc_args_bytes: bytes = b'' - - # Pubkey and signature of the transaction owner / caller. - self.nc_pubkey: bytes = b'' - self.nc_signature: bytes = b'' - - ################################ - # Methods for Transaction - ################################ - - def get_funds_fields_from_struct(self, buf: bytes) -> bytes: - from hathorlib.headers import DeprecatedNanoHeader, VertexHeaderId - buf = super().get_funds_fields_from_struct(buf) - nano_header, buf = DeprecatedNanoHeader.deserialize(self, VertexHeaderId.NANO_HEADER.value + buf) - self.headers.append(nano_header) - return buf - - def get_funds_struct(self) -> bytes: - from hathorlib.headers import DeprecatedNanoHeader - struct_bytes = super().get_funds_struct() - nano_header_bytes = self._get_header(DeprecatedNanoHeader).serialize() - struct_bytes += nano_header_bytes[1:] - return struct_bytes - - def get_headers_hash(self) -> bytes: - return b'' - - def get_headers_struct(self) -> bytes: - return b'' diff --git a/hathorlib/hathorlib/nanocontracts/nc_types/collection_nc_type.py b/hathorlib/hathorlib/nanocontracts/nc_types/collection_nc_type.py index bb95a1f19..d556e641d 100644 --- a/hathorlib/hathorlib/nanocontracts/nc_types/collection_nc_type.py +++ b/hathorlib/hathorlib/nanocontracts/nc_types/collection_nc_type.py @@ -17,7 +17,7 @@ from abc import ABC, abstractmethod from collections import deque from collections.abc import Collection, Hashable, Iterable, Set -from typing import TypeVar, get_args, get_origin +from typing import TypeVar, cast, get_args, get_origin from typing_extensions import Self, override @@ -62,7 +62,7 @@ def _get_member_type(cls, type_: type[Collection[T]]) -> type[T]: args = get_args(type_) if not args or len(args) != 1: raise TypeError(f'expected {type_.__name__}[]') - return args[0] + return cast(type[T], args[0]) def _check_item(self, item: T) -> None: self._item._check_value(item, deep=True) @@ -136,7 +136,7 @@ def _get_member_type(cls, type_: type[Collection[T]]) -> type[T]: member_type, = args if not is_origin_hashable(args[0]): raise TypeError(f'{args[0]} is not hashable') - return member_type + return cast(type[T], member_type) @override def _check_item(self, item: H) -> None: diff --git a/hathorlib/hathorlib/nanocontracts/nc_types/sized_int_nc_type.py b/hathorlib/hathorlib/nanocontracts/nc_types/sized_int_nc_type.py index 42ed9d3de..5f2da424d 100644 --- a/hathorlib/hathorlib/nanocontracts/nc_types/sized_int_nc_type.py +++ b/hathorlib/hathorlib/nanocontracts/nc_types/sized_int_nc_type.py @@ -38,16 +38,16 @@ def _upper_bound_value(self) -> int | None: if self._byte_size is None: return None if self._signed: - return 2**(self._byte_size * 8 - 1) - 1 + return int(2**(self._byte_size * 8 - 1) - 1) else: - return 2**(self._byte_size * 8) - 1 + return int(2**(self._byte_size * 8) - 1) @classmethod def _lower_bound_value(self) -> int | None: if self._byte_size is None: return None if self._signed: - return -(2**(self._byte_size * 8 - 1)) + return int(-(2**(self._byte_size * 8 - 1))) else: return 0 diff --git a/hathorlib/hathorlib/nanocontracts/nc_types/utils.py b/hathorlib/hathorlib/nanocontracts/nc_types/utils.py index d6f23933a..b961eebfa 100644 --- a/hathorlib/hathorlib/nanocontracts/nc_types/utils.py +++ b/hathorlib/hathorlib/nanocontracts/nc_types/utils.py @@ -171,7 +171,7 @@ def _get_aliased_type(type_: type | UnionType, alias_map: TypeAliasMap) -> tuple aliased_origin: type replaced = False - if origin_type is Union: + if origin_type is Union: # type: ignore[comparison-overlap,unused-ignore] aliased_origin = UnionType elif origin_type in alias_map: aliased_origin = alias_map[origin_type] diff --git a/hathorlib/hathorlib/nanocontracts/nc_types/varint_nc_type.py b/hathorlib/hathorlib/nanocontracts/nc_types/varint_nc_type.py index 4a21819a9..43677504a 100644 --- a/hathorlib/hathorlib/nanocontracts/nc_types/varint_nc_type.py +++ b/hathorlib/hathorlib/nanocontracts/nc_types/varint_nc_type.py @@ -36,16 +36,16 @@ def _upper_bound_value(self) -> int | None: if self._max_byte_size is None: return None if self._signed: - return 2**(self._max_byte_size * 7 - 1) - 1 + return int(2**(self._max_byte_size * 7 - 1) - 1) else: - return 2**(self._max_byte_size * 7) - 1 + return int(2**(self._max_byte_size * 7) - 1) @classmethod def _lower_bound_value(self) -> int | None: if not self._signed: return 0 if self._max_byte_size is not None: - return -(2**(self._max_byte_size * 7)) + return int(-(2**(self._max_byte_size * 7))) else: return None diff --git a/hathorlib/hathorlib/nanocontracts/proxy_accessor.py b/hathorlib/hathorlib/nanocontracts/proxy_accessor.py new file mode 100644 index 000000000..1c5121176 --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/proxy_accessor.py @@ -0,0 +1,272 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Sequence, final + +from hathorlib.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ +from hathorlib.nanocontracts.types import BlueprintId, NCAction, NCArgs, NCFee, NCParsedArgs + +if TYPE_CHECKING: + # XXX: Temporary lazy import from hathor + from hathor.nanocontracts.runner import Runner # type: ignore[import-not-found] + + +@final +class ProxyAccessor(FauxImmutable): + """ + This class represents a "proxy instance", or a proxy accessor, during a blueprint method execution. + Calling custom blueprint methods on this class will forward the call to the actual wrapped blueprint via syscalls. + """ + __slots__ = ('__runner', '__blueprint_id') + + def __init__( + self, + *, + runner: Runner, + blueprint_id: BlueprintId, + ) -> None: + self.__runner: Runner + self.__blueprint_id: BlueprintId + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__blueprint_id', blueprint_id) + + def get_blueprint_id(self) -> BlueprintId: + """Return the blueprint id of this proxy.""" + return self.__blueprint_id + + def view(self) -> Any: + """Prepare a call to a proxy view method.""" + return PreparedProxyViewCall( + runner=self.__runner, + blueprint_id=self.__blueprint_id, + ) + + def public(self, *actions: NCAction, fees: Sequence[NCFee] | None = None, forbid_fallback: bool = False) -> Any: + """Prepare a proxy call to a public method.""" + return PreparedProxyPublicCall( + runner=self.__runner, + blueprint_id=self.__blueprint_id, + actions=actions, + fees=fees or (), + forbid_fallback=forbid_fallback, + ) + + def get_view_method(self, method_name: str) -> ProxyViewMethodAccessor: + """Get a proxy view method.""" + return ProxyViewMethodAccessor( + runner=self.__runner, + blueprint_id=self.__blueprint_id, + method_name=method_name, + ) + + def get_public_method( + self, + method_name: str, + *actions: NCAction, + fees: Sequence[NCFee] | None = None, + forbid_fallback: bool = False, + ) -> ProxyPublicMethodAccessor: + """Get a proxy public method.""" + return ProxyPublicMethodAccessor( + runner=self.__runner, + blueprint_id=self.__blueprint_id, + method_name=method_name, + actions=actions, + fees=fees or (), + forbid_fallback=forbid_fallback, + ) + + +@final +class PreparedProxyViewCall(FauxImmutable): + __slots__ = ('__runner', '__blueprint_id') + __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ + + def __init__(self, *, runner: Runner, blueprint_id: BlueprintId) -> None: + self.__runner: Runner + self.__blueprint_id: BlueprintId + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__blueprint_id', blueprint_id) + + def __getattr__(self, method_name: str) -> ProxyViewMethodAccessor: + return ProxyViewMethodAccessor( + runner=self.__runner, + blueprint_id=self.__blueprint_id, + method_name=method_name, + ) + + +@final +class PreparedProxyPublicCall(FauxImmutable): + __slots__ = ( + '__runner', + '__blueprint_id', + '__actions', + '__fees', + '__forbid_fallback', + '__is_dirty', + ) + __skip_faux_immutability_validation__ = True # Needed to implement __getattr__ + + def __init__( + self, + *, + runner: Runner, + blueprint_id: BlueprintId, + actions: Sequence[NCAction], + fees: Sequence[NCFee], + forbid_fallback: bool, + ) -> None: + self.__runner: Runner + self.__blueprint_id: BlueprintId + self.__actions: Sequence[NCAction] + self.__fees: Sequence[NCFee] + self.__forbid_fallback: bool + self.__is_dirty: bool + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__blueprint_id', blueprint_id) + __set_faux_immutable__(self, '__actions', actions) + __set_faux_immutable__(self, '__fees', fees) + __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) + __set_faux_immutable__(self, '__is_dirty', False) + + def __getattr__(self, method_name: str) -> ProxyPublicMethodAccessor: + from hathorlib.nanocontracts.exception import NCFail + if self.__is_dirty: + raise NCFail( + f'prepared proxy public method for blueprint `{self.__blueprint_id.hex()}` was already used, ' + f'you must use `public` on the proxy to call it again' + ) + + __set_faux_immutable__(self, '__is_dirty', True) + + return ProxyPublicMethodAccessor( + runner=self.__runner, + blueprint_id=self.__blueprint_id, + method_name=method_name, + actions=self.__actions, + fees=self.__fees, + forbid_fallback=self.__forbid_fallback, + ) + + +@final +class ProxyViewMethodAccessor(FauxImmutable): + """ + This class represents a "proxy view method", or a proxy view method accessor, during a blueprint method execution. + It's a callable that will forward the call to the actual wrapped blueprint via syscall. + It may be used multiple times to call the same method with different arguments. + """ + __slots__ = ('__runner', '__blueprint_id', '__method_name') + + def __init__(self, *, runner: Runner, blueprint_id: BlueprintId, method_name: str) -> None: + self.__runner: Runner + self.__blueprint_id: BlueprintId + self.__method_name: str + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__blueprint_id', blueprint_id) + __set_faux_immutable__(self, '__method_name', method_name) + + def call(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments. This is just an alias for calling the object directly.""" + return self(*args, **kwargs) + + def __call__(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments.""" + return self.__runner.syscall_proxy_call_view_method( + blueprint_id=self.__blueprint_id, + method_name=self.__method_name, + args=args, + kwargs=kwargs, + ) + + +@final +class ProxyPublicMethodAccessor(FauxImmutable): + """ + This class represents a "proxy public method", or a proxy public method accessor, during a blueprint method + execution. + It's a callable that will forward the call to the actual wrapped blueprint via syscall. + It can only be used once because it consumes the provided actions after a single use. + """ + __slots__ = ( + '__runner', + '__blueprint_id', + '__method_name', + '__actions', + '__fees', + '__forbid_fallback', + '__is_dirty', + ) + + def __init__( + self, + *, + runner: Runner, + blueprint_id: BlueprintId, + method_name: str, + actions: Sequence[NCAction], + fees: Sequence[NCFee], + forbid_fallback: bool, + ) -> None: + self.__runner: Runner + self.__blueprint_id: BlueprintId + self.__method_name: str + self.__actions: Sequence[NCAction] + self.__fees: Sequence[NCFee] + self.__forbid_fallback: bool + self.__is_dirty: bool + + __set_faux_immutable__(self, '__runner', runner) + __set_faux_immutable__(self, '__blueprint_id', blueprint_id) + __set_faux_immutable__(self, '__method_name', method_name) + __set_faux_immutable__(self, '__actions', actions) + __set_faux_immutable__(self, '__fees', fees) + __set_faux_immutable__(self, '__forbid_fallback', forbid_fallback) + __set_faux_immutable__(self, '__is_dirty', False) + + def call(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments. This is just an alias for calling the object directly.""" + return self(*args, **kwargs) + + def __call__(self, *args: Any, **kwargs: Any) -> object: + """Call the method with the provided arguments.""" + nc_args = NCParsedArgs(args, kwargs) + return self.call_with_nc_args(nc_args) + + def call_with_nc_args(self, nc_args: NCArgs) -> object: + """Call the method with the provided NCArgs.""" + from hathorlib.nanocontracts.exception import NCFail + if self.__is_dirty: + raise NCFail( + f'accessor for proxy public method `{self.__method_name}` was already used, ' + f'you must use `public`/`public_method` on the proxy to call it again' + ) + + __set_faux_immutable__(self, '__is_dirty', True) + + return self.__runner.syscall_proxy_call_public_method( + blueprint_id=self.__blueprint_id, + method_name=self.__method_name, + actions=self.__actions, + fees=self.__fees, + nc_args=nc_args, + forbid_fallback=self.__forbid_fallback, + ) diff --git a/hathorlib/hathorlib/nanocontracts/rng.py b/hathorlib/hathorlib/nanocontracts/rng.py new file mode 100644 index 000000000..efa965f9d --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/rng.py @@ -0,0 +1,95 @@ +# Copyright 2026 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Sequence, TypeVar, final + +from cryptography.hazmat.primitives.ciphers import Cipher, CipherContext, algorithms + +from hathorlib.nanocontracts.faux_immutable import FauxImmutable, __set_faux_immutable__ + +T = TypeVar('T') + + +@final +class NanoRNG(FauxImmutable): + """Implement a deterministic random number generator that will be used by the sorter. + + This implementation uses the ChaCha20 encryption as RNG. + """ + + __slots__ = ('__seed', '__encryptor') + + def __init__(self, seed: bytes) -> None: + self.__seed: bytes + self.__encryptor: CipherContext + if len(seed) != 32: + raise ValueError(f'seed has invalid length {len(seed)}, expected 32') + __set_faux_immutable__(self, '__seed', seed) + + key = self.__seed + nonce = self.__seed[:16] + + algorithm = algorithms.ChaCha20(key, nonce) + cipher = Cipher(algorithm, mode=None) + __set_faux_immutable__(self, '__encryptor', cipher.encryptor()) + + def randbytes(self, size: int) -> bytes: + """Return a random string of bytes.""" + assert size >= 1 + ciphertext = self.__encryptor.update(b'\0' * size) + assert len(ciphertext) == size + return ciphertext + + def randbits(self, bits: int) -> int: + """Return a random integer in the range [0, 2**bits).""" + assert bits >= 1 + size = (bits + 7) // 8 + ciphertext = self.randbytes(size) + x = int.from_bytes(ciphertext, byteorder='little', signed=False) + return x % (2**bits) + + def randbelow(self, n: int) -> int: + """Return a random integer in the range [0, n).""" + assert n >= 1 + k = n.bit_length() + r = self.randbits(k) # 0 <= r < 2**k + while r >= n: + r = self.randbits(k) + return r + + def randrange(self, start: int, stop: int, step: int = 1) -> int: + """Return a random integer in the range [start, stop) with a given step. + + Roughly equivalent to `choice(range(start, stop, step))` but supports arbitrarily large ranges.""" + assert stop > start + assert step >= 1 + qty = (stop - start + step - 1) // step + k = self.randbelow(qty) + return start + k * step + + def randint(self, a: int, b: int) -> int: + """Return a random integer in the range [a, b].""" + assert b >= a + return a + self.randbelow(b - a + 1) + + def choice(self, seq: Sequence[T]) -> T: + """Choose a random element from a non-empty sequence.""" + return seq[self.randbelow(len(seq))] + + def random(self) -> float: + """Return a random float in the range [0, 1).""" + # 2**53 is the maximum integer float can represent without loss of precision. + return self.randbits(53) / 2**53 diff --git a/hathorlib/hathorlib/nanocontracts/types.py b/hathorlib/hathorlib/nanocontracts/types.py index b40e7fa76..f4c288198 100644 --- a/hathorlib/hathorlib/nanocontracts/types.py +++ b/hathorlib/hathorlib/nanocontracts/types.py @@ -518,11 +518,10 @@ def __repr__(self) -> str: return f"NCRawArgs('{str(self)}')" def try_parse_as(self, arg_types: tuple[type, ...]) -> tuple[Any, ...] | None: - # mypy: disable-error-code="import-not-found" - from hathor.nanocontracts.method import ArgsOnly # type: ignore[import-not-found] + from hathorlib.nanocontracts.method import ArgsOnly try: args_parser = ArgsOnly.from_arg_types(arg_types) - return cast(tuple[Any, ...], args_parser.deserialize_args_bytes(self.args_bytes)) + return args_parser.deserialize_args_bytes(self.args_bytes) except (NCSerializationError, SerializationError, TypeError, ValueError): return None diff --git a/hathorlib/hathorlib/nanocontracts/utils.py b/hathorlib/hathorlib/nanocontracts/utils.py new file mode 100644 index 000000000..5e70340aa --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/utils.py @@ -0,0 +1,34 @@ +# Copyright 2025 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from collections.abc import Callable + +from hathorlib.nanocontracts.types import NC_METHOD_TYPE_ATTR, NCMethodType + + +def is_nc_public_method(method: Callable) -> bool: + """Return True if the method is nc_public.""" + return getattr(method, NC_METHOD_TYPE_ATTR, None) == NCMethodType.PUBLIC + + +def is_nc_view_method(method: Callable) -> bool: + """Return True if the method is nc_view.""" + return getattr(method, NC_METHOD_TYPE_ATTR, None) == NCMethodType.VIEW + + +def is_nc_fallback_method(method: Callable) -> bool: + """Return True if the method is nc_fallback.""" + return getattr(method, NC_METHOD_TYPE_ATTR, None) == NCMethodType.FALLBACK diff --git a/hathorlib/hathorlib/nanocontracts/vertex_data.py b/hathorlib/hathorlib/nanocontracts/vertex_data.py new file mode 100644 index 000000000..db227730b --- /dev/null +++ b/hathorlib/hathorlib/nanocontracts/vertex_data.py @@ -0,0 +1,83 @@ +# Copyright 2023 Hathor Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum, unique + +from hathorlib import TxVersion +from hathorlib.nanocontracts.types import TokenUid, VertexId + + +@dataclass(frozen=True, slots=True, kw_only=True) +class VertexData: + version: TxVersion + hash: bytes + nonce: int + signal_bits: int + weight: float + inputs: tuple[TxInputData, ...] + outputs: tuple[TxOutputData, ...] + tokens: tuple[TokenUid, ...] + parents: tuple[VertexId, ...] + headers: tuple[HeaderData, ...] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TxInputData: + tx_id: VertexId + index: int + data: bytes + info: TxOutputData | None + + +@unique +class ScriptType(StrEnum): + P2PKH = 'P2PKH' + MULTI_SIG = 'MultiSig' + + +@dataclass(slots=True, frozen=True, kw_only=True) +class ScriptInfo: + type: ScriptType + address: str + timelock: int | None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TxOutputData: + value: int + raw_script: bytes + parsed_script: ScriptInfo | None + token_data: int + + +@dataclass(frozen=True, slots=True, kw_only=True) +class BlockData: + hash: VertexId + timestamp: int + height: int + + +class HeaderData: + """Marker class, represents an arbitrary vertex-header.""" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class NanoHeaderData(HeaderData): + nc_seqnum: int + nc_id: VertexId + nc_method: str + nc_args_bytes: bytes diff --git a/hathorlib/pyproject.toml b/hathorlib/pyproject.toml index ddadbd0ee..ccd5348ee 100644 --- a/hathorlib/pyproject.toml +++ b/hathorlib/pyproject.toml @@ -86,7 +86,6 @@ module = [ check_untyped_defs = false disallow_untyped_defs = false - [tool.pytest.ini_options] minversion = "6.0" testpaths = ["tests"] diff --git a/hathorlib/tests/test_basic.py b/hathorlib/tests/test_basic.py index 0495ad3b5..d93e642aa 100644 --- a/hathorlib/tests/test_basic.py +++ b/hathorlib/tests/test_basic.py @@ -8,7 +8,7 @@ import unittest from hathorlib import Block, TokenCreationTransaction, Transaction -from hathorlib.base_transaction import tx_or_block_from_bytes +from hathorlib.base_transaction import get_cls_from_tx_version, tx_or_block_from_bytes from hathorlib.conf import HathorSettings from hathorlib.scripts import create_output_script from hathorlib.utils import decode_address @@ -257,9 +257,9 @@ def test_tx_version_and_signal_bits(self): # test get the correct class version = TxVersion(0x00) - self.assertEqual(version.get_cls(), Block) + self.assertEqual(get_cls_from_tx_version(version), Block) version = TxVersion(0x01) - self.assertEqual(version.get_cls(), Transaction) + self.assertEqual(get_cls_from_tx_version(version), Transaction) # test serialization doesn't mess up with signal_bits and version data = bytes.fromhex('f00001ffffffe8b789180000001976a9147fd4ae0e4fb2d2854e76d359029d8078bb9' diff --git a/hathorlib/tests/test_deprecated.py b/hathorlib/tests/test_deprecated.py deleted file mode 100644 index 1c402fa70..000000000 --- a/hathorlib/tests/test_deprecated.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest - -from hathorlib.base_transaction import tx_or_block_from_bytes -from hathorlib.nanocontracts import DeprecatedNanoContract - - -@pytest.mark.parametrize( - ['hex_hash', 'hex_bytes'], - [ - # Without actions - ( - '000081fc23f06c2e0198e92d88bae373b9291281eaa1bde70a25895e8f395ebe', - '0004000000013cb032600bdf7db784800e4ea911b10676fa2f67591f82bb62628c234e7715950a696e697469616c697a650024001' - '976a9145f6557d55ebd9b9f17ac6d3dec9e62c3983e0f9d88ac000100000467d3162d21038125cdd1ba7942439d1cca8a622ce046' - 'ba94549375f8125b166a4c9f9545a9044730450221009ce1c5bd1f53a3123bbce623fb3ce54460814bb8fba7bee3a2d147a6d32e0' - 'd87022066857e268dd8e84272543ab3e5ba9e8389155be5f289116df07e76a1204f33e24030f50e7c7b57cb67d308ce0200000744' - '71704d198d5ebcfa31bc281d69a1d900c0c197444386d7bdf5db13c4000016cfc9ea80a9faebd599af3eb1a6c50308e2c74e003c4' - 'a502b9bddbf639400ff68b3', - ), - - # With actions - ( - '0000540ff09eff4811932fd954f7e070c37a36428d73c931af243eff43bb970b', - '00040000010000012c00001976a9145f6557d55ebd9b9f17ac6d3dec9e62c3983e0f9d88ac010000049be6b42e863d93c304519e6' - 'fa2e1731529b0ca3958b1a2f36c869fbd5c087769746864726177000021038125cdd1ba7942439d1cca8a622ce046ba94549375f8' - '125b166a4c9f9545a9044730450221009d12fc897c1a78658c8448f0c5b733f8f0019c079c51ab5d0f1804f58a9f128502202d16b' - '85ae939d2d8e023dec0f466aa3cf62c4839ed1a82d54f191ac516bf21e9403105b214e1b93767db0afb02000026ff3dd377bfab1e' - '643caa5bc4b51981cead3a53389610f8b3eb6df89c300000006f1d0156981bc023f2e99c1d3ee653418ff2a2da5a187d07d8a7dfe' - '26900fbcd09' - ) - ] -) -def test_deprecated_nano_contract(hex_bytes: str, hex_hash: str) -> None: - tx_bytes = bytes.fromhex(hex_bytes) - expected_tx_hash = bytes.fromhex(hex_hash) - - tx = tx_or_block_from_bytes(tx_bytes) - assert isinstance(tx, DeprecatedNanoContract) - assert tx.hash == expected_tx_hash - assert bytes(tx) == tx_bytes