diff --git a/hathor/nanocontracts/context.py b/hathor/nanocontracts/context.py index 2002472fa..6b94b3ba6 100644 --- a/hathor/nanocontracts/context.py +++ b/hathor/nanocontracts/context.py @@ -17,11 +17,11 @@ from collections import defaultdict from itertools import chain from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Sequence, final +from typing import TYPE_CHECKING, Any, Sequence, assert_never, final from hathor.crypto.util import get_address_b58_from_bytes from hathor.nanocontracts.exception import NCFail, NCInvalidContext -from hathor.nanocontracts.types import Address, ContractId, NCAction, TokenUid +from hathor.nanocontracts.types import Address, CallerId, ContractId, NCAction, TokenUid from hathor.nanocontracts.vertex_data import VertexData from hathor.transaction.exceptions import TxValidationError @@ -39,9 +39,9 @@ class Context: 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', '__address', '__vertex', '__timestamp', '__all_actions__') + __slots__ = ('__actions', '__caller_id', '__vertex', '__timestamp', '__all_actions__') __actions: MappingProxyType[TokenUid, tuple[NCAction, ...]] - __address: Address | ContractId + __caller_id: CallerId __vertex: VertexData __timestamp: int @@ -49,7 +49,7 @@ def __init__( self, actions: Sequence[NCAction], vertex: BaseTransaction | VertexData, - address: Address | ContractId, + caller_id: CallerId, timestamp: int, ) -> None: # Dict of action where the key is the token_uid. @@ -77,7 +77,7 @@ def __init__( self.__vertex = VertexData.create_from_vertex(vertex) # Address calling the method. - self.__address = address + self.__caller_id = caller_id # Timestamp of the first block confirming tx. self.__timestamp = timestamp @@ -87,8 +87,29 @@ def vertex(self) -> VertexData: return self.__vertex @property - def address(self) -> Address | ContractId: - return self.__address + 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 _: + 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 _: + assert_never(self.caller_id) @property def timestamp(self) -> int: @@ -116,7 +137,7 @@ def copy(self) -> Context: return Context( actions=list(self.__all_actions__), vertex=self.vertex, - address=self.address, + caller_id=self.caller_id, timestamp=self.timestamp, ) @@ -124,6 +145,6 @@ def to_json(self) -> dict[str, Any]: """Return a JSON representation of the context.""" return { 'actions': [action.to_json() for action in self.__all_actions__], - 'address': get_address_b58_from_bytes(self.address), + 'caller_id': get_address_b58_from_bytes(self.caller_id), 'timestamp': self.timestamp, } diff --git a/hathor/nanocontracts/nc_types/__init__.py b/hathor/nanocontracts/nc_types/__init__.py index f416d8321..442891305 100644 --- a/hathor/nanocontracts/nc_types/__init__.py +++ b/hathor/nanocontracts/nc_types/__init__.py @@ -19,6 +19,7 @@ from hathor.nanocontracts.nc_types.address_nc_type import AddressNCType from hathor.nanocontracts.nc_types.bool_nc_type import BoolNCType from hathor.nanocontracts.nc_types.bytes_nc_type import BytesLikeNCType, BytesNCType +from hathor.nanocontracts.nc_types.caller_id_nc_type import CallerIdNCType from hathor.nanocontracts.nc_types.collection_nc_type import DequeNCType, FrozenSetNCType, ListNCType, SetNCType from hathor.nanocontracts.nc_types.dataclass_nc_type import DataclassNCType from hathor.nanocontracts.nc_types.fixed_size_bytes_nc_type import Bytes32NCType @@ -56,6 +57,7 @@ 'BoolNCType', 'BytesLikeNCType', 'BytesNCType', + 'CallerIdNCType', 'DataclassNCType', 'DequeNCType', 'DictNCType', @@ -124,6 +126,7 @@ TxOutputScript: BytesLikeNCType[TxOutputScript], VertexId: Bytes32NCType, SignedData: SignedDataNCType, + (Address, ContractId): CallerIdNCType, } # This mapping includes all supported NCType classes, should only be used for parsing function calls diff --git a/hathor/nanocontracts/nc_types/caller_id_nc_type.py b/hathor/nanocontracts/nc_types/caller_id_nc_type.py new file mode 100644 index 000000000..5e3dee442 --- /dev/null +++ b/hathor/nanocontracts/nc_types/caller_id_nc_type.py @@ -0,0 +1,115 @@ +# 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 types import UnionType +from typing import _UnionGenericAlias as UnionGenericAlias, assert_never, get_args # type: ignore[attr-defined] + +from typing_extensions import Self, override + +from hathor.crypto.util import decode_address, get_address_b58_from_bytes +from hathor.nanocontracts.nc_types.nc_type import NCType +from hathor.nanocontracts.types import Address, CallerId, ContractId +from hathor.serialization import Deserializer, Serializer +from hathor.serialization.compound_encoding.caller_id import decode_caller_id, encode_caller_id +from hathor.transaction.base_transaction import TX_HASH_SIZE +from hathor.transaction.headers.nano_header import ADDRESS_LEN_BYTES + + +class CallerIdNCType(NCType[CallerId]): + """Represents `CallerID` values, which can be `Address` or `ContractId`.""" + __slots__ = () + _is_hashable = True + + @override + @classmethod + def _from_type(cls, type_: type[Address] | type[ContractId], /, *, type_map: NCType.TypeMap) -> Self: + if not isinstance(type_, (UnionType, UnionGenericAlias)): + raise TypeError('expected type union') + args = get_args(type_) + assert args, 'union always has args' + if len(args) != 2 or Address not in args or ContractId not in args: + raise TypeError('type must be either `Address | ContractId` or `ContractId | Address`') + return cls() + + @override + def _check_value(self, value: CallerId, /, *, deep: bool) -> None: + match value: + case Address(): + if len(value) != ADDRESS_LEN_BYTES: + raise ValueError(f'an address must always have {ADDRESS_LEN_BYTES} bytes') + case ContractId(): + if len(value) != TX_HASH_SIZE: + raise ValueError(f'an contract id must always have {TX_HASH_SIZE} bytes') + case _: + assert_never(value) + + @override + def _serialize(self, serializer: Serializer, value: CallerId, /) -> None: + encode_caller_id(serializer, value) + + @override + def _deserialize(self, deserializer: Deserializer, /) -> CallerId: + return decode_caller_id(deserializer) + + @override + def _json_to_value(self, json_value: NCType.Json, /) -> CallerId: + """ + >>> nc_type = CallerIdNCType() + >>> value = nc_type.json_to_value('HH5As5aLtzFkcbmbXZmE65wSd22GqPWq2T') + >>> isinstance(value, Address) + True + >>> value == Address(bytes.fromhex('2873c0a326af979a12be89ee8a00e8871c8e2765022e9b803c')) + True + >>> contract_id = ContractId(b'\x11' * 32) + >>> value = nc_type.json_to_value(contract_id.hex()) + >>> isinstance(value, ContractId) + True + >>> value == contract_id + True + >>> nc_type.json_to_value('foo') + Traceback (most recent call last): + ... + ValueError: cannot decode "foo" as CallerId + """ + if not isinstance(json_value, str): + raise ValueError('expected str') + + if len(json_value) == 34: + return Address(decode_address(json_value)) + + if len(json_value) == TX_HASH_SIZE * 2: + return ContractId(bytes.fromhex(json_value)) + + raise ValueError(f'cannot decode "{json_value}" as CallerId') + + @override + def _value_to_json(self, value: CallerId, /) -> NCType.Json: + """ + >>> nc_type = CallerIdNCType() + >>> address = Address(bytes.fromhex('2873c0a326af979a12be89ee8a00e8871c8e2765022e9b803c')) + >>> nc_type.value_to_json(address) + 'HH5As5aLtzFkcbmbXZmE65wSd22GqPWq2T' + >>> contract_id = ContractId(b'\x11' * 32) + >>> nc_type.value_to_json(contract_id) + '1111111111111111111111111111111111111111111111111111111111111111' + """ + match value: + case Address(): + return get_address_b58_from_bytes(value) + case ContractId(): + return value.hex() + case _: + assert_never(value) diff --git a/hathor/nanocontracts/nc_types/utils.py b/hathor/nanocontracts/nc_types/utils.py index 48cc9a309..0e8799dd3 100644 --- a/hathor/nanocontracts/nc_types/utils.py +++ b/hathor/nanocontracts/nc_types/utils.py @@ -33,7 +33,7 @@ T = TypeVar('T') TypeAliasMap: TypeAlias = Mapping[type | UnionType, type] -TypeToNCTypeMap: TypeAlias = Mapping[type | UnionType, type['NCType']] +TypeToNCTypeMap: TypeAlias = Mapping[type | UnionType | tuple[type, ...], type['NCType']] def get_origin_classes(type_: type) -> Iterator[type]: @@ -218,7 +218,7 @@ def get_usable_origin_type( *, type_map: 'NCType.TypeMap', _verbose: bool = True, -) -> type: +) -> type | tuple[type, ...]: """ The purpose of this function is to map a given type into a type that is usable in a NCType.TypeMap It takes into account type-aliasing according to NCType.TypeMap.alias_map. If the given type cannot be used in the @@ -243,7 +243,16 @@ def get_usable_origin_type( # if we have a `dict[int, int]` we use `get_origin()` to get the `dict` part, since it's a different instance aliased_type: type = get_aliased_type(type_, type_map.alias_map, _verbose=_verbose) - origin_aliased_type: type = get_origin(aliased_type) or aliased_type + origin_aliased_type: type | tuple[type, ...] = get_origin(aliased_type) or aliased_type + + if origin_aliased_type is UnionType: + # When it's an union and None is not in it, it's not Optional, + # so we must index by args which is a tuple of types. + # This is done for support of specific union types such as CallerId (Address | ContractId) + args = get_args(aliased_type) + assert args is not None + if NoneType not in args: + origin_aliased_type = args if origin_aliased_type in type_map.nc_types_map: return origin_aliased_type diff --git a/hathor/nanocontracts/runner/runner.py b/hathor/nanocontracts/runner/runner.py index a142c93c0..ebccae7a5 100644 --- a/hathor/nanocontracts/runner/runner.py +++ b/hathor/nanocontracts/runner/runner.py @@ -410,7 +410,7 @@ def _unsafe_call_another_contract_public_method( ctx = Context( actions=actions, vertex=first_ctx.vertex, - address=last_call_record.contract_id, + caller_id=last_call_record.contract_id, timestamp=first_ctx.timestamp, ) return self._execute_public_method_call( diff --git a/hathor/nanocontracts/types.py b/hathor/nanocontracts/types.py index f0876edd4..115c68c5f 100644 --- a/hathor/nanocontracts/types.py +++ b/hathor/nanocontracts/types.py @@ -31,15 +31,26 @@ from hathor.transaction.util import bytes_to_int, int_to_bytes from hathor.utils.typing import InnerTypeMixin + # Types to be used by blueprints. -Address = NewType('Address', bytes) +class Address(bytes): + __slots__ = () + + +class VertexId(bytes): + __slots__ = () + + +class ContractId(VertexId): + __slots__ = () + + Amount = NewType('Amount', int) Timestamp = NewType('Timestamp', int) TokenUid = NewType('TokenUid', bytes) TxOutputScript = NewType('TxOutputScript', bytes) -VertexId = NewType('VertexId', bytes) BlueprintId = NewType('BlueprintId', VertexId) -ContractId = NewType('ContractId', VertexId) +CallerId: TypeAlias = Address | ContractId T = TypeVar('T') diff --git a/hathor/serialization/compound_encoding/caller_id.py b/hathor/serialization/compound_encoding/caller_id.py new file mode 100644 index 000000000..0b29fa24a --- /dev/null +++ b/hathor/serialization/compound_encoding/caller_id.py @@ -0,0 +1,80 @@ +# 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. + +r""" +A caller ID union type is encoded with a single byte identifier followed by the encoded value according to the type. + +Layout: + + [0x00][address] when Address + [0x01][contract_id] when ContractId + +>>> from hathor.nanocontracts.types import Address, ContractId +>>> se = Serializer.build_bytes_serializer() +>>> addr = Address(b'\x11' * 25) +>>> encode_caller_id(se, addr) +>>> bytes(se.finalize()).hex() +'0011111111111111111111111111111111111111111111111111' + +>>> se = Serializer.build_bytes_serializer() +>>> contract_id = ContractId(b'\x22' * 32) +>>> encode_caller_id(se, contract_id) +>>> bytes(se.finalize()).hex() +'012222222222222222222222222222222222222222222222222222222222222222' + +>>> de = Deserializer.build_bytes_deserializer(bytes.fromhex('0011111111111111111111111111111111111111111111111111')) +>>> result = decode_caller_id(de) +>>> isinstance(result, Address) +True +>>> de.finalize() + +>>> value = bytes.fromhex('012222222222222222222222222222222222222222222222222222222222222222') +>>> de = Deserializer.build_bytes_deserializer(value) +>>> result = decode_caller_id(de) +>>> isinstance(result, ContractId) +True +>>> de.finalize() +""" + +from typing import assert_never + +from hathor.nanocontracts.types import Address, CallerId, ContractId +from hathor.serialization import Deserializer, Serializer +from hathor.serialization.encoding.bool import decode_bool, encode_bool + +from ...transaction.base_transaction import TX_HASH_SIZE +from ...transaction.headers.nano_header import ADDRESS_LEN_BYTES + + +def encode_caller_id(serializer: Serializer, value: CallerId) -> None: + match value: + case Address(): + assert len(value) == ADDRESS_LEN_BYTES + encode_bool(serializer, False) + case ContractId(): + assert len(value) == TX_HASH_SIZE + encode_bool(serializer, True) + case _: + assert_never(value) + serializer.write_bytes(value) + + +def decode_caller_id(deserializer: Deserializer) -> CallerId: + is_contract = decode_bool(deserializer) + if is_contract: + data = bytes(deserializer.read_bytes(TX_HASH_SIZE)) + return ContractId(data) + else: + data = bytes(deserializer.read_bytes(ADDRESS_LEN_BYTES)) + return Address(data) diff --git a/hathor/transaction/headers/nano_header.py b/hathor/transaction/headers/nano_header.py index 709df3031..4d15f96c8 100644 --- a/hathor/transaction/headers/nano_header.py +++ b/hathor/transaction/headers/nano_header.py @@ -324,7 +324,7 @@ def get_context(self) -> Context: context = Context( actions=action_list, vertex=self.tx, - address=Address(self.nc_address), + caller_id=Address(self.nc_address), timestamp=timestamp, ) return context diff --git a/tests/nanocontracts/blueprints/test_swap_demo.py b/tests/nanocontracts/blueprints/test_swap_demo.py index 059282990..adea38690 100644 --- a/tests/nanocontracts/blueprints/test_swap_demo.py +++ b/tests/nanocontracts/blueprints/test_swap_demo.py @@ -37,7 +37,7 @@ def _initialize( context = Context( actions=[deposit_a, deposit_b], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) @@ -68,7 +68,7 @@ def _swap( context = Context( actions=[swap_a, swap_b], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) diff --git a/tests/nanocontracts/blueprints/unittest.py b/tests/nanocontracts/blueprints/unittest.py index 972b03276..7a4ab9362 100644 --- a/tests/nanocontracts/blueprints/unittest.py +++ b/tests/nanocontracts/blueprints/unittest.py @@ -178,6 +178,6 @@ def create_context( return Context( actions=actions if actions is not None else [], vertex=vertex or self.get_genesis_tx(), - address=address or self.gen_random_address(), + caller_id=address or self.gen_random_address(), timestamp=timestamp or self.now, ) diff --git a/tests/nanocontracts/test_allowed_actions.py b/tests/nanocontracts/test_allowed_actions.py index 0841bd2bb..ff7349ce3 100644 --- a/tests/nanocontracts/test_allowed_actions.py +++ b/tests/nanocontracts/test_allowed_actions.py @@ -88,7 +88,7 @@ def _get_context(self, *actions: NCAction) -> Context: return Context( actions=list(actions), vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now, ) diff --git a/tests/nanocontracts/test_authorities_call_another.py b/tests/nanocontracts/test_authorities_call_another.py index e4bec4867..27525993a 100644 --- a/tests/nanocontracts/test_authorities_call_another.py +++ b/tests/nanocontracts/test_authorities_call_another.py @@ -127,13 +127,13 @@ def _initialize(self, caller_actions: list[NCAction] | None = None) -> None: caller_ctx = Context( actions=caller_actions or [], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) callee_ctx = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.create_contract(self.caller_id, self.caller_blueprint_id, caller_ctx, other_id=self.callee_id) @@ -145,7 +145,7 @@ def _grant_to_other(self, *, mint: bool, melt: bool) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method( @@ -156,7 +156,7 @@ def _revoke_from_self(self, contract_id: ContractId, *, actions: list[NCAction], context = Context( actions=actions, vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method( @@ -167,7 +167,7 @@ def _revoke_from_other(self, *, mint: bool, melt: bool) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method( @@ -219,7 +219,7 @@ def test_acquire_mint(self) -> None: context = Context( actions=[NCGrantAuthorityAction(token_uid=self.token_a, mint=True, melt=False)], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method(self.callee_id, 'nop', context) @@ -229,7 +229,7 @@ def test_acquire_mint(self) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method( @@ -244,7 +244,7 @@ def test_acquire_melt(self) -> None: context = Context( actions=[NCGrantAuthorityAction(token_uid=self.token_a, mint=False, melt=True)], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method(self.callee_id, 'nop', context) @@ -254,7 +254,7 @@ def test_acquire_melt(self) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method( @@ -284,7 +284,7 @@ def test_revoke_then_grant_same_call_another_contract(self) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method(self.caller_id, 'call_grant_all_to_other_then_revoke', context, self.token_a) @@ -296,7 +296,7 @@ def test_grant_then_revoke_same_call_another_contract(self) -> None: context = Context( actions=[NCGrantAuthorityAction(token_uid=self.token_a, mint=True, melt=True)], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.call_public_method(self.caller_id, 'call_revoke_all_from_other', context, self.token_a) diff --git a/tests/nanocontracts/test_blueprint_syntax.py b/tests/nanocontracts/test_blueprint_syntax.py index ba651d633..6059eacad 100644 --- a/tests/nanocontracts/test_blueprint_syntax.py +++ b/tests/nanocontracts/test_blueprint_syntax.py @@ -31,7 +31,7 @@ def setUp(self) -> None: self.ctx = Context( actions=[], vertex=self.get_genesis_tx(), - address=Address(self.gen_random_address()), + caller_id=Address(self.gen_random_address()), timestamp=self.now, ) diff --git a/tests/nanocontracts/test_blueprints/bet.py b/tests/nanocontracts/test_blueprints/bet.py index fe81332b8..69aa94abf 100644 --- a/tests/nanocontracts/test_blueprints/bet.py +++ b/tests/nanocontracts/test_blueprints/bet.py @@ -191,7 +191,9 @@ def withdraw(self, ctx: Context) -> None: assert isinstance(action, NCWithdrawalAction) self.fail_if_result_is_not_available() self.fail_if_invalid_token(action) - address = Address(ctx.address) + caller_address = ctx.get_caller_address() + assert caller_address is not None + address = Address(caller_address) allowed = self.get_max_withdrawal(address) if action.amount > allowed: raise InsufficientBalance(f'withdrawal amount is greater than available (max: {allowed})') diff --git a/tests/nanocontracts/test_caller_id.py b/tests/nanocontracts/test_caller_id.py new file mode 100644 index 000000000..73aade0ef --- /dev/null +++ b/tests/nanocontracts/test_caller_id.py @@ -0,0 +1,90 @@ +# 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 hathor.nanocontracts import Blueprint, Context, public +from hathor.nanocontracts.types import Address, BlueprintId, CallerId, ContractId +from tests.nanocontracts.blueprints.unittest import BlueprintTestCase + + +class MyBlueprint(Blueprint): + address: Address | None + contract_id: ContractId | None + caller_id: CallerId + + @public + def initialize(self, ctx: Context) -> None: + self.caller_id = ctx.caller_id + + if address := ctx.get_caller_address(): + self.address = address + self.contract_id = None + elif contract_id := ctx.get_caller_contract_id(): + self.address = None + self.contract_id = contract_id + else: + raise AssertionError + + @public + def create_another(self, ctx: Context, blueprint_id: BlueprintId) -> ContractId: + contract_id, _ = self.syscall.create_contract(blueprint_id, b'1', []) + return contract_id + + @public + def test_args_and_return(self, ctx: Context, caller_id: CallerId) -> CallerId: + return caller_id + + +class TestCallerId(BlueprintTestCase): + def setUp(self) -> None: + super().setUp() + self.blueprint_id = self._register_blueprint_class(MyBlueprint) + self.contract_id1 = self.gen_random_contract_id() + + def test_callers(self) -> None: + address = self.gen_random_address() + ctx = self.create_context(address=address) + self.runner.create_contract(self.contract_id1, self.blueprint_id, ctx) + contract1 = self.get_readonly_contract(self.contract_id1) + + # Caller is an address (a tx) + assert isinstance(contract1, MyBlueprint) + assert contract1.address == address + assert contract1.contract_id is None + assert contract1.caller_id == address + + contract_id2 = self.runner.call_public_method(self.contract_id1, 'create_another', ctx, self.blueprint_id) + contract2 = self.get_readonly_contract(contract_id2) + + # Caller is another contract + assert isinstance(contract2, MyBlueprint) + assert contract2.address is None + assert contract2.contract_id == self.contract_id1 + assert contract2.caller_id == self.contract_id1 + + def test_args_and_return(self) -> None: + self.runner.create_contract(self.contract_id1, self.blueprint_id, self.create_context()) + + # Receive and return an address + address = self.gen_random_address() + ret = self.runner.call_public_method( + self.contract_id1, 'test_args_and_return', self.create_context(), address + ) + assert ret == address + + # Receive and return a contract id + contract_id = self.gen_random_contract_id() + ret = self.runner.call_public_method( + self.contract_id1, 'test_args_and_return', self.create_context(), contract_id + ) + assert ret == contract_id diff --git a/tests/nanocontracts/test_contract_upgrade.py b/tests/nanocontracts/test_contract_upgrade.py index 909947aef..c06b43f88 100644 --- a/tests/nanocontracts/test_contract_upgrade.py +++ b/tests/nanocontracts/test_contract_upgrade.py @@ -113,7 +113,7 @@ def test_basic(self) -> None: tx = self.get_genesis_tx() address = self.gen_random_address() - ctx = Context(actions=[], vertex=tx, address=address, timestamp=0) + ctx = Context(actions=[], vertex=tx, caller_id=address, timestamp=0) self.runner.create_contract(code1_id, self.code1_bp_id, ctx) self.runner.create_contract(code2_id, self.code2_bp_id, ctx) diff --git a/tests/nanocontracts/test_execution_order.py b/tests/nanocontracts/test_execution_order.py index 4d8d83c3d..0252d0429 100644 --- a/tests/nanocontracts/test_execution_order.py +++ b/tests/nanocontracts/test_execution_order.py @@ -140,7 +140,7 @@ def _get_context(self, *actions: NCAction) -> Context: return Context( actions=list(actions), vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now, ) diff --git a/tests/nanocontracts/test_exposed_properties.py b/tests/nanocontracts/test_exposed_properties.py index 2c6718f65..9d3afe336 100644 --- a/tests/nanocontracts/test_exposed_properties.py +++ b/tests/nanocontracts/test_exposed_properties.py @@ -47,8 +47,10 @@ 'hathor.nanocontracts.blueprint.Blueprint.syscall', 'hathor.nanocontracts.context.Context.actions', 'hathor.nanocontracts.context.Context.actions_list', - 'hathor.nanocontracts.context.Context.address', + 'hathor.nanocontracts.context.Context.caller_id', 'hathor.nanocontracts.context.Context.copy', + 'hathor.nanocontracts.context.Context.get_caller_address', + 'hathor.nanocontracts.context.Context.get_caller_contract_id', 'hathor.nanocontracts.context.Context.get_single_action', 'hathor.nanocontracts.context.Context.some_new_attribute', 'hathor.nanocontracts.context.Context.timestamp', @@ -58,10 +60,94 @@ 'hathor.nanocontracts.exception.NCFail.args', 'hathor.nanocontracts.exception.NCFail.some_new_attribute', 'hathor.nanocontracts.exception.NCFail.with_traceback', + 'hathor.nanocontracts.types.Address.capitalize', + 'hathor.nanocontracts.types.Address.center', + 'hathor.nanocontracts.types.Address.count', + 'hathor.nanocontracts.types.Address.decode', + 'hathor.nanocontracts.types.Address.endswith', + 'hathor.nanocontracts.types.Address.expandtabs', + 'hathor.nanocontracts.types.Address.find', + 'hathor.nanocontracts.types.Address.fromhex', + 'hathor.nanocontracts.types.Address.hex', + 'hathor.nanocontracts.types.Address.index', + 'hathor.nanocontracts.types.Address.isalnum', + 'hathor.nanocontracts.types.Address.isalpha', + 'hathor.nanocontracts.types.Address.isascii', + 'hathor.nanocontracts.types.Address.isdigit', + 'hathor.nanocontracts.types.Address.islower', + 'hathor.nanocontracts.types.Address.isspace', + 'hathor.nanocontracts.types.Address.istitle', + 'hathor.nanocontracts.types.Address.isupper', + 'hathor.nanocontracts.types.Address.join', + 'hathor.nanocontracts.types.Address.ljust', + 'hathor.nanocontracts.types.Address.lower', + 'hathor.nanocontracts.types.Address.lstrip', + 'hathor.nanocontracts.types.Address.maketrans', + 'hathor.nanocontracts.types.Address.partition', + 'hathor.nanocontracts.types.Address.removeprefix', + 'hathor.nanocontracts.types.Address.removesuffix', + 'hathor.nanocontracts.types.Address.replace', + 'hathor.nanocontracts.types.Address.rfind', + 'hathor.nanocontracts.types.Address.rindex', + 'hathor.nanocontracts.types.Address.rjust', + 'hathor.nanocontracts.types.Address.rpartition', + 'hathor.nanocontracts.types.Address.rsplit', + 'hathor.nanocontracts.types.Address.rstrip', 'hathor.nanocontracts.types.Address.some_new_attribute', + 'hathor.nanocontracts.types.Address.split', + 'hathor.nanocontracts.types.Address.splitlines', + 'hathor.nanocontracts.types.Address.startswith', + 'hathor.nanocontracts.types.Address.strip', + 'hathor.nanocontracts.types.Address.swapcase', + 'hathor.nanocontracts.types.Address.title', + 'hathor.nanocontracts.types.Address.translate', + 'hathor.nanocontracts.types.Address.upper', + 'hathor.nanocontracts.types.Address.zfill', 'hathor.nanocontracts.types.Amount.some_new_attribute', 'hathor.nanocontracts.types.BlueprintId.some_new_attribute', + 'hathor.nanocontracts.types.ContractId.capitalize', + 'hathor.nanocontracts.types.ContractId.center', + 'hathor.nanocontracts.types.ContractId.count', + 'hathor.nanocontracts.types.ContractId.decode', + 'hathor.nanocontracts.types.ContractId.endswith', + 'hathor.nanocontracts.types.ContractId.expandtabs', + 'hathor.nanocontracts.types.ContractId.find', + 'hathor.nanocontracts.types.ContractId.fromhex', + 'hathor.nanocontracts.types.ContractId.hex', + 'hathor.nanocontracts.types.ContractId.index', + 'hathor.nanocontracts.types.ContractId.isalnum', + 'hathor.nanocontracts.types.ContractId.isalpha', + 'hathor.nanocontracts.types.ContractId.isascii', + 'hathor.nanocontracts.types.ContractId.isdigit', + 'hathor.nanocontracts.types.ContractId.islower', + 'hathor.nanocontracts.types.ContractId.isspace', + 'hathor.nanocontracts.types.ContractId.istitle', + 'hathor.nanocontracts.types.ContractId.isupper', + 'hathor.nanocontracts.types.ContractId.join', + 'hathor.nanocontracts.types.ContractId.ljust', + 'hathor.nanocontracts.types.ContractId.lower', + 'hathor.nanocontracts.types.ContractId.lstrip', + 'hathor.nanocontracts.types.ContractId.maketrans', + 'hathor.nanocontracts.types.ContractId.partition', + 'hathor.nanocontracts.types.ContractId.removeprefix', + 'hathor.nanocontracts.types.ContractId.removesuffix', + 'hathor.nanocontracts.types.ContractId.replace', + 'hathor.nanocontracts.types.ContractId.rfind', + 'hathor.nanocontracts.types.ContractId.rindex', + 'hathor.nanocontracts.types.ContractId.rjust', + 'hathor.nanocontracts.types.ContractId.rpartition', + 'hathor.nanocontracts.types.ContractId.rsplit', + 'hathor.nanocontracts.types.ContractId.rstrip', 'hathor.nanocontracts.types.ContractId.some_new_attribute', + 'hathor.nanocontracts.types.ContractId.split', + 'hathor.nanocontracts.types.ContractId.splitlines', + 'hathor.nanocontracts.types.ContractId.startswith', + 'hathor.nanocontracts.types.ContractId.strip', + 'hathor.nanocontracts.types.ContractId.swapcase', + 'hathor.nanocontracts.types.ContractId.title', + 'hathor.nanocontracts.types.ContractId.translate', + 'hathor.nanocontracts.types.ContractId.upper', + 'hathor.nanocontracts.types.ContractId.zfill', 'hathor.nanocontracts.types.NCAcquireAuthorityAction.melt', 'hathor.nanocontracts.types.NCAcquireAuthorityAction.mint', 'hathor.nanocontracts.types.NCAcquireAuthorityAction.name', @@ -137,7 +223,49 @@ 'hathor.nanocontracts.types.Timestamp.some_new_attribute', 'hathor.nanocontracts.types.TokenUid.some_new_attribute', 'hathor.nanocontracts.types.TxOutputScript.some_new_attribute', + 'hathor.nanocontracts.types.VertexId.capitalize', + 'hathor.nanocontracts.types.VertexId.center', + 'hathor.nanocontracts.types.VertexId.count', + 'hathor.nanocontracts.types.VertexId.decode', + 'hathor.nanocontracts.types.VertexId.endswith', + 'hathor.nanocontracts.types.VertexId.expandtabs', + 'hathor.nanocontracts.types.VertexId.find', + 'hathor.nanocontracts.types.VertexId.fromhex', + 'hathor.nanocontracts.types.VertexId.hex', + 'hathor.nanocontracts.types.VertexId.index', + 'hathor.nanocontracts.types.VertexId.isalnum', + 'hathor.nanocontracts.types.VertexId.isalpha', + 'hathor.nanocontracts.types.VertexId.isascii', + 'hathor.nanocontracts.types.VertexId.isdigit', + 'hathor.nanocontracts.types.VertexId.islower', + 'hathor.nanocontracts.types.VertexId.isspace', + 'hathor.nanocontracts.types.VertexId.istitle', + 'hathor.nanocontracts.types.VertexId.isupper', + 'hathor.nanocontracts.types.VertexId.join', + 'hathor.nanocontracts.types.VertexId.ljust', + 'hathor.nanocontracts.types.VertexId.lower', + 'hathor.nanocontracts.types.VertexId.lstrip', + 'hathor.nanocontracts.types.VertexId.maketrans', + 'hathor.nanocontracts.types.VertexId.partition', + 'hathor.nanocontracts.types.VertexId.removeprefix', + 'hathor.nanocontracts.types.VertexId.removesuffix', + 'hathor.nanocontracts.types.VertexId.replace', + 'hathor.nanocontracts.types.VertexId.rfind', + 'hathor.nanocontracts.types.VertexId.rindex', + 'hathor.nanocontracts.types.VertexId.rjust', + 'hathor.nanocontracts.types.VertexId.rpartition', + 'hathor.nanocontracts.types.VertexId.rsplit', + 'hathor.nanocontracts.types.VertexId.rstrip', 'hathor.nanocontracts.types.VertexId.some_new_attribute', + 'hathor.nanocontracts.types.VertexId.split', + 'hathor.nanocontracts.types.VertexId.splitlines', + 'hathor.nanocontracts.types.VertexId.startswith', + 'hathor.nanocontracts.types.VertexId.strip', + 'hathor.nanocontracts.types.VertexId.swapcase', + 'hathor.nanocontracts.types.VertexId.title', + 'hathor.nanocontracts.types.VertexId.translate', + 'hathor.nanocontracts.types.VertexId.upper', + 'hathor.nanocontracts.types.VertexId.zfill', 'hathor.nanocontracts.types.fallback.some_new_attribute', 'hathor.nanocontracts.types.public.some_new_attribute', 'hathor.nanocontracts.types.view.some_new_attribute', diff --git a/tests/nanocontracts/test_fallback_method.py b/tests/nanocontracts/test_fallback_method.py index 30c7f2242..d1d17d860 100644 --- a/tests/nanocontracts/test_fallback_method.py +++ b/tests/nanocontracts/test_fallback_method.py @@ -84,7 +84,7 @@ def setUp(self) -> None: self.ctx = Context( actions=[NCDepositAction(token_uid=TokenUid(HATHOR_TOKEN_UID), amount=123)], vertex=self.get_genesis_tx(), - address=self.gen_random_address(), + caller_id=self.gen_random_address(), timestamp=self.now, ) self.runner.create_contract(self.contract_id, self.blueprint_id, self.ctx) diff --git a/tests/nanocontracts/test_reentrancy.py b/tests/nanocontracts/test_reentrancy.py index c5f94df81..434f23b59 100644 --- a/tests/nanocontracts/test_reentrancy.py +++ b/tests/nanocontracts/test_reentrancy.py @@ -21,7 +21,7 @@ def initialize(self, ctx: Context) -> None: @public(allow_deposit=True) def deposit(self, ctx: Context) -> None: - address = ctx.address + address = ctx.caller_id action = ctx.get_single_action(HTR_TOKEN_UID) assert isinstance(action, NCDepositAction) amount = action.amount @@ -33,7 +33,7 @@ def deposit(self, ctx: Context) -> None: @public def transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method: str) -> None: - address = ctx.address + address = ctx.caller_id if amount > self.balances.get(address, 0): raise InsufficientBalance('insufficient balance') @@ -45,7 +45,7 @@ def transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method @public def fixed_transfer_to(self, ctx: Context, amount: Amount, contract: ContractId, method: str) -> None: - address = ctx.address + address = ctx.caller_id if amount > self.balances.get(address, 0): raise InsufficientBalance('insufficient balance') diff --git a/tests/nanocontracts/test_syscalls.py b/tests/nanocontracts/test_syscalls.py index cc14ddc71..6bb6574a3 100644 --- a/tests/nanocontracts/test_syscalls.py +++ b/tests/nanocontracts/test_syscalls.py @@ -102,7 +102,7 @@ def test_authorities(self) -> None: NCDepositAction(token_uid=token_a_uid, amount=1000), ], vertex=self.get_genesis_tx(), - address=self.gen_random_address(), + caller_id=self.gen_random_address(), timestamp=0, ) @@ -112,7 +112,7 @@ def test_authorities(self) -> None: ctx_grant = Context( actions=[NCGrantAuthorityAction(token_uid=token_a_uid, mint=True, melt=True)], vertex=self.get_genesis_tx(), - address=self.gen_random_address(), + caller_id=self.gen_random_address(), timestamp=0, ) self.runner.call_public_method(nc_id, 'nop', ctx_grant) @@ -120,7 +120,7 @@ def test_authorities(self) -> None: ctx = Context( actions=[], vertex=self.get_genesis_tx(), - address=self.gen_random_address(), + caller_id=self.gen_random_address(), timestamp=0, ) diff --git a/tests/nanocontracts/test_syscalls_in_view.py b/tests/nanocontracts/test_syscalls_in_view.py index 3058d4958..e1e0866c2 100644 --- a/tests/nanocontracts/test_syscalls_in_view.py +++ b/tests/nanocontracts/test_syscalls_in_view.py @@ -124,7 +124,7 @@ def setUp(self) -> None: self.ctx = Context( actions=[], vertex=self.get_genesis_tx(), - address=self.gen_random_address(), + caller_id=self.gen_random_address(), timestamp=self.now, ) diff --git a/tests/nanocontracts/test_violations.py b/tests/nanocontracts/test_violations.py index c17cb0cac..4e716fd4e 100644 --- a/tests/nanocontracts/test_violations.py +++ b/tests/nanocontracts/test_violations.py @@ -43,7 +43,7 @@ def test_modify_actions(self) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.create_contract(self.contract_id, self.blueprint_id, context) @@ -57,7 +57,7 @@ def test_modify_vertex(self) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.create_contract(self.contract_id, self.blueprint_id, context) @@ -70,7 +70,7 @@ def test_assign_non_declared_attribute(self) -> None: context = Context( actions=[], vertex=self.tx, - address=self.address, + caller_id=self.address, timestamp=self.now ) self.runner.create_contract(self.contract_id, self.blueprint_id, context)