Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hathor/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def __init__(self) -> None:
self._enable_ipv6: bool = False
self._disable_ipv4: bool = False

self._nc_anti_mev: bool = False
self._nc_anti_mev: bool = True

self._nc_storage_factory: NCStorageFactory | None = None
self._nc_log_storage: NCLogStorage | None = None
Expand Down
4 changes: 2 additions & 2 deletions hathor/builder/cli_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ def create_manager(self, reactor: Reactor) -> HathorManager:
self.log.debug('enable nano indices')
tx_storage.indexes.enable_nc_indices()

from hathor.nanocontracts.sorter.timestamp_sorter import timestamp_nc_calls_sorter
nc_calls_sorter = timestamp_nc_calls_sorter
from hathor.nanocontracts.sorter.random_sorter import random_nc_calls_sorter
nc_calls_sorter = random_nc_calls_sorter

assert self.nc_storage_factory is not None
runner_factory = RunnerFactory(
Expand Down
3 changes: 2 additions & 1 deletion hathor/nanocontracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from hathor.nanocontracts.on_chain_blueprint import OnChainBlueprint
from hathor.nanocontracts.runner import Runner
from hathor.nanocontracts.storage import NCMemoryStorageFactory, NCRocksDBStorageFactory, NCStorageFactory
from hathor.nanocontracts.types import public, view
from hathor.nanocontracts.types import fallback, public, view

__all__ = [
'Blueprint',
Expand All @@ -30,5 +30,6 @@
'NCRocksDBStorageFactory',
'NCStorageFactory',
'public',
'fallback',
'view',
]
3 changes: 2 additions & 1 deletion hathor/nanocontracts/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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:
Expand Down Expand Up @@ -79,7 +80,7 @@ def __new__(cls, name, bases, attrs, **kwargs):
field = make_field_for_type(field_name, field_type)
except TypeError:
raise BlueprintSyntaxError(
f'unsupported field type `{field_type.__name__}` on field `{field_name}`'
f'unsupported field type: `{field_name}: {pretty_type(field_type)}`'
)
setattr(new_class, field_name, field)
else:
Expand Down
50 changes: 41 additions & 9 deletions hathor/nanocontracts/blueprint_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
from typing_extensions import deprecated

from hathor.nanocontracts.storage import NCContractStorage
from hathor.nanocontracts.types import BlueprintId, ContractId, NCAction, TokenUid
from hathor.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, TokenUid

if TYPE_CHECKING:
from hathor.nanocontracts.nc_exec_logs import NCLogger
from hathor.nanocontracts.rng import NanoRNG
from hathor.nanocontracts.runner import Runner
from hathor.nanocontracts.runner.types import NCArgs


class BlueprintEnvironment:
Expand All @@ -37,11 +38,14 @@ def __init__(
runner: Runner,
nc_logger: NCLogger,
storage: NCContractStorage,
*,
disable_cache: bool = False,
) -> None:
self.__log__ = nc_logger
self.__runner = runner
self.__storage__ = storage
self.__cache__: dict[str, Any] = {}
# XXX: we could replace dict|None with a Cache that can be disabled, cleared, limited, etc
self.__cache__: dict[str, Any] | None = None if disable_cache else {}

@final
@property
Expand All @@ -68,7 +72,7 @@ def get_balance(
token_uid: Optional[TokenUid] = None,
*,
contract_id: Optional[ContractId] = None,
) -> int:
) -> Amount:
"""
Return the balance for a given token before the current call, that is,
excluding any actions and changes in the current call.
Expand All @@ -84,35 +88,35 @@ def get_balance_before_current_call(
token_uid: Optional[TokenUid] = None,
*,
contract_id: Optional[ContractId] = None,
) -> int:
) -> 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."""
balance = self.__runner.get_balance_before_current_call(contract_id, token_uid)
return balance.value
return Amount(balance.value)

def get_current_balance(
self,
token_uid: Optional[TokenUid] = None,
*,
contract_id: Optional[ContractId] = None,
) -> int:
) -> 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(contract_id, token_uid)
return balance.value
return Amount(balance.value)

