From 7a2a4eb0a906294a67206f4e52fc55dca9acf4bf Mon Sep 17 00:00:00 2001 From: raul-oliveira Date: Wed, 10 Sep 2025 05:45:39 -0300 Subject: [PATCH] feat: fee token creation in nano --- hathor/indexes/manager.py | 17 +- hathor/nanocontracts/blueprint_env.py | 27 +- hathor/nanocontracts/exception.py | 4 + .../nc_types/dataclass_nc_type.py | 13 +- .../nc_types/sized_int_nc_type.py | 5 + hathor/nanocontracts/runner/runner.py | 250 ++++++--- hathor/nanocontracts/runner/types.py | 42 +- hathor/nanocontracts/storage/block_storage.py | 25 +- .../nanocontracts/storage/changes_tracker.py | 26 +- .../nanocontracts/storage/contract_storage.py | 21 +- hathor/nanocontracts/storage/token_proxy.py | 21 +- .../syscall_token_balance_rules.py | 368 +++++++++++++ hathor/transaction/token_info.py | 1 + tests/nanocontracts/test_execution_order.py | 10 + tests/nanocontracts/test_syscalls.py | 498 +++++++++++++++++- tests/nanocontracts/test_syscalls_in_view.py | 4 +- tests/nanocontracts/test_token_creation.py | 13 +- 17 files changed, 1197 insertions(+), 148 deletions(-) create mode 100644 hathor/nanocontracts/syscall_token_balance_rules.py diff --git a/hathor/indexes/manager.py b/hathor/indexes/manager.py index 75dbbd28e..76f638946 100644 --- a/hathor/indexes/manager.py +++ b/hathor/indexes/manager.py @@ -216,11 +216,10 @@ def handle_contract_execution(self, tx: BaseTransaction) -> None: Update indexes according to a Nano Contract execution. Must be called only once for each time a contract is executed. """ - from hathor.conf.settings import HATHOR_TOKEN_UID from hathor.nanocontracts.runner.types import ( NCIndexUpdateRecord, SyscallCreateContractRecord, - SyscallUpdateTokensRecord, + SyscallUpdateTokenRecord, UpdateAuthoritiesRecord, ) from hathor.nanocontracts.types import ContractId @@ -260,7 +259,7 @@ def handle_contract_execution(self, tx: BaseTransaction) -> None: if self.blueprint_history: self.blueprint_history.add_single_key(blueprint_id, tx) - case SyscallUpdateTokensRecord(): + case SyscallUpdateTokenRecord(): # Minted/melted tokens are added/removed to/from the tokens index, # and the respective destroyed/created HTR too. if self.tokens: @@ -280,8 +279,7 @@ def handle_contract_execution(self, tx: BaseTransaction) -> None: version=record.token_version ) - self.tokens.add_to_total(record.token_uid, record.token_amount) - self.tokens.add_to_total(HATHOR_TOKEN_UID, record.htr_amount) + self.tokens.add_to_total(record.token_uid, record.amount) case UpdateAuthoritiesRecord(): if self.tokens: @@ -295,11 +293,10 @@ def handle_contract_unexecution(self, tx: BaseTransaction) -> None: Update indexes according to a Nano Contract unexecution, which happens when a reorg unconfirms a nano tx. Must be called only once for each time a contract is unexecuted. """ - from hathor.conf.settings import HATHOR_TOKEN_UID from hathor.nanocontracts.runner.types import ( NCIndexUpdateRecord, SyscallCreateContractRecord, - SyscallUpdateTokensRecord, + SyscallUpdateTokenRecord, UpdateAuthoritiesRecord, ) from hathor.nanocontracts.types import NC_INITIALIZE_METHOD, ContractId @@ -341,11 +338,9 @@ def handle_contract_unexecution(self, tx: BaseTransaction) -> None: if self.blueprint_history: self.blueprint_history.remove_single_key(blueprint_id, tx) - case SyscallUpdateTokensRecord(): - # Undo the tokens update. + case SyscallUpdateTokenRecord(): if self.tokens: - self.tokens.add_to_total(record.token_uid, -record.token_amount) - self.tokens.add_to_total(HATHOR_TOKEN_UID, -record.htr_amount) + self.tokens.add_to_total(record.token_uid, -record.amount) from hathor.nanocontracts.runner.types import IndexUpdateRecordType if record.type is IndexUpdateRecordType.CREATE_TOKEN: diff --git a/hathor/nanocontracts/blueprint_env.py b/hathor/nanocontracts/blueprint_env.py index 78b607e6f..35ea0e3cf 100644 --- a/hathor/nanocontracts/blueprint_env.py +++ b/hathor/nanocontracts/blueprint_env.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any, Collection, Optional, Sequence, TypeAlias, final +from hathor.conf.settings import HATHOR_TOKEN_UID from hathor.nanocontracts.storage import NCContractStorage from hathor.nanocontracts.types import Amount, BlueprintId, ContractId, NCAction, TokenUid @@ -218,14 +219,26 @@ def revoke_authorities(self, token_uid: TokenUid, *, revoke_mint: bool, revoke_m self.__runner.syscall_revoke_authorities(token_uid=token_uid, revoke_mint=revoke_mint, revoke_melt=revoke_melt) @final - def mint_tokens(self, token_uid: TokenUid, amount: int) -> None: + 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) + self.__runner.syscall_mint_tokens(token_uid=token_uid, amount=amount, fee_payment_token=fee_payment_token) @final - def melt_tokens(self, token_uid: TokenUid, amount: int) -> None: + 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) + self.__runner.syscall_melt_tokens(token_uid=token_uid, amount=amount, fee_payment_token=fee_payment_token) @final def create_contract( @@ -250,9 +263,9 @@ def create_deposit_token( 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.""" @@ -271,10 +284,11 @@ def create_fee_token( 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( @@ -284,6 +298,7 @@ def create_fee_token( amount=amount, mint_authority=mint_authority, melt_authority=melt_authority, + fee_payment_token=fee_payment_token ) @final diff --git a/hathor/nanocontracts/exception.py b/hathor/nanocontracts/exception.py index de687c5aa..3e9ff96df 100644 --- a/hathor/nanocontracts/exception.py +++ b/hathor/nanocontracts/exception.py @@ -106,6 +106,10 @@ class NCInvalidMethodCall(NCFail, NCTxValidationError): """Raised when a contract calls another contract's invalid method.""" +class NCInvalidFeePaymentToken(NCFail): + """Raised when a payment token is invalid.""" + + class NCInvalidInitializeMethodCall(NCFail): """Raised when a contract calls another contract's initialize method.""" diff --git a/hathor/nanocontracts/nc_types/dataclass_nc_type.py b/hathor/nanocontracts/nc_types/dataclass_nc_type.py index e460974e1..f16edcb93 100644 --- a/hathor/nanocontracts/nc_types/dataclass_nc_type.py +++ b/hathor/nanocontracts/nc_types/dataclass_nc_type.py @@ -34,14 +34,23 @@ if TYPE_CHECKING: from _typeshed import DataclassInstance + from hathor.nanocontracts.nc_types import TypeToNCTypeMap + D = TypeVar('D', bound='DataclassInstance') -def make_dataclass_nc_type(class_: type[D]) -> DataclassNCType[D]: +def make_dataclass_nc_type( + class_: type[D], + *, + extra_nc_types_map: TypeToNCTypeMap | None = None, +) -> DataclassNCType[D]: """ Helper function to build a NCType for the given dataclass. """ from hathor.nanocontracts.nc_types import DEFAULT_TYPE_ALIAS_MAP, RETURN_TYPE_TO_NC_TYPE_MAP - type_map = NCType.TypeMap(DEFAULT_TYPE_ALIAS_MAP, RETURN_TYPE_TO_NC_TYPE_MAP) + alias_map = DEFAULT_TYPE_ALIAS_MAP + extras = extra_nc_types_map or {} + nc_types_map = {**RETURN_TYPE_TO_NC_TYPE_MAP, **extras} + type_map = NCType.TypeMap(alias_map, nc_types_map) return DataclassNCType._from_type(class_, type_map=type_map) diff --git a/hathor/nanocontracts/nc_types/sized_int_nc_type.py b/hathor/nanocontracts/nc_types/sized_int_nc_type.py index d19812e5d..d37538702 100644 --- a/hathor/nanocontracts/nc_types/sized_int_nc_type.py +++ b/hathor/nanocontracts/nc_types/sized_int_nc_type.py @@ -101,3 +101,8 @@ class Int32NCType(_SizedIntNCType): class Uint32NCType(_SizedIntNCType): _signed = False _byte_size = 4 # 4-bytes -> 32-bits + + +class Uint8NCType(_SizedIntNCType): + _signed = False + _byte_size = 1 diff --git a/hathor/nanocontracts/runner/runner.py b/hathor/nanocontracts/runner/runner.py index 9c12d3582..81a409f20 100644 --- a/hathor/nanocontracts/runner/runner.py +++ b/hathor/nanocontracts/runner/runner.py @@ -47,14 +47,14 @@ CallInfo, CallRecord, CallType, - IndexUpdateRecordType, SyscallCreateContractRecord, - SyscallUpdateTokensRecord, + SyscallUpdateTokenRecord, UpdateAuthoritiesRecord, UpdateAuthoritiesRecordType, ) from hathor.nanocontracts.storage import NCBlockStorage, NCChangesTracker, NCContractStorage, NCStorageFactory from hathor.nanocontracts.storage.contract_storage import Balance +from hathor.nanocontracts.syscall_token_balance_rules import TokenSyscallBalanceRules from hathor.nanocontracts.types import ( NC_ALLOW_REENTRANCY, NC_ALLOWED_ACTIONS_ATTR, @@ -87,13 +87,9 @@ from hathor.transaction import Transaction from hathor.transaction.exceptions import TransactionDataError from hathor.transaction.storage import TransactionStorage -from hathor.transaction.token_info import TokenVersion -from hathor.transaction.util import ( - clean_token_string, - get_deposit_token_deposit_amount, - get_deposit_token_withdraw_amount, - validate_token_name_and_symbol, -) +from hathor.transaction.storage.exceptions import TransactionDoesNotExist +from hathor.transaction.token_info import TokenDescription, TokenVersion +from hathor.transaction.util import clean_token_string, validate_token_name_and_symbol P = ParamSpec('P') T = TypeVar('T') @@ -474,9 +470,8 @@ def _validate_balances(self, ctx: Context) -> None: case SyscallCreateContractRecord() | UpdateAuthoritiesRecord(): # Nothing to do here. pass - case SyscallUpdateTokensRecord(): - calculated_tokens_totals[record.token_uid] += record.token_amount - calculated_tokens_totals[TokenUid(HATHOR_TOKEN_UID)] += record.htr_amount + case SyscallUpdateTokenRecord(): + calculated_tokens_totals[record.token_uid] += record.amount case _: assert_never(record) @@ -919,68 +914,72 @@ def syscall_revoke_authorities(self, token_uid: TokenUid, *, revoke_mint: bool, call_record.index_updates.append(syscall_record) @_forbid_syscall_from_view('mint_tokens') - def syscall_mint_tokens(self, token_uid: TokenUid, amount: int) -> None: - """Mint tokens and add them to the balance of this nano contract.""" + def syscall_mint_tokens( + self, + *, + token_uid: TokenUid, + amount: int, + fee_payment_token: TokenUid = TokenUid(HATHOR_TOKEN_UID) + ) -> None: + """Mint tokens and adds them to the balance of this nano contract. + The tokens should be already created otherwise it will raise. + """ + if amount <= 0: + raise NCInvalidSyscall(f"token amount must be always positive. amount={amount}") + call_record = self.get_current_call_record() if token_uid == HATHOR_TOKEN_UID: raise NCInvalidSyscall(f'contract {call_record.contract_id.hex()} cannot mint HTR tokens') changes_tracker = self.get_current_changes_tracker(call_record.contract_id) assert changes_tracker.nc_id == call_record.contract_id - balance = changes_tracker.get_balance(token_uid) + balance = changes_tracker.get_balance(token_uid) if not balance.can_mint: raise NCInvalidSyscall(f'contract {call_record.contract_id.hex()} cannot mint {token_uid.hex()} tokens') - token_amount = amount - htr_amount = -get_deposit_token_deposit_amount(self._settings, token_amount) + fee_payment_token_info = self._get_token(fee_payment_token) + token_info = self._get_token(token_uid) - changes_tracker.add_balance(token_uid, token_amount) - changes_tracker.add_balance(HATHOR_TOKEN_UID, htr_amount) + syscall_rules = TokenSyscallBalanceRules.get_rules(token_uid, token_info.token_version, self._settings) + syscall_balance = syscall_rules.mint(amount, fee_payment_token=fee_payment_token_info) + records = syscall_rules.get_syscall_update_token_records(syscall_balance) - self._updated_tokens_totals[token_uid] += token_amount - self._updated_tokens_totals[TokenUid(HATHOR_TOKEN_UID)] += htr_amount - - assert call_record.index_updates is not None - syscall_record = SyscallUpdateTokensRecord( - type=IndexUpdateRecordType.MINT_TOKENS, - token_uid=token_uid, - token_amount=token_amount, - htr_amount=htr_amount, - ) - call_record.index_updates.append(syscall_record) + self._update_tokens_amount(records) @_forbid_syscall_from_view('melt_tokens') - def syscall_melt_tokens(self, token_uid: TokenUid, amount: int) -> None: - """Melt tokens by removing them from the balance of this nano contract.""" + def syscall_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. + The tokens should be already created otherwise it will raise. + """ + if amount <= 0: + raise NCInvalidSyscall(f"token amount must be always positive. amount={amount}") + call_record = self.get_current_call_record() if token_uid == HATHOR_TOKEN_UID: raise NCInvalidSyscall(f'contract {call_record.contract_id.hex()} cannot melt HTR tokens') changes_tracker = self.get_current_changes_tracker(call_record.contract_id) assert changes_tracker.nc_id == call_record.contract_id - balance = changes_tracker.get_balance(token_uid) + balance = changes_tracker.get_balance(token_uid) if not balance.can_melt: raise NCInvalidSyscall(f'contract {call_record.contract_id.hex()} cannot melt {token_uid.hex()} tokens') - token_amount = -amount - htr_amount = get_deposit_token_withdraw_amount(self._settings, token_amount) + token_info = self._get_token(token_uid) + fee_payment_token_info = self._get_token(fee_payment_token) - changes_tracker.add_balance(token_uid, token_amount) - changes_tracker.add_balance(HATHOR_TOKEN_UID, htr_amount) + syscall_rules = TokenSyscallBalanceRules.get_rules(token_uid, token_info.token_version, self._settings) + syscall_balance = syscall_rules.melt(amount, fee_payment_token=fee_payment_token_info) + records = syscall_rules.get_syscall_update_token_records(syscall_balance) - self._updated_tokens_totals[token_uid] += token_amount - self._updated_tokens_totals[TokenUid(HATHOR_TOKEN_UID)] += htr_amount - - assert call_record.index_updates is not None - syscall_record = SyscallUpdateTokensRecord( - type=IndexUpdateRecordType.MELT_TOKENS, - token_uid=token_uid, - token_amount=token_amount, - htr_amount=htr_amount, - ) - call_record.index_updates.append(syscall_record) + self._update_tokens_amount(records) def _validate_context(self, ctx: Context) -> None: """Check whether the context is valid.""" @@ -1031,42 +1030,45 @@ def syscall_create_child_deposit_token( melt_authority: bool, ) -> TokenUid: """Create a child deposit token from a contract.""" + if amount <= 0: + raise NCInvalidSyscall(f"token amount must be always positive. amount={amount}") + try: validate_token_name_and_symbol(self._settings, token_name, token_symbol) except TransactionDataError as e: raise NCInvalidSyscall(str(e)) from e - last_call_record = self.get_current_call_record() - parent_id = last_call_record.contract_id + call_record = self.get_current_call_record() + parent_id = call_record.contract_id cleaned_token_symbol = clean_token_string(token_symbol) - token_id = derive_child_token_id(parent_id, cleaned_token_symbol, salt=salt) - token_amount = amount - htr_amount = get_deposit_token_deposit_amount(self._settings, token_amount) + token_id = derive_child_token_id(parent_id, cleaned_token_symbol, salt=salt) + token_version = TokenVersion.DEPOSIT changes_tracker = self.get_current_changes_tracker(parent_id) - changes_tracker.create_token(token_id, token_name, token_symbol) + changes_tracker.create_token( + token_id=token_id, + token_name=token_name, + token_symbol=token_symbol, + token_version=token_version + ) changes_tracker.grant_authorities( token_id, grant_mint=mint_authority, grant_melt=melt_authority, ) - changes_tracker.add_balance(token_id, amount) - changes_tracker.add_balance(HATHOR_TOKEN_UID, -htr_amount) - self._updated_tokens_totals[token_id] += amount - self._updated_tokens_totals[TokenUid(HATHOR_TOKEN_UID)] -= htr_amount - assert last_call_record.index_updates is not None - syscall_record = SyscallUpdateTokensRecord( - type=IndexUpdateRecordType.CREATE_TOKEN, + syscall_rules = TokenSyscallBalanceRules.get_rules(token_id, token_version, self._settings) + syscall_balance = syscall_rules.create_token( token_uid=token_id, - token_amount=token_amount, - htr_amount=-htr_amount, token_symbol=token_symbol, token_name=token_name, - token_version=TokenVersion.DEPOSIT + amount=amount, + fee_payment_token=self._get_token(TokenUid(HATHOR_TOKEN_UID)) ) - last_call_record.index_updates.append(syscall_record) + records = syscall_rules.get_syscall_update_token_records(syscall_balance) + + self._update_tokens_amount(records) return token_id @@ -1080,9 +1082,50 @@ def syscall_create_child_fee_token( amount: int, mint_authority: bool, melt_authority: bool, + fee_payment_token: TokenUid ) -> TokenUid: - """Create a child deposit token from a contract.""" - raise NotImplementedError('syscall not implemented') + """Create a child fee token from a contract.""" + if amount <= 0: + raise NCInvalidSyscall(f"token amount must be always positive. amount={amount}") + + try: + validate_token_name_and_symbol(self._settings, token_name, token_symbol) + except TransactionDataError as e: + raise NCInvalidSyscall(str(e)) from e + + call_record = self.get_current_call_record() + parent_id = call_record.contract_id + cleaned_token_symbol = clean_token_string(token_symbol) + + fee_payment_token_info = self._get_token(fee_payment_token) + token_id = derive_child_token_id(parent_id, cleaned_token_symbol, salt=salt) + token_version = TokenVersion.FEE + + changes_tracker = self.get_current_changes_tracker(parent_id) + changes_tracker.create_token( + token_id=token_id, + token_name=token_name, + token_symbol=token_symbol, + token_version=token_version + ) + changes_tracker.grant_authorities( + token_id, + grant_mint=mint_authority, + grant_melt=melt_authority, + ) + syscall_rules = TokenSyscallBalanceRules.get_rules(token_id, token_version, self._settings) + syscall_balance = syscall_rules.create_token( + token_uid=token_id, + token_symbol=token_symbol, + token_name=token_name, + amount=amount, + fee_payment_token=fee_payment_token_info + ) + records = syscall_rules.get_syscall_update_token_records(syscall_balance) + + self._update_tokens_amount(records) + + return token_id @_forbid_syscall_from_view('emit_event') def syscall_emit_event(self, data: bytes) -> None: @@ -1105,6 +1148,79 @@ def syscall_change_blueprint(self, blueprint_id: BlueprintId) -> None: nc_storage = self.get_current_changes_tracker(last_call_record.contract_id) nc_storage.set_blueprint_id(blueprint_id) + def _get_token(self, token_uid: TokenUid) -> TokenDescription: + """ + Get a token from the current changes tracker or storage. + + Raises: + NCInvalidSyscall when the token isn't found. + """ + call_record = self.get_current_call_record() + changes_tracker = self.get_current_changes_tracker(call_record.contract_id) + assert call_record.contract_id == changes_tracker.nc_id + + if changes_tracker.has_token(token_uid): + return changes_tracker.get_token(token_uid) + + # Special case for HTR token (native token with UID 00) + if token_uid == HATHOR_TOKEN_UID: + return TokenDescription( + token_version=TokenVersion.NATIVE, # HTR is the native token + token_name=self._settings.HATHOR_TOKEN_NAME, + token_symbol=self._settings.HATHOR_TOKEN_SYMBOL, + token_id=HATHOR_TOKEN_UID # HTR token ID is the same as its UID + ) + + # Check the transaction storage for existing tokens + try: + token_creation_tx = self.tx_storage.get_token_creation_transaction(token_uid) + except TransactionDoesNotExist: + raise NCInvalidSyscall( + f'contract {call_record.contract_id.hex()} could not find {token_uid.hex()} token' + ) + + if token_creation_tx.get_metadata().first_block is None: + raise NCInvalidSyscall( + f'The {token_uid.hex()} token is not confirmed by any block ' + f'for contract {call_record.contract_id.hex()}' + ) + + return TokenDescription( + token_version=token_creation_tx.token_version, + token_name=token_creation_tx.token_name, + token_symbol=token_creation_tx.token_symbol, + token_id=token_creation_tx.hash + ) + + def _update_tokens_amount( + self, + records: list[SyscallUpdateTokenRecord] + ) -> None: + """ + Update token balances and create index records for a token operation. + + This method performs the complete flow of updating token balances for syscalls: + 1. Updates the contract's token balances in the changes tracker + 2. Updates the global token totals + 3. Appends the syscall records to call_record.index_updates + + Args: + records: List of syscall update records (typically main token + fee payment) + + Raises: + AssertionError: If call_record.index_updates is None + """ + call_record = self.get_current_call_record() + changes_tracker = self.get_current_changes_tracker(call_record.contract_id) + + assert changes_tracker.nc_id == call_record.contract_id + assert call_record.index_updates is not None + + for record in records: + changes_tracker.add_balance(record.token_uid, record.amount) + self._updated_tokens_totals[record.token_uid] += record.amount + call_record.index_updates.append(record) + class RunnerFactory: __slots__ = ('reactor', 'settings', 'tx_storage', 'nc_storage_factory') diff --git a/hathor/nanocontracts/runner/types.py b/hathor/nanocontracts/runner/types.py index f3a79d5a4..67477e466 100644 --- a/hathor/nanocontracts/runner/types.py +++ b/hathor/nanocontracts/runner/types.py @@ -67,35 +67,32 @@ def from_json(cls, json_dict: dict[str, Any]) -> Self: @dataclass(slots=True, frozen=True, kw_only=True) -class SyscallUpdateTokensRecord: +class SyscallUpdateTokenRecord: + """Record for token balance updates in syscalls. + + This record represents a single token operation (mint, melt, or create). + Each syscall may generate multiple records (e.g., main token + fee payment token). + """ + token_uid: TokenUid + amount: int type: ( Literal[IndexUpdateRecordType.MINT_TOKENS] | Literal[IndexUpdateRecordType.MELT_TOKENS] | Literal[IndexUpdateRecordType.CREATE_TOKEN] ) - token_uid: TokenUid - token_amount: int - htr_amount: int + # Optional fields used for CREATE_TOKEN operations token_symbol: str | None = None token_name: str | None = None token_version: TokenVersion | None = None - def __post_init__(self) -> None: - match self.type: - case IndexUpdateRecordType.MINT_TOKENS | IndexUpdateRecordType.CREATE_TOKEN: - assert self.token_amount > 0 and self.htr_amount < 0 - case IndexUpdateRecordType.MELT_TOKENS: - assert self.token_amount < 0 and self.htr_amount > 0 - case _: - assert_never(self.type) - def to_json(self) -> dict[str, Any]: return dict( type=self.type, token_uid=self.token_uid.hex(), - token_amount=self.token_amount, + amount=self.amount, + token_name=self.token_name, + token_symbol=self.token_symbol, token_version=self.token_version, - htr_amount=self.htr_amount, ) @classmethod @@ -107,9 +104,10 @@ def from_json(cls, json_dict: dict[str, Any]) -> Self: return cls( type=json_dict['type'], token_uid=TokenUid(VertexId(bytes.fromhex(json_dict['token_uid']))), - token_amount=json_dict['token_amount'], - token_version=json_dict['token_version'], - htr_amount=json_dict['htr_amount'], + amount=json_dict['amount'], + token_version=json_dict.get('token_version'), + token_name=json_dict.get('token_name'), + token_symbol=json_dict.get('token_symbol'), ) @@ -149,7 +147,11 @@ def from_json(cls, json_dict: dict[str, Any]) -> Self: ) -NCIndexUpdateRecord: TypeAlias = SyscallCreateContractRecord | SyscallUpdateTokensRecord | UpdateAuthoritiesRecord +NCIndexUpdateRecord: TypeAlias = ( + SyscallCreateContractRecord | + SyscallUpdateTokenRecord | + UpdateAuthoritiesRecord +) def nc_index_update_record_from_json(json_dict: dict[str, Any]) -> NCIndexUpdateRecord: @@ -162,7 +164,7 @@ def nc_index_update_record_from_json(json_dict: dict[str, Any]) -> NCIndexUpdate | IndexUpdateRecordType.MELT_TOKENS | IndexUpdateRecordType.CREATE_TOKEN ): - return SyscallUpdateTokensRecord.from_json(json_dict) + return SyscallUpdateTokenRecord.from_json(json_dict) case IndexUpdateRecordType.UPDATE_AUTHORITIES: return UpdateAuthoritiesRecord.from_json(json_dict) case _: diff --git a/hathor/nanocontracts/storage/block_storage.py b/hathor/nanocontracts/storage/block_storage.py index 238606ef5..106f44d27 100644 --- a/hathor/nanocontracts/storage/block_storage.py +++ b/hathor/nanocontracts/storage/block_storage.py @@ -19,11 +19,13 @@ from hathor.nanocontracts.exception import NanoContractDoesNotExist from hathor.nanocontracts.nc_types.dataclass_nc_type import make_dataclass_nc_type +from hathor.nanocontracts.nc_types.sized_int_nc_type import Uint8NCType from hathor.nanocontracts.storage.contract_storage import NCContractStorage from hathor.nanocontracts.storage.patricia_trie import NodeId, PatriciaTrie from hathor.nanocontracts.storage.token_proxy import TokenProxy from hathor.nanocontracts.types import Address, ContractId, TokenUid from hathor.transaction.headers.nano_header import ADDRESS_SEQNUM_SIZE +from hathor.transaction.token_info import TokenVersion from hathor.utils import leb128 @@ -59,7 +61,12 @@ class NCBlockStorage: This implementation works for both memory and rocksdb backends.""" from hathor.transaction.token_info import TokenDescription - _TOKEN_DESCRIPTION_NC_TYPE = make_dataclass_nc_type(TokenDescription) + _TOKEN_DESCRIPTION_NC_TYPE = make_dataclass_nc_type( + TokenDescription, + extra_nc_types_map={ + TokenVersion: Uint8NCType, + }, + ) def __init__(self, block_trie: PatriciaTrie) -> None: self._block_trie: PatriciaTrie = block_trie @@ -134,11 +141,23 @@ def has_token(self, token_id: TokenUid) -> bool: else: return True - def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) -> None: + def create_token( + self, + *, + token_id: TokenUid, + token_name: str, + token_symbol: str, + token_version: TokenVersion + ) -> None: """Create a new token in this block's nano state.""" from hathor.transaction.token_info import TokenDescription key = TokenKey(token_id) - token_description = TokenDescription(token_id=token_id, token_name=token_name, token_symbol=token_symbol) + token_description = TokenDescription( + token_id=token_id, + token_name=token_name, + token_symbol=token_symbol, + token_version=token_version + ) token_description_bytes = self._TOKEN_DESCRIPTION_NC_TYPE.to_bytes(token_description) self._block_trie.update(bytes(key), token_description_bytes) diff --git a/hathor/nanocontracts/storage/changes_tracker.py b/hathor/nanocontracts/storage/changes_tracker.py index 49ec81f65..fef8791cd 100644 --- a/hathor/nanocontracts/storage/changes_tracker.py +++ b/hathor/nanocontracts/storage/changes_tracker.py @@ -32,7 +32,7 @@ ) from hathor.nanocontracts.storage.types import _NOT_PROVIDED, DeletedKey, DeletedKeyType from hathor.nanocontracts.types import BlueprintId, ContractId, TokenUid -from hathor.transaction.token_info import TokenDescription +from hathor.transaction.token_info import TokenDescription, TokenVersion T = TypeVar('T') D = TypeVar('D') @@ -86,7 +86,14 @@ def __init__(self, nc_id: ContractId, storage: NCContractStorage): self.has_been_commited = False self.has_been_blocked = False - def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) -> None: + def create_token( + self, + *, + token_id: TokenUid, + token_name: str, + token_symbol: str, + token_version: TokenVersion + ) -> None: """Create a new token in this changes tracker.""" if self.has_token(token_id): raise NCTokenAlreadyExists @@ -94,6 +101,7 @@ def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) - token_id=token_id, token_name=token_name, token_symbol=token_symbol, + token_version=token_version, ) def has_token(self, token_id: TokenUid) -> bool: @@ -102,6 +110,13 @@ def has_token(self, token_id: TokenUid) -> bool: return True return self.storage.has_token(token_id) + def get_token(self, token_id: TokenUid) -> TokenDescription: + """Get token description for a given token ID.""" + token_description = self._created_tokens.get(token_id) + if token_description is not None: + return token_description + return self.storage.get_token(token_id) + def get_balance_diff(self) -> MappingProxyType[BalanceKey, int]: """Return the balance diff of this change tracker.""" return MappingProxyType(self._balance_diff) @@ -186,7 +201,12 @@ def commit(self) -> None: ) for td in self._created_tokens.values(): - self.storage.create_token(TokenUid(td.token_id), td.token_name, td.token_symbol) + self.storage.create_token( + token_id=TokenUid(td.token_id), + token_name=td.token_name, + token_symbol=td.token_symbol, + token_version=TokenVersion(td.token_version) + ) if self._blueprint_id is not None: self.storage.set_blueprint_id(self._blueprint_id) diff --git a/hathor/nanocontracts/storage/contract_storage.py b/hathor/nanocontracts/storage/contract_storage.py index 6dcba6a8b..7bc169fad 100644 --- a/hathor/nanocontracts/storage/contract_storage.py +++ b/hathor/nanocontracts/storage/contract_storage.py @@ -29,6 +29,7 @@ from hathor.nanocontracts.storage.types import _NOT_PROVIDED, DeletedKey, DeletedKeyType from hathor.nanocontracts.types import BlueprintId, TokenUid, VertexId from hathor.serialization import Deserializer, Serializer +from hathor.transaction.token_info import TokenDescription, TokenVersion T = TypeVar('T') D = TypeVar('D') @@ -154,9 +155,25 @@ def has_token(self, token_id: TokenUid) -> bool: """Return True if token_id exists in the current block.""" return self._token_proxy.has_token(token_id) - def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) -> None: + def get_token(self, token_id: TokenUid) -> TokenDescription: + """Get token description for a given token ID.""" + return self._token_proxy.get_token(token_id) + + def create_token( + self, + *, + token_id: TokenUid, + token_name: str, + token_symbol: str, + token_version: TokenVersion + ) -> None: """Create a new token in the current block.""" - self._token_proxy.create_token(token_id, token_name, token_symbol) + self._token_proxy.create_token( + token_id=token_id, + token_name=token_name, + token_symbol=token_symbol, + token_version=token_version + ) def lock(self) -> None: """Lock the storage for changes or commits.""" diff --git a/hathor/nanocontracts/storage/token_proxy.py b/hathor/nanocontracts/storage/token_proxy.py index 107362e3a..10306e10a 100644 --- a/hathor/nanocontracts/storage/token_proxy.py +++ b/hathor/nanocontracts/storage/token_proxy.py @@ -19,6 +19,7 @@ if TYPE_CHECKING: from hathor.nanocontracts.storage.block_storage import NCBlockStorage from hathor.nanocontracts.types import TokenUid + from hathor.transaction.token_info import TokenDescription, TokenVersion class TokenProxy: @@ -31,6 +32,22 @@ def has_token(self, token_id: TokenUid) -> bool: """Proxy to block_storage.has_token().""" return self.__block_storage.has_token(token_id) - def create_token(self, token_id: TokenUid, token_name: str, token_symbol: str) -> None: + def get_token(self, token_id: TokenUid) -> TokenDescription: + """Proxy to block_storage.get_token().""" + return self.__block_storage.get_token_description(token_id) + + def create_token( + self, + *, + token_id: TokenUid, + token_name: str, + token_symbol: str, + token_version: TokenVersion + ) -> None: """Proxy to block_storage.create_token().""" - self.__block_storage.create_token(token_id, token_name, token_symbol) + self.__block_storage.create_token( + token_id=token_id, + token_name=token_name, + token_symbol=token_symbol, + token_version=token_version + ) diff --git a/hathor/nanocontracts/syscall_token_balance_rules.py b/hathor/nanocontracts/syscall_token_balance_rules.py new file mode 100644 index 000000000..567fe27b1 --- /dev/null +++ b/hathor/nanocontracts/syscall_token_balance_rules.py @@ -0,0 +1,368 @@ +# 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 abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import StrEnum, auto, unique +from typing import TYPE_CHECKING + +from typing_extensions import Literal, assert_never + +from hathor.conf.settings import HATHOR_TOKEN_UID, HathorSettings +from hathor.nanocontracts.exception import NCInvalidFeePaymentToken +from hathor.nanocontracts.runner.types import IndexUpdateRecordType +from hathor.nanocontracts.types import TokenUid +from hathor.transaction.token_info import TokenDescription, TokenVersion +from hathor.transaction.util import get_deposit_token_deposit_amount, get_deposit_token_withdraw_amount + +if TYPE_CHECKING: + from hathor.nanocontracts.runner.types import SyscallUpdateTokenRecord + + +@unique +class TokenOperationType(StrEnum): + """Types of token operations for syscalls.""" + CREATE = auto() + MINT = auto() + MELT = auto() + + +def to_index_update_type(op_type: TokenOperationType) -> ( + Literal[IndexUpdateRecordType.MINT_TOKENS] + | Literal[IndexUpdateRecordType.MELT_TOKENS] + | Literal[IndexUpdateRecordType.CREATE_TOKEN] +): + """Convert TokenOperationType to IndexUpdateRecordType for compatibility.""" + match op_type: + case TokenOperationType.CREATE: + return IndexUpdateRecordType.CREATE_TOKEN + case TokenOperationType.MINT: + return IndexUpdateRecordType.MINT_TOKENS + case TokenOperationType.MELT: + return IndexUpdateRecordType.MELT_TOKENS + case _: + assert_never(op_type) + + +@dataclass(slots=True, kw_only=True) +class TokenSyscallBalanceEntry: + token_uid: TokenUid + amount: int + + +@dataclass(slots=True, kw_only=True) +class TokenSyscallBalance: + type: TokenOperationType + token: TokenSyscallBalanceEntry + fee_payment: TokenSyscallBalanceEntry + # create token syscall + token_version: TokenVersion | None = None + token_symbol: str | None = None + token_name: str | None = None + + def to_syscall_records(self) -> list['SyscallUpdateTokenRecord']: + """ + Convert TokenSyscallBalance to a list of SyscallUpdateTokenRecord. + + Each operation generates two records: + 1. Main token operation (mint/melt/create) + 2. Fee payment token operation + + Returns: + A list with two SyscallUpdateTokenRecord instances + """ + from hathor.nanocontracts.runner.types import SyscallUpdateTokenRecord + + operation_type = to_index_update_type(self.type) + + # First record: main token operation + main_token_record = SyscallUpdateTokenRecord( + type=operation_type, + token_uid=self.token.token_uid, + amount=self.token.amount, + token_version=self.token_version, + token_symbol=self.token_symbol, + token_name=self.token_name, + ) + + # Second record: fee payment token + fee_payment_record = SyscallUpdateTokenRecord( + type=operation_type, + token_uid=self.fee_payment.token_uid, + amount=self.fee_payment.amount, + ) + + return [main_token_record, fee_payment_record] + + +class TokenSyscallBalanceRules(ABC): + """ + An abstract base class that unifies token balance rules for syscalls. + + Requires definitions for create tokens, mint, and melt syscalls. + """ + + __slots__ = ('_settings', 'token_version', 'token_uid') + + def __init__( + self, + settings: HathorSettings, + token_uid: TokenUid, + token_version: TokenVersion + ) -> None: + self._settings = settings + self.token_version = token_version + self.token_uid = token_uid + + assert token_uid != TokenUid(HATHOR_TOKEN_UID) + assert token_version is not TokenVersion.NATIVE + + @abstractmethod + def create_token( + self, + *, + token_uid: TokenUid, + token_symbol: str, + token_name: str, + amount: int, + fee_payment_token: TokenDescription + ) -> TokenSyscallBalance: + """ + Calculate and return the token amounts needed for token creation syscalls. + + Returns: + `TokenSyscallBalance` with the token data and the amounts + """ + raise NotImplementedError + + @abstractmethod + def mint(self, amount: int, *, fee_payment_token: TokenDescription) -> TokenSyscallBalance: + """ + Calculate and return the token amounts needed for minting operations. + + Args: + amount: The amount to be minted. + fee_payment_token: The token that will be used to pay fees + + Returns: + TokenSyscallBalance: A data class with the current syscall record type, token UIDs, and + their respective amounts that will be used by the Runner class for balance updates during token minting. + """ + raise NotImplementedError + + @abstractmethod + def melt(self, amount: int, *, fee_payment_token: TokenDescription) -> TokenSyscallBalance: + """ + Calculate and return the token amounts needed for melting operations. + + Args: + amount: The amount to be melted. + fee_payment_token: The token that will be used to pay fees + + Returns: + TokenSyscallBalance: A data class with the current syscall record type, token UIDs, and + their respective amounts that will be used by the Runner class for balance updates during token melting. + """ + raise NotImplementedError + + @abstractmethod + def get_syscall_update_token_records( + self, + syscall_balance: TokenSyscallBalance + ) -> list['SyscallUpdateTokenRecord']: + """ + Create syscall update records for the given token operation. + + This method transforms a TokenSyscallBalance into a list of SyscallUpdateTokenRecord + that will be appended to the call record's index_updates for tracking token operations. + + Args: + syscall_balance: The token balance operation containing operation type, + token amounts, and payment details. + + Returns: + A list of syscall update records (main token + fee payment). + """ + raise NotImplementedError + + @staticmethod + def get_rules( + token_uid: TokenUid, + token_version: TokenVersion, + settings: HathorSettings + ) -> TokenSyscallBalanceRules: + """Get the balance rules instance for the provided token version.""" + match token_version: + case TokenVersion.DEPOSIT: + return _DepositTokenRules( + settings, + token_uid, + token_version, + ) + case TokenVersion.FEE: + return _FeeTokenRules( + settings, + token_uid, + token_version, + ) + case TokenVersion.NATIVE: + raise AssertionError(f"NATIVE token version is not supported for token {token_uid.hex()}") + case _: + assert_never(token_version) + + +class _DepositTokenRules(TokenSyscallBalanceRules): + + def create_token( + self, + *, + token_uid: TokenUid, + token_symbol: str, + token_name: str, + amount: int, + fee_payment_token: TokenDescription + ) -> TokenSyscallBalance: + assert amount > 0 + self._validate_payment_token(fee_payment_token) + htr_amount = -get_deposit_token_deposit_amount(self._settings, amount) + + return TokenSyscallBalance( + type=TokenOperationType.CREATE, + token_version=TokenVersion.DEPOSIT, + token_name=token_name, + token_symbol=token_symbol, + token=TokenSyscallBalanceEntry(token_uid=self.token_uid, amount=amount), + fee_payment=TokenSyscallBalanceEntry(token_uid=TokenUid(fee_payment_token.token_id), amount=htr_amount) + ) + + def mint(self, amount: int, *, fee_payment_token: TokenDescription) -> TokenSyscallBalance: + assert amount > 0 + self._validate_payment_token(fee_payment_token) + htr_amount = -get_deposit_token_deposit_amount(self._settings, amount) + + return TokenSyscallBalance( + type=TokenOperationType.MINT, + token=TokenSyscallBalanceEntry(token_uid=self.token_uid, amount=amount), + fee_payment=TokenSyscallBalanceEntry(token_uid=TokenUid(fee_payment_token.token_id), amount=htr_amount) + ) + + def melt(self, amount: int, *, fee_payment_token: TokenDescription) -> TokenSyscallBalance: + assert amount > 0 + self._validate_payment_token(fee_payment_token) + htr_amount = +get_deposit_token_withdraw_amount(self._settings, amount) + + return TokenSyscallBalance( + type=TokenOperationType.MELT, + token=TokenSyscallBalanceEntry(token_uid=self.token_uid, amount=-amount), + fee_payment=TokenSyscallBalanceEntry(token_uid=TokenUid(fee_payment_token.token_id), amount=htr_amount) + ) + + def get_syscall_update_token_records(self, operation: TokenSyscallBalance) -> list['SyscallUpdateTokenRecord']: + match operation.type: + case TokenOperationType.MINT | TokenOperationType.CREATE: + assert operation.token.amount > 0 and operation.fee_payment.amount < 0 + case TokenOperationType.MELT: + assert operation.token.amount < 0 and operation.fee_payment.amount > 0 + case _: + assert_never(operation.type) + + return operation.to_syscall_records() + + def _validate_payment_token(self, token: TokenDescription) -> bool: + if token.token_id == TokenUid(HATHOR_TOKEN_UID): + return True + raise NCInvalidFeePaymentToken("Only HTR is allowed to be used with deposit based token syscalls") + + +class _FeeTokenRules(TokenSyscallBalanceRules): + + def _get_fee_amount(self, fee_payment_token: TokenUid) -> int: + # For fee tokens, we only need to pay the transaction fee, not deposit HTR + if fee_payment_token == TokenUid(HATHOR_TOKEN_UID): + fee_amount = -self._settings.FEE_PER_OUTPUT + else: + fee_amount = -int(self._settings.FEE_PER_OUTPUT / self._settings.TOKEN_DEPOSIT_PERCENTAGE) + + assert fee_amount < 0 + return fee_amount + + def create_token( + self, + *, + token_uid: TokenUid, + token_symbol: str, + token_name: str, + amount: int, + fee_payment_token: TokenDescription + ) -> TokenSyscallBalance: + assert amount > 0 + self._validate_payment_token(fee_payment_token) + # For fee tokens, we only need to pay the transaction fee, not deposit HTR + fee_amount = self._get_fee_amount(TokenUid(fee_payment_token.token_id)) + + return TokenSyscallBalance( + type=TokenOperationType.CREATE, + token_version=TokenVersion.FEE, + token_name=token_name, + token_symbol=token_symbol, + token=TokenSyscallBalanceEntry(token_uid=self.token_uid, amount=amount), + fee_payment=TokenSyscallBalanceEntry(token_uid=TokenUid(fee_payment_token.token_id), amount=fee_amount) + ) + + def mint(self, amount: int, *, fee_payment_token: TokenDescription) -> TokenSyscallBalance: + assert amount > 0 + self._validate_payment_token(fee_payment_token) + fee_amount = self._get_fee_amount(TokenUid(fee_payment_token.token_id)) + return TokenSyscallBalance( + type=TokenOperationType.MINT, + token=TokenSyscallBalanceEntry(token_uid=self.token_uid, amount=amount), + fee_payment=TokenSyscallBalanceEntry(token_uid=TokenUid(fee_payment_token.token_id), amount=fee_amount) + ) + + def melt(self, amount: int, *, fee_payment_token: TokenDescription) -> TokenSyscallBalance: + assert amount > 0 + self._validate_payment_token(fee_payment_token) + fee_amount = self._get_fee_amount(TokenUid(fee_payment_token.token_id)) + + return TokenSyscallBalance( + type=TokenOperationType.MELT, + token=TokenSyscallBalanceEntry(token_uid=self.token_uid, amount=-amount), + fee_payment=TokenSyscallBalanceEntry(token_uid=TokenUid(fee_payment_token.token_id), amount=fee_amount) + ) + + def get_syscall_update_token_records(self, operation: TokenSyscallBalance) -> list['SyscallUpdateTokenRecord']: + assert operation.fee_payment.amount < 0 + + match operation.type: + case TokenOperationType.MINT | TokenOperationType.CREATE: + assert operation.token.amount > 0 + case TokenOperationType.MELT: + assert operation.token.amount < 0 + case _: + assert_never(operation.type) + + return operation.to_syscall_records() + + def _validate_payment_token(self, token_info: TokenDescription) -> None: + match token_info.token_version: + case TokenVersion.FEE: + raise NCInvalidFeePaymentToken("fee-based tokens aren't allowed for paying fees") + case TokenVersion.DEPOSIT: + pass + case TokenVersion.NATIVE: + pass + case _: + assert_never(token_info.token_version) diff --git a/hathor/transaction/token_info.py b/hathor/transaction/token_info.py index 94cd8f293..6970863df 100644 --- a/hathor/transaction/token_info.py +++ b/hathor/transaction/token_info.py @@ -76,6 +76,7 @@ class TokenDescription: token_id: bytes token_name: str token_symbol: str + token_version: TokenVersion class TokenInfoDict(dict[TokenUid, TokenInfo]): diff --git a/tests/nanocontracts/test_execution_order.py b/tests/nanocontracts/test_execution_order.py index c168dcc2b..3b5f64177 100644 --- a/tests/nanocontracts/test_execution_order.py +++ b/tests/nanocontracts/test_execution_order.py @@ -152,6 +152,16 @@ def test_deposit_and_withdrawal(self) -> None: self.runner.call_public_method(self.contract_id1, 'withdrawal', self._get_context(action)) def test_mint_and_melt(self) -> None: + # First create the token so it exists in the system + from hathor.transaction.token_info import TokenVersion + changes_tracker = self.runner.get_storage(self.contract_id1) + changes_tracker.create_token( + token_id=self.token_a, + token_name="Test Token", + token_symbol="TST", + token_version=TokenVersion.DEPOSIT + ) + action: NCAction = NCGrantAuthorityAction(token_uid=self.token_a, mint=True, melt=False) self.runner.call_public_method(self.contract_id1, 'mint', self._get_context(action)) diff --git a/tests/nanocontracts/test_syscalls.py b/tests/nanocontracts/test_syscalls.py index 5f19484ee..f836e7d22 100644 --- a/tests/nanocontracts/test_syscalls.py +++ b/tests/nanocontracts/test_syscalls.py @@ -5,7 +5,7 @@ from hathor.conf.settings import HATHOR_TOKEN_UID from hathor.nanocontracts.blueprint import Blueprint from hathor.nanocontracts.context import Context -from hathor.nanocontracts.exception import NCInvalidSyscall +from hathor.nanocontracts.exception import NCInsufficientFunds, NCInvalidSyscall from hathor.nanocontracts.nc_types import NCType, make_nc_type_for_arg_type as make_nc_type from hathor.nanocontracts.storage.contract_storage import Balance, BalanceKey from hathor.nanocontracts.types import ( @@ -16,6 +16,7 @@ TokenUid, public, ) +from hathor.transaction.token_info import TokenVersion from tests.nanocontracts.blueprints.unittest import BlueprintTestCase CONTRACT_NC_TYPE = make_nc_type(ContractId) @@ -55,11 +56,42 @@ def revoke(self, ctx: Context, token_uid: TokenUid, revoke_mint: bool, revoke_me @public def mint(self, ctx: Context, token_uid: TokenUid, amount: int) -> None: - self.syscall.mint_tokens(token_uid, amount) + self.syscall.mint_tokens(token_uid, amount=amount) @public def melt(self, ctx: Context, token_uid: TokenUid, amount: int) -> None: - self.syscall.melt_tokens(token_uid, amount) + self.syscall.melt_tokens(token_uid, amount=amount) + + +class FeeTokenBlueprint(Blueprint): + + @public(allow_deposit=True, allow_grant_authority=True) + def initialize(self, ctx: Context) -> None: + pass + + @public(allow_deposit=True, allow_grant_authority=True) + def create_fee_token(self, ctx: Context, name: str, symbol: str, amount: int, + fee_payment_token: TokenUid) -> TokenUid: + token_uid = self.syscall.create_fee_token( + name, + symbol, + amount, + mint_authority=True, + melt_authority=True, + fee_payment_token=fee_payment_token) + return token_uid + + @public(allow_deposit=True, allow_grant_authority=True) + def create_deposit_token(self, ctx: Context, name: str, symbol: str, amount: int) -> TokenUid: + return self.syscall.create_deposit_token(name, symbol, amount) + + @public(allow_deposit=True) + def mint(self, ctx: Context, token: TokenUid, amount: int, fee_payment_token: TokenUid) -> None: + self.syscall.mint_tokens(token, amount=amount, fee_payment_token=fee_payment_token) + + @public(allow_deposit=True) + def melt(self, ctx: Context, token: TokenUid, amount: int, fee_payment_token: TokenUid) -> None: + self.syscall.melt_tokens(token, amount=amount, fee_payment_token=fee_payment_token) class NCNanoContractTestCase(BlueprintTestCase): @@ -68,9 +100,11 @@ def setUp(self) -> None: self.my_blueprint_id = self.gen_random_blueprint_id() self.other_blueprint_id = self.gen_random_blueprint_id() + self.fee_blueprint_id = self.gen_random_blueprint_id() self.nc_catalog.blueprints[self.my_blueprint_id] = MyBlueprint self.nc_catalog.blueprints[self.other_blueprint_id] = OtherBlueprint + self.nc_catalog.blueprints[self.fee_blueprint_id] = FeeTokenBlueprint def test_basics(self) -> None: nc1_id = self.gen_random_contract_id() @@ -89,26 +123,34 @@ def test_basics(self) -> None: assert storage2.get_obj(b'other_blueprint_id', OPT_BLUEPRINT_NC_TYPE) == self.other_blueprint_id def test_authorities(self) -> None: + # Dummy contract just to create the token before it's used below + aux_nc_id = self.gen_random_contract_id() + self.runner.create_contract(aux_nc_id, self.other_blueprint_id, self.create_context()) + dbt_token_uid = self.gen_random_token_uid() + storage = self.runner.get_storage(aux_nc_id) + storage.create_token( + token_id=dbt_token_uid, + token_name="Test Token", + token_symbol="TST", + token_version=TokenVersion.DEPOSIT + ) + nc_id = self.gen_random_contract_id() - token_a_uid = self.gen_random_token_uid() htr_balance_key = BalanceKey(nc_id=nc_id, token_uid=HATHOR_TOKEN_UID) - tka_balance_key = BalanceKey(nc_id=nc_id, token_uid=token_a_uid) + dbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=dbt_token_uid) ctx_initialize = self.create_context( actions=[ NCDepositAction(token_uid=TokenUid(HATHOR_TOKEN_UID), amount=1000), - NCDepositAction(token_uid=token_a_uid, amount=1000), + NCDepositAction(token_uid=dbt_token_uid, amount=1000), ], - vertex=self.get_genesis_tx(), - caller_id=self.gen_random_address(), - timestamp=0, ) self.runner.create_contract(nc_id, self.other_blueprint_id, ctx_initialize) storage = self.runner.get_storage(nc_id) ctx_grant = self.create_context( - actions=[NCGrantAuthorityAction(token_uid=token_a_uid, mint=True, melt=True)], + actions=[NCGrantAuthorityAction(token_uid=dbt_token_uid, mint=True, melt=True)], vertex=self.get_genesis_tx(), caller_id=self.gen_random_address(), timestamp=0, @@ -125,56 +167,56 @@ def test_authorities(self) -> None: # Starting state assert storage.get_all_balances() == { htr_balance_key: Balance(value=1000, can_mint=False, can_melt=False), - tka_balance_key: Balance(value=1000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=1000, can_mint=True, can_melt=True), } # After mint - self.runner.call_public_method(nc_id, 'mint', ctx, token_a_uid, 123) + self.runner.call_public_method(nc_id, 'mint', ctx, dbt_token_uid, 123) assert storage.get_all_balances() == { htr_balance_key: Balance(value=998, can_mint=False, can_melt=False), - tka_balance_key: Balance(value=1123, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=1123, can_mint=True, can_melt=True), } # After melt - self.runner.call_public_method(nc_id, 'melt', ctx, token_a_uid, 456) + self.runner.call_public_method(nc_id, 'melt', ctx, dbt_token_uid, 456) assert storage.get_all_balances() == { htr_balance_key: Balance(value=1002, can_mint=False, can_melt=False), - tka_balance_key: Balance(value=667, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=667, can_mint=True, can_melt=True), } # After revoke mint - self.runner.call_public_method(nc_id, 'revoke', ctx, token_a_uid, True, False) + self.runner.call_public_method(nc_id, 'revoke', ctx, dbt_token_uid, True, False) assert storage.get_all_balances() == { htr_balance_key: Balance(value=1002, can_mint=False, can_melt=False), - tka_balance_key: Balance(value=667, can_mint=False, can_melt=True), + dbt_balance_key: Balance(value=667, can_mint=False, can_melt=True), } # After revoke melt - self.runner.call_public_method(nc_id, 'revoke', ctx, token_a_uid, False, True) + self.runner.call_public_method(nc_id, 'revoke', ctx, dbt_token_uid, False, True) assert storage.get_all_balances() == { htr_balance_key: Balance(value=1002, can_mint=False, can_melt=False), - tka_balance_key: Balance(value=667, can_mint=False, can_melt=False), + dbt_balance_key: Balance(value=667, can_mint=False, can_melt=False), } # Try revoke mint without having the authority - msg = f'contract {nc_id.hex()} cannot mint {token_a_uid.hex()} tokens' + msg = f'contract {nc_id.hex()} cannot mint {dbt_token_uid.hex()} tokens' with pytest.raises(NCInvalidSyscall, match=msg): - self.runner.call_public_method(nc_id, 'revoke', ctx, token_a_uid, True, False) + self.runner.call_public_method(nc_id, 'revoke', ctx, dbt_token_uid, True, False) # Try revoke melt without having the authority - msg = f'contract {nc_id.hex()} cannot melt {token_a_uid.hex()} tokens' + msg = f'contract {nc_id.hex()} cannot melt {dbt_token_uid.hex()} tokens' with pytest.raises(NCInvalidSyscall, match=msg): - self.runner.call_public_method(nc_id, 'revoke', ctx, token_a_uid, False, True) + self.runner.call_public_method(nc_id, 'revoke', ctx, dbt_token_uid, False, True) # Try mint TKA - msg = f'contract {nc_id.hex()} cannot mint {token_a_uid.hex()} tokens' + msg = f'contract {nc_id.hex()} cannot mint {dbt_token_uid.hex()} tokens' with pytest.raises(NCInvalidSyscall, match=msg): - self.runner.call_public_method(nc_id, 'mint', ctx, token_a_uid, 123) + self.runner.call_public_method(nc_id, 'mint', ctx, dbt_token_uid, 123) # Try melt TKA - msg = f'contract {nc_id.hex()} cannot melt {token_a_uid.hex()} tokens' + msg = f'contract {nc_id.hex()} cannot melt {dbt_token_uid.hex()} tokens' with pytest.raises(NCInvalidSyscall, match=msg): - self.runner.call_public_method(nc_id, 'melt', ctx, token_a_uid, 456) + self.runner.call_public_method(nc_id, 'melt', ctx, dbt_token_uid, 456) # Try mint HTR with pytest.raises(NCInvalidSyscall, match=f'contract {nc_id.hex()} cannot mint HTR tokens'): @@ -191,5 +233,405 @@ def test_authorities(self) -> None: # Final state assert storage.get_all_balances() == { htr_balance_key: Balance(value=1002, can_mint=False, can_melt=False), - tka_balance_key: Balance(value=667, can_mint=False, can_melt=False), + dbt_balance_key: Balance(value=667, can_mint=False, can_melt=False), + } + + def test_deposit_token_creation(self) -> None: + nc_id = self.gen_random_contract_id() + + ctx_initialize = self.create_context([], self.get_genesis_tx()) + + self.runner.create_contract(nc_id, self.fee_blueprint_id, ctx_initialize) + storage = self.runner.get_storage(nc_id) + + # Try to create a token with negative amount + msg = 'token amount must be always positive. amount=-10' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'create_deposit_token', + self.create_context(), + 'DBT', + 'DBT', + -10, + ) + + msg = 'token amount must be always positive. amount=0' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'create_deposit_token', + self.create_context(), + 'DBT', + 'DBT', + 0, + ) + + # created fee token paying with deposit token + assert storage.get_all_balances() == {} + + def test_fee_token_creation(self) -> None: + nc_id = self.gen_random_contract_id() + + ctx_initialize = self.create_context([], self.get_genesis_tx()) + + self.runner.create_contract(nc_id, self.fee_blueprint_id, ctx_initialize) + storage = self.runner.get_storage(nc_id) + + # Starting state + assert storage.get_all_balances() == {} + + ctx_create_token = self.create_context( + [NCDepositAction(token_uid=TokenUid(HATHOR_TOKEN_UID), amount=2)], + self.get_genesis_tx() + ) + + token_uid = self.runner.call_public_method(nc_id, 'create_fee_token', ctx_create_token, + 'FeeToken', 'FBT', 1000000, TokenUid(HATHOR_TOKEN_UID)) + + htr_balance_key = BalanceKey(nc_id=nc_id, token_uid=HATHOR_TOKEN_UID) + fbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=token_uid) + + # fee token creation charging 1 HTR + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=1, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + } + + ctx_create_deposit_token = self.create_context() + dbt_token_uid = self.runner.call_public_method(nc_id, 'create_deposit_token', + ctx_create_deposit_token, 'DepositToken', 'DBT', 100) + + dbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=dbt_token_uid) + + # deposit token creation charging 1 HTR + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=100, can_mint=True, can_melt=True), + } + + fbt_token2_uid = self.runner.call_public_method(nc_id, 'create_fee_token', self.create_context(), + 'FeeToken2', 'FB2', 1000000, dbt_token_uid) + fbt2_balance_key = BalanceKey(nc_id=nc_id, token_uid=fbt_token2_uid) + + # created fee token paying with deposit token + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=0, can_mint=True, can_melt=True), + fbt2_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + } + + # Try to create fee tokens without enough dbt balances + msg = f'negative balance for contract {nc_id.hex()}' + with pytest.raises(NCInsufficientFunds, match=msg): + self.runner.call_public_method( + nc_id, + 'create_fee_token', + self.create_context(), + 'FeeToken3', + 'FB3', + 1000000, + dbt_token_uid + ) + + # Try to create a token with negative amount + msg = 'token amount must be always positive. amount=-10' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'create_fee_token', + self.create_context(), + 'FeeToken3', + 'FB3', + -10, + TokenUid(HATHOR_TOKEN_UID) + ) + + msg = 'token amount must be always positive. amount=0' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'create_fee_token', + self.create_context(), + 'FeeToken3', + 'FB3', + 0, + TokenUid(HATHOR_TOKEN_UID) + ) + + # created fee token paying with deposit token + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=0, can_mint=True, can_melt=True), + fbt2_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + } + + def test_fee_token_melt(self) -> None: + nc_id = self.gen_random_contract_id() + + ctx_initialize = self.create_context([], self.get_genesis_tx()) + + self.runner.create_contract(nc_id, self.fee_blueprint_id, ctx_initialize) + storage = self.runner.get_storage(nc_id) + + # Starting state + assert storage.get_all_balances() == {} + + # Create a fee token first so we have something to melt + ctx_create_token = self.create_context( + [NCDepositAction(token_uid=TokenUid(HATHOR_TOKEN_UID), amount=2)], + self.get_genesis_tx() + ) + + token_uid = self.runner.call_public_method(nc_id, 'create_fee_token', ctx_create_token, + 'FeeToken', 'FBT', 1000000, TokenUid(HATHOR_TOKEN_UID)) + + htr_balance_key = BalanceKey(nc_id=nc_id, token_uid=HATHOR_TOKEN_UID) + fbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=token_uid) + + ctx_create_deposit_token = self.create_context() + dbt_token_uid = self.runner.call_public_method(nc_id, 'create_deposit_token', + ctx_create_deposit_token, 'DepositToken', 'DBT', 100) + + dbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=dbt_token_uid) + + # fee token creation charging 1 HTR, creating 1000000 tokens + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=100, can_mint=True, can_melt=True), + } + + # Successfully melt some tokens - don't deposit, melt from existing balance using deposit token + self.runner.call_public_method(nc_id, 'melt', self.create_context(), token_uid, 500000, dbt_token_uid) + + # Balance should decrease by melted amount, HTR consumed for fee + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=500000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=0, can_mint=True, can_melt=True), + } + + # Try to melt more tokens - should fail due to insufficient HTR for fee payment + msg = f'negative balance for contract {nc_id.hex()}' + with pytest.raises(NCInsufficientFunds, match=msg): + self.runner.call_public_method( + nc_id, + 'melt', + self.create_context(), + token_uid, + 1, + TokenUid(HATHOR_TOKEN_UID) + ) + + # Balance should remain unchanged after failed melt attempt + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=500000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=0, can_mint=True, can_melt=True), + } + + # Try to melt a deposit token paying with another deposit token + from hathor.nanocontracts.exception import NCInvalidFeePaymentToken + msg = 'Only HTR is allowed to be used with deposit based token syscalls' + with pytest.raises(NCInvalidFeePaymentToken, match=msg): + self.runner.call_public_method(nc_id, 'melt', self.create_context(), dbt_token_uid, 1, dbt_token_uid) + + # Try to melt a token with negative amount + msg = 'token amount must be always positive. amount=-10' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'melt', + self.create_context(), + dbt_token_uid, + -10, # negative amount + TokenUid(HATHOR_TOKEN_UID) + ) + + msg = 'token amount must be always positive. amount=-10' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'melt', + self.create_context(), + dbt_token_uid, + -10, # negative amount + TokenUid(HATHOR_TOKEN_UID) + ) + + def test_fee_token_mint(self) -> None: + nc_id = self.gen_random_contract_id() + + ctx_initialize = self.create_context([], self.get_genesis_tx()) + + self.runner.create_contract(nc_id, self.fee_blueprint_id, ctx_initialize) + storage = self.runner.get_storage(nc_id) + + # Starting state + assert storage.get_all_balances() == {} + + # Create a fee token first so we have something to mint to + ctx_create_token = self.create_context( + [NCDepositAction(token_uid=TokenUid(HATHOR_TOKEN_UID), amount=6)], + self.get_genesis_tx() + ) + + token_uid = self.runner.call_public_method(nc_id, 'create_fee_token', ctx_create_token, + 'FeeToken', 'FBT', 1000000, TokenUid(HATHOR_TOKEN_UID)) + + # Create a deposit token to use as fee payment + dbt_token_uid = self.runner.call_public_method(nc_id, 'create_deposit_token', self.create_context(), + 'DepositToken', 'DBT', 500) + + htr_balance_key = BalanceKey(nc_id=nc_id, token_uid=HATHOR_TOKEN_UID) + fbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=token_uid) + dbt_balance_key = BalanceKey(nc_id=nc_id, token_uid=dbt_token_uid) + + # After token creation: HTR consumed by token creation fees and deposit amounts + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=500, can_mint=True, can_melt=True), + } + + # Successfully mint tokens using deposit token as fee payment (no HTR left) + self.runner.call_public_method(nc_id, 'mint', self.create_context(), token_uid, 100000, dbt_token_uid) + + # Balance should increase by minted amount, deposit token consumed for fee + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1100000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=400, can_mint=True, can_melt=True), + } + + # Successfully mint more tokens using deposit token as fee payment + self.runner.call_public_method(nc_id, 'mint', self.create_context(), token_uid, 200000, dbt_token_uid) + + # Balance should increase, deposit token consumed for fee + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1300000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=300, can_mint=True, can_melt=True), + } + + # Drain remaining deposit tokens + self.runner.call_public_method(nc_id, 'mint', self.create_context(), token_uid, 50000, dbt_token_uid) + self.runner.call_public_method(nc_id, 'mint', self.create_context(), token_uid, 50000, dbt_token_uid) + self.runner.call_public_method(nc_id, 'mint', self.create_context(), token_uid, 50000, dbt_token_uid) + + # All deposit tokens should be consumed + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1450000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=0, can_mint=True, can_melt=True), + } + + # Try to mint with insufficient deposit tokens for fee payment - should fail + msg = f'negative balance for contract {nc_id.hex()}' + with pytest.raises(NCInsufficientFunds, match=msg): + self.runner.call_public_method(nc_id, 'mint', self.create_context(), token_uid, 1, dbt_token_uid) + + # Try to mint with insufficient HTR for fee payment - should also fail + with pytest.raises(NCInsufficientFunds, match=msg): + self.runner.call_public_method( + nc_id, + 'mint', + self.create_context(), + token_uid, + 1, + TokenUid(HATHOR_TOKEN_UID) + ) + + # Balance should remain unchanged after failed mint attempts + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=0, can_mint=False, can_melt=False), + fbt_balance_key: Balance(value=1450000, can_mint=True, can_melt=True), + dbt_balance_key: Balance(value=0, can_mint=True, can_melt=True), + } + + # Try to mint a deposit token paying with another deposit token + from hathor.nanocontracts.exception import NCInvalidFeePaymentToken + msg = 'Only HTR is allowed to be used with deposit based token syscalls' + with pytest.raises(NCInvalidFeePaymentToken, match=msg): + self.runner.call_public_method(nc_id, 'mint', self.create_context(), dbt_token_uid, 1, dbt_token_uid) + + # Try to mint a token with negative amount + msg = 'token amount must be always positive. amount=-10' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'mint', + self.create_context(), + dbt_token_uid, + -10, # negative amount + TokenUid(HATHOR_TOKEN_UID) + ) + + # Try to mint a token with negative amount + msg = 'token amount must be always positive. amount=0' + with pytest.raises(NCInvalidSyscall, match=msg): + self.runner.call_public_method( + nc_id, + 'mint', + self.create_context(), + dbt_token_uid, + 0, # negative amount + TokenUid(HATHOR_TOKEN_UID) + ) + + def test_fee_token_as_payment_rejected(self) -> None: + """Test that fee tokens cannot be used as payment tokens for fee operations.""" + nc_id = self.gen_random_contract_id() + + # Initialize contract with HTR deposit to create the first fee token + ctx_initialize = self.create_context( + [NCDepositAction(token_uid=TokenUid(HATHOR_TOKEN_UID), amount=10)], + self.get_genesis_tx() + ) + self.runner.create_contract(nc_id, self.fee_blueprint_id, ctx_initialize) + storage = self.runner.get_storage(nc_id) + + # Create a fee token using HTR as payment + fee_token_uid = self.runner.call_public_method( + nc_id, 'create_fee_token', self.create_context(), + 'FeeToken1', 'FT1', 1000000, TokenUid(HATHOR_TOKEN_UID) + ) + + htr_balance_key = BalanceKey(nc_id=nc_id, token_uid=HATHOR_TOKEN_UID) + ft1_balance_key = BalanceKey(nc_id=nc_id, token_uid=fee_token_uid) + + # After first fee token creation + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=9, can_mint=False, can_melt=False), + ft1_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), + } + + # Try to create another fee token using the first fee token as payment - should be rejected + from hathor.nanocontracts.exception import NCInvalidFeePaymentToken + with pytest.raises(NCInvalidFeePaymentToken, match="fee-based tokens aren't allowed for paying fees"): + self.runner.call_public_method( + nc_id, 'create_fee_token', self.create_context(), + 'FeeToken2', 'FT2', 500000, fee_token_uid + ) + + # Also test that fee tokens cannot be used as payment for minting + with pytest.raises(NCInvalidFeePaymentToken, match="fee-based tokens aren't allowed for paying fees"): + self.runner.call_public_method( + nc_id, 'mint', self.create_context(), fee_token_uid, 100, fee_token_uid + ) + + # Also test that fee tokens cannot be used as payment for melting + with pytest.raises(NCInvalidFeePaymentToken, match="fee-based tokens aren't allowed for paying fees"): + self.runner.call_public_method( + nc_id, 'melt', self.create_context(), fee_token_uid, 100, fee_token_uid + ) + + # Balance should remain unchanged after failed attempts + assert storage.get_all_balances() == { + htr_balance_key: Balance(value=9, can_mint=False, can_melt=False), + ft1_balance_key: Balance(value=1000000, can_mint=True, can_melt=True), } diff --git a/tests/nanocontracts/test_syscalls_in_view.py b/tests/nanocontracts/test_syscalls_in_view.py index 5a6ce0563..ff7f0bedc 100644 --- a/tests/nanocontracts/test_syscalls_in_view.py +++ b/tests/nanocontracts/test_syscalls_in_view.py @@ -83,11 +83,11 @@ def revoke_authorities(self) -> None: @view def mint_tokens(self) -> None: - self.syscall.mint_tokens(TokenUid(b''), 0) + self.syscall.mint_tokens(TokenUid(b''), amount=0) @view def melt_tokens(self) -> None: - self.syscall.melt_tokens(TokenUid(b''), 0) + self.syscall.melt_tokens(TokenUid(b''), amount=0) @view def create_contract(self) -> None: diff --git a/tests/nanocontracts/test_token_creation.py b/tests/nanocontracts/test_token_creation.py index 076e6c014..dc4d85fe3 100644 --- a/tests/nanocontracts/test_token_creation.py +++ b/tests/nanocontracts/test_token_creation.py @@ -10,7 +10,7 @@ from hathor.nanocontracts.utils import derive_child_token_id from hathor.transaction import Block, Transaction from hathor.transaction.nc_execution_state import NCExecutionState -from hathor.transaction.token_info import TokenDescription +from hathor.transaction.token_info import TokenDescription, TokenVersion from tests import unittest from tests.dag_builder.builder import TestDAGBuilder from tests.nanocontracts.utils import assert_nc_failure_reason @@ -41,7 +41,14 @@ def create_deposit_token( mint_authority: bool, melt_authority: bool, ) -> None: - self.syscall.create_deposit_token(token_name, token_symbol, amount, mint_authority, melt_authority, salt=salt) + self.syscall.create_deposit_token( + token_name, + token_symbol, + amount, + mint_authority=mint_authority, + melt_authority=melt_authority, + salt=salt + ) class NCNanoContractTestCase(unittest.TestCase): @@ -240,11 +247,13 @@ def test_token_creation_by_contract(self) -> None: token_id=child_token_id0, token_name='MyToken', token_symbol=token_symbol, + token_version=TokenVersion.DEPOSIT, ) assert block_storage.get_token_description(child_token_id1) == TokenDescription( token_id=child_token_id1, token_name='MyToken', token_symbol=token_symbol, + token_version=TokenVersion.DEPOSIT, ) nc_storage = block_storage.get_contract_storage(tx1.hash)