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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion hathor/nanocontracts/runner/index_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ def to_json(self) -> dict[str, Any]:

@classmethod
def from_json(cls, json_dict: dict[str, Any]) -> Self:
token_version = TokenVersion(json_dict['token_version'])
assert token_version in (TokenVersion.DEPOSIT, TokenVersion.FEE)
return cls(
token_uid=TokenUid(VertexId(bytes.fromhex(json_dict['token_uid']))),
amount=json_dict['amount'],
token_version=json_dict['token_version'],
token_version=token_version, # type: ignore[arg-type]
token_name=json_dict['token_name'],
token_symbol=json_dict['token_symbol'],
)
Expand Down
130 changes: 89 additions & 41 deletions hathor/nanocontracts/runner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
UpdateAuthoritiesRecord,
UpdateTokenBalanceRecord,
)
from hathor.nanocontracts.runner.token_fees import calculate_melt_fee, calculate_mint_fee
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,
Expand Down Expand Up @@ -475,9 +475,9 @@ def _unsafe_call_another_contract_public_method(
# execution, the verification of the tokens and amounts will be done after it
for fee in fees:
assert fee.amount > 0
self._update_tokens_amount([
UpdateTokenBalanceRecord(token_uid=fee.token_uid, amount=-fee.amount)
])
self._update_tokens_amount(
fee=UpdateTokenBalanceRecord(token_uid=fee.token_uid, amount=-fee.amount),
)
self._register_paid_fee(fee.token_uid, fee.amount)

ctx_actions = Context.__group_actions__(actions)
Expand Down Expand Up @@ -1033,14 +1033,19 @@ def syscall_mint_tokens(
if not balance.can_mint:
raise NCInvalidSyscall(f'contract {call_record.contract_id.hex()} cannot mint {token_uid.hex()} tokens')

fee_payment_token_info = self._get_token(fee_payment_token)
token_info = self._get_token(token_uid)
fee_amount = calculate_mint_fee(
settings=self._settings,
token_version=token_info.token_version,
amount=amount,
fee_payment_token=self._get_token(fee_payment_token),
)

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._update_tokens_amount(records)
assert amount > 0 and fee_amount < 0
self._update_tokens_amount(
operation=UpdateTokenBalanceRecord(token_uid=token_uid, amount=amount),
fee=UpdateTokenBalanceRecord(token_uid=fee_payment_token, amount=fee_amount),
)

@_forbid_syscall_from_view('melt_tokens')
def syscall_melt_tokens(
Expand Down Expand Up @@ -1068,13 +1073,28 @@ def syscall_melt_tokens(
raise NCInvalidSyscall(f'contract {call_record.contract_id.hex()} cannot melt {token_uid.hex()} tokens')

token_info = self._get_token(token_uid)
fee_payment_token_info = self._get_token(fee_payment_token)

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)
fee_amount = calculate_melt_fee(
settings=self._settings,
token_version=token_info.token_version,
amount=amount,
fee_payment_token=self._get_token(fee_payment_token),
)

self._update_tokens_amount(records)
assert amount > 0
match token_info.token_version:
case TokenVersion.NATIVE:
raise AssertionError
case TokenVersion.DEPOSIT:
assert fee_amount > 0
case TokenVersion.FEE:
assert fee_amount < 0
case _: # pragma: no cover
assert_never(token_info.token_version)

self._update_tokens_amount(
operation=UpdateTokenBalanceRecord(token_uid=token_uid, amount=-amount),
fee=UpdateTokenBalanceRecord(token_uid=fee_payment_token, amount=fee_amount),
)

def _validate_context(self, ctx: Context) -> None:
"""Check whether the context is valid."""
Expand Down Expand Up @@ -1154,17 +1174,14 @@ def syscall_create_child_deposit_token(
grant_melt=melt_authority,
)

syscall_rules = TokenSyscallBalanceRules.get_rules(token_id, token_version, self._settings)
syscall_balance = syscall_rules.create_token(
self._create_token(
token_version=token_version,
token_uid=token_id,
token_symbol=token_symbol,
token_name=token_name,
amount=amount,
fee_payment_token=self._get_token(TokenUid(HATHOR_TOKEN_UID))
fee_payment_token=self._get_token(TokenUid(HATHOR_TOKEN_UID)),
token_name=token_name,
token_symbol=token_symbol,
)
records = syscall_rules.get_syscall_update_token_records(syscall_balance)

self._update_tokens_amount(records)

return token_id

Expand Down Expand Up @@ -1194,7 +1211,6 @@ def syscall_create_child_fee_token(
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

Expand All @@ -1210,17 +1226,15 @@ def syscall_create_child_fee_token(
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(

self._create_token(
token_version=token_version,
token_uid=token_id,
amount=amount,
fee_payment_token=self._get_token(fee_payment_token),
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

Expand Down Expand Up @@ -1289,28 +1303,62 @@ def _get_token(self, token_uid: TokenUid) -> TokenDescription:
token_id=token_creation_tx.hash
)

def _update_tokens_amount(self, records: list[UpdateTokenBalanceRecord | CreateTokenRecord]) -> None:
def _create_token(
self,
*,
token_version: TokenVersion,
token_uid: TokenUid,
amount: int,
fee_payment_token: TokenDescription,
token_symbol: str,
token_name: str,
) -> None:
"""Create a new token."""
assert token_version in (TokenVersion.DEPOSIT, TokenVersion.FEE)
fee_amount = calculate_mint_fee(
settings=self._settings,
token_version=token_version,
amount=amount,
fee_payment_token=fee_payment_token,
)
assert amount > 0 and fee_amount < 0
self._update_tokens_amount(
operation=CreateTokenRecord(
token_uid=token_uid,
amount=amount,
token_version=token_version, # type: ignore[arg-type]
token_symbol=token_symbol,
token_name=token_name,
),
fee=UpdateTokenBalanceRecord(
token_uid=TokenUid(fee_payment_token.token_id),
amount=fee_amount,
),
)

def _update_tokens_amount(
self,
*,
operation: UpdateTokenBalanceRecord | CreateTokenRecord | None = None,
fee: UpdateTokenBalanceRecord | None = None,
) -> 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()

assert operation or fee
assert changes_tracker.nc_id == call_record.contract_id
assert call_record.index_updates is not None

for record in records:
for record in (operation, fee):
if record is None:
continue
changes_tracker.add_balance(record.token_uid, record.amount)
self._updated_tokens_totals[record.token_uid] += record.amount
call_record.index_updates.append(record)
Expand Down
88 changes: 88 additions & 0 deletions hathor/nanocontracts/runner/token_fees.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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 typing_extensions import assert_never

from hathor.conf.settings import HathorSettings
from hathor.nanocontracts.exception import NCInvalidFeePaymentToken
from hathor.transaction.token_info import TokenDescription, TokenVersion
from hathor.transaction.util import get_deposit_token_deposit_amount, get_deposit_token_withdraw_amount


def calculate_mint_fee(
*,
settings: HathorSettings,
token_version: TokenVersion,
amount: int,
fee_payment_token: TokenDescription,
) -> int:
"""Calculate the fee for a mint operation."""
match token_version:
case TokenVersion.NATIVE:
raise AssertionError
case TokenVersion.DEPOSIT:
_validate_deposit_based_payment_token(fee_payment_token)
return -get_deposit_token_deposit_amount(settings, amount)
case TokenVersion.FEE:
_validate_fee_based_payment_token(fee_payment_token)
return -_calculate_unit_fee_token_fee(settings, fee_payment_token)
case _: # pragma: no cover
assert_never(token_version)


def calculate_melt_fee(
*,
settings: HathorSettings,
token_version: TokenVersion,
amount: int,
fee_payment_token: TokenDescription,
) -> int:
"""Calculate the fee for a melt operation."""
match token_version:
case TokenVersion.NATIVE:
raise AssertionError
case TokenVersion.DEPOSIT:
_validate_deposit_based_payment_token(fee_payment_token)
return +get_deposit_token_withdraw_amount(settings, amount)
case TokenVersion.FEE:
_validate_fee_based_payment_token(fee_payment_token)
return -_calculate_unit_fee_token_fee(settings, fee_payment_token)
case _: # pragma: no cover
assert_never(token_version)


def _validate_deposit_based_payment_token(fee_payment_token: TokenDescription) -> None:
"""Validate the token used to pay the fee of a deposit-based token operation."""
from hathor import HATHOR_TOKEN_UID
if fee_payment_token.token_id != HATHOR_TOKEN_UID:
raise NCInvalidFeePaymentToken('Only HTR is allowed to be used with deposit based token syscalls')


def _validate_fee_based_payment_token(fee_payment_token: TokenDescription) -> None:
"""Validate the token used to pay the fee of a fee-based token operation."""
match fee_payment_token.token_version:
case TokenVersion.FEE:
raise NCInvalidFeePaymentToken("fee-based tokens aren't allowed for paying fees")
case TokenVersion.DEPOSIT | TokenVersion.NATIVE:
pass
case _: # pragma: no cover
assert_never(fee_payment_token.token_version)


def _calculate_unit_fee_token_fee(settings: HathorSettings, fee_payment_token: TokenDescription) -> int:
"""Calculate the fee for handling a fee-based token"""
from hathor import HATHOR_TOKEN_UID
if fee_payment_token.token_id == HATHOR_TOKEN_UID:
return settings.FEE_PER_OUTPUT
return int(settings.FEE_PER_OUTPUT / settings.TOKEN_DEPOSIT_PERCENTAGE)
Loading
Loading