@final
def can_mint_before_current_call(
self,
token_uid: Optional[TokenUid] = None,
token_uid: TokenUid,
*,
contract_id: Optional[ContractId] = None,
) -> bool:
Expand Down Expand Up @@ -146,7 +150,7 @@ def can_mint(
@final
def can_melt_before_current_call(
self,
token_uid: Optional[TokenUid] = None,
token_uid: TokenUid,
*,
contract_id: Optional[ContractId] = None,
) -> bool:
Expand Down Expand Up @@ -189,6 +193,29 @@ def call_public_method(
"""Call a public method of another contract."""
return self.__runner.syscall_call_another_contract_public_method(nc_id, method_name, actions, args, kwargs)

@final
def proxy_call_public_method(
self,
blueprint_id: BlueprintId,
method_name: str,
actions: list[NCAction],
*args: Any,
**kwargs: Any,
) -> Any:
"""Execute a proxy call to a public method of another blueprint."""
return self.__runner.syscall_proxy_call_public_method(blueprint_id, method_name, actions, args, kwargs)

@final
def proxy_call_public_method_nc_args(
self,
blueprint_id: BlueprintId,
method_name: str,
actions: list[NCAction],
nc_args: NCArgs,
) -> Any:
"""Execute a proxy call to a public method of another blueprint."""
return self.__runner.syscall_proxy_call_public_method_nc_args(blueprint_id, method_name, actions, nc_args)

@final
def call_view_method(self, nc_id: ContractId, method_name: str, *args: Any, **kwargs: Any) -> Any:
"""Call a view method of another contract."""
Expand Down Expand Up @@ -243,3 +270,8 @@ def create_token(
mint_authority,
melt_authority,
)

@final
def change_blueprint(self, blueprint_id: BlueprintId) -> None:
"""Change the blueprint of this contract."""
self.__runner.syscall_change_blueprint(blueprint_id)
5 changes: 5 additions & 0 deletions hathor/nanocontracts/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ def actions(self) -> MappingProxyType[TokenUid, tuple[NCAction, ...]]:
"""Get a mapping of actions per token."""
return self.__actions

@property
def actions_list(self) -> list[NCAction]:
"""Get a list of all actions."""
return list(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)
Expand Down
12 changes: 6 additions & 6 deletions hathor/nanocontracts/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
from hathor.nanocontracts.fields.field import Field
from hathor.nanocontracts.fields.set_field import SetField
from hathor.nanocontracts.fields.utils import TypeToFieldMap
from hathor.nanocontracts.nc_types import DEFAULT_TYPE_ALIAS_MAP, DEFAULT_TYPE_TO_NC_TYPE_MAP
from hathor.nanocontracts.nc_types import DEFAULT_TYPE_ALIAS_MAP, FIELD_TYPE_TO_NC_TYPE_MAP
from hathor.nanocontracts.nc_types.utils import TypeAliasMap, TypeToNCTypeMap

__all__ = [
'DEFAULT_TYPE_TO_FIELD_MAP',
'TYPE_TO_FIELD_MAP',
'DequeField',
'DictField',
'Field',
Expand All @@ -35,12 +35,12 @@

T = TypeVar('T')

DEFAULT_TYPE_TO_FIELD_MAP: TypeToFieldMap = {
TYPE_TO_FIELD_MAP: TypeToFieldMap = {
dict: DictField,
list: DequeField, # XXX: we should really make a ListField, a deque is different from a list
set: SetField,
deque: DequeField,
# XXX: other types fallback to DEFAULT_TYPE_TO_NC_TYPE_MAP
# XXX: other types fallback to FIELD_TYPE_TO_NC_TYPE_MAP
}


Expand All @@ -49,8 +49,8 @@ def make_field_for_type(
type_: type[T],
/,
*,
type_field_map: TypeToFieldMap = DEFAULT_TYPE_TO_FIELD_MAP,
type_nc_type_map: TypeToNCTypeMap = DEFAULT_TYPE_TO_NC_TYPE_MAP,
type_field_map: TypeToFieldMap = TYPE_TO_FIELD_MAP,
type_nc_type_map: TypeToNCTypeMap = FIELD_TYPE_TO_NC_TYPE_MAP,
type_alias_map: TypeAliasMap = DEFAULT_TYPE_ALIAS_MAP,
) -> Field[T]:
""" Like Field.from_name_and_type, but with default maps.
Expand Down
6 changes: 4 additions & 2 deletions hathor/nanocontracts/fields/container_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ def __set__(self, instance: Blueprint, value: T) -> None:

@override
def __get__(self, instance: Blueprint, owner: object | None = None) -> T:
if obj := instance.syscall.__cache__.get(self.__name):
cache = instance.syscall.__cache__
if cache is not None and (obj := cache.get(self.__name)):
return obj

# XXX: ideally we would instantiate the storage within _from_name_and_type, but we need the blueprint instance
Expand All @@ -120,7 +121,8 @@ def __get__(self, instance: Blueprint, owner: object | None = None) -> T:
self.__type,
type_map=self.__type_map,
)
instance.syscall.__cache__[self.__name] = storage
if cache is not None:
cache[self.__name] = storage
return storage

@override
Expand Down
7 changes: 4 additions & 3 deletions hathor/nanocontracts/fields/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ def from_name_and_type(name: str, type_: type[T], /, *, type_map: TypeMap) -> Fi
if origin_type in type_map.fields_map:
field_class = type_map.fields_map[origin_type]
return field_class._from_name_and_type(name, type_, type_map=type_map)
elif NCType.is_supported(origin_type, type_map=type_map.to_nc_type_map()):
return NCTypeField._from_name_and_type(name, type_, type_map=type_map)
else:
raise TypeError(f'type {type_} is not supported by any Field class')
try:
return NCTypeField._from_name_and_type(name, type_, type_map=type_map)
except TypeError as e:
raise TypeError(f'type {type_} is not supported by any Field class') from e

@classmethod
@abstractmethod
Expand Down
12 changes: 8 additions & 4 deletions hathor/nanocontracts/fields/nc_type_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,19 @@ def __storage_key(self) -> bytes:

def __set__(self, instance: Blueprint, obj: T) -> None:
instance.syscall.__storage__.put_obj(self.__storage_key(), self.__nc_type, obj)
instance.syscall.__cache__[self.__name] = obj
cache = instance.syscall.__cache__
if cache is not None:
cache[self.__name] = obj

def __get__(self, instance: Blueprint, owner: object | None = None) -> T:
if self.__name in instance.syscall.__cache__:
return instance.syscall.__cache__[self.__name]
cache = instance.syscall.__cache__
if cache is not None and self.__name in cache:
return cache[self.__name]

try:
obj = instance.syscall.__storage__.get_obj(self.__storage_key(), self.__nc_type)
instance.syscall.__cache__[self.__name] = obj
if cache is not None:
cache[self.__name] = obj
return obj
except KeyError:
raise AttributeError(f'Contract has no attribute \'{self.__name}\'')
Expand Down
20 changes: 7 additions & 13 deletions hathor/nanocontracts/method.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,16 @@
from hathor.nanocontracts import Context
from hathor.nanocontracts.exception import NCFail, NCSerializationArgTooLong, NCSerializationError
from hathor.nanocontracts.nc_types import (
ESSENTIAL_TYPE_ALIAS_MAP,
EXTENDED_TYPE_TO_NC_TYPE_MAP,
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()
_TYPE_MAP = NCType.TypeMap(ESSENTIAL_TYPE_ALIAS_MAP, EXTENDED_TYPE_TO_NC_TYPE_MAP)
T = TypeVar('T')

MAX_BYTES_SERIALIZED_ARG: int = 1000
Expand Down Expand Up @@ -65,10 +64,6 @@ def _serialize_map_exception(nc_type: NCType[T], value: T) -> bytes:
return bytes(serializer.finalize())


def _make_nc_type_for_type(type_: type[T], /) -> NCType[T]:
return NCType.from_type(type_, type_map=_TYPE_MAP)


class _ArgsNCType(NCType):
""" Inner implementation of a callable "args" using the NCType model.
"""
Expand Down Expand Up @@ -145,7 +140,7 @@ def __init__(self, args_nc_type: _ArgsNCType) -> None:
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_type(arg_type))
args_nc_types.append(make_nc_type_for_arg_type(arg_type))

return cls(_ArgsNCType(args_nc_types, max_bytes=MAX_BYTES_SERIALIZED_ARG))

Expand All @@ -169,9 +164,8 @@ class Method:
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.

Differently from the default map used by `make_nc_type_for_type`, this class uses the extended map, which means
that when the method's type signature requests a `dict`, it will produce a `dict` instead of a `mappingproxy` when
deserilizing. This is makes sense for values used on method calls.
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`.
"""
args: _ArgsNCType
return_: NCType
Expand Down Expand Up @@ -250,11 +244,11 @@ def from_callable(cls, method: Callable) -> Self:
# XXX: this can (and probably will) be implemented in the future
if param.default is not EMPTY:
raise TypeError('default values are not supported')
args_nc_types.append(_make_nc_type_for_type(param.annotation))
args_nc_types.append(make_nc_type_for_arg_type(param.annotation))

return cls(
_ArgsNCType(args_nc_types, max_bytes=MAX_BYTES_SERIALIZED_ARG),
_make_nc_type_for_type(method_signature.return_annotation),
make_nc_type_for_return_type(method_signature.return_annotation),
)

def serialize_args_bytes(self, args: tuple[Any, ...] | list[Any]) -> bytes:
Expand Down
Loading