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
3 changes: 3 additions & 0 deletions hathor/dag_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ def from_manager(
blueprints_module=blueprints_module,
)

def get_main_wallet(self) -> BaseWallet:
return self._exporter.get_wallet('main')

def parse_tokens(self, tokens: Iterator[Token]) -> None:
"""Parse tokens and update the DAG accordingly."""
for parts in tokens:
Expand Down
66 changes: 40 additions & 26 deletions hathor/indexes/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,10 @@ def handle_contract_execution(self, tx: BaseTransaction) -> None:
"""
from hathor.conf.settings import HATHOR_TOKEN_UID
from hathor.nanocontracts.runner.types import (
NCSyscallRecord,
NCIndexUpdateRecord,
SyscallCreateContractRecord,
SyscallUpdateTokensRecord,
UpdateAuthoritiesRecord,
)
from hathor.nanocontracts.types import ContractId
from hathor.transaction.nc_execution_state import NCExecutionState
Expand All @@ -230,20 +231,20 @@ def handle_contract_execution(self, tx: BaseTransaction) -> None:
assert meta.nc_execution is NCExecutionState.SUCCESS
assert meta.nc_calls
first_call = meta.nc_calls[0]
nc_syscalls: list[NCSyscallRecord] = []
index_records: list[NCIndexUpdateRecord] = []

# Add to indexes.
for call in meta.nc_calls:
# Txs that call other contracts are added to those contracts' history. This includes calls to `initialize`.
if self.nc_history:
self.nc_history.add_single_key(call.contract_id, tx)

# Accumulate all syscalls.
nc_syscalls.extend(call.index_updates)
# Accumulate all index update records.
index_records.extend(call.index_updates)

created_contracts: set[ContractId] = set()
for syscall in nc_syscalls:
match syscall:
for record in index_records:
match record:
case SyscallCreateContractRecord(blueprint_id=blueprint_id, contract_id=contract_id):
assert contract_id not in created_contracts, f'contract {contract_id.hex()} created multiple times'
assert contract_id != first_call.contract_id, (
Expand All @@ -264,19 +265,27 @@ def handle_contract_execution(self, tx: BaseTransaction) -> None:
# and the respective destroyed/created HTR too.
if self.tokens:
try:
self.tokens.get_token_info(syscall.token_uid)
self.tokens.get_token_info(record.token_uid)
except KeyError:
# If the token doesn't exist in the index yet, it must be a token creation syscall.
from hathor.nanocontracts.runner.types import SyscallRecordType
assert syscall.type is SyscallRecordType.CREATE_TOKEN, syscall.type
assert syscall.token_name is not None and syscall.token_symbol is not None
self.tokens.create_token_info(syscall.token_uid, syscall.token_name, syscall.token_symbol)

self.tokens.add_to_total(syscall.token_uid, syscall.token_amount)
self.tokens.add_to_total(HATHOR_TOKEN_UID, syscall.htr_amount)
from hathor.nanocontracts.runner.types import IndexUpdateRecordType
assert record.type is IndexUpdateRecordType.CREATE_TOKEN, record.type
assert record.token_name is not None and record.token_symbol is not None
self.tokens.create_token_info_from_contract(
token_uid=record.token_uid,
name=record.token_name,
symbol=record.token_symbol,
)

self.tokens.add_to_total(record.token_uid, record.token_amount)
self.tokens.add_to_total(HATHOR_TOKEN_UID, record.htr_amount)

case UpdateAuthoritiesRecord():
if self.tokens:
self.tokens.update_authorities_from_contract(record)

case _:
assert_never(syscall)
assert_never(record)

def handle_contract_unexecution(self, tx: BaseTransaction) -> None:
"""
Expand All @@ -285,9 +294,10 @@ def handle_contract_unexecution(self, tx: BaseTransaction) -> None:
"""
from hathor.conf.settings import HATHOR_TOKEN_UID
from hathor.nanocontracts.runner.types import (
NCSyscallRecord,
NCIndexUpdateRecord,
SyscallCreateContractRecord,
SyscallUpdateTokensRecord,
UpdateAuthoritiesRecord,
)
from hathor.nanocontracts.types import NC_INITIALIZE_METHOD, ContractId

Expand All @@ -296,7 +306,7 @@ def handle_contract_unexecution(self, tx: BaseTransaction) -> None:
assert meta.nc_execution is NCExecutionState.SUCCESS
assert meta.nc_calls
first_call = meta.nc_calls[0]
nc_syscalls: list[NCSyscallRecord] = []
records: list[NCIndexUpdateRecord] = []

# Remove from indexes, but we must keep the first call's contract still in the indexes.
for call in meta.nc_calls:
Expand All @@ -305,11 +315,11 @@ def handle_contract_unexecution(self, tx: BaseTransaction) -> None:
self.nc_history.remove_single_key(call.contract_id, tx)

# Accumulate all syscalls.
nc_syscalls.extend(call.index_updates)
records.extend(call.index_updates)

created_contracts: set[ContractId] = set()
for syscall in nc_syscalls:
match syscall:
for record in records:
match record:
case SyscallCreateContractRecord(blueprint_id=blueprint_id, contract_id=contract_id):
assert contract_id not in created_contracts, f'contract {contract_id.hex()} created multiple times'
assert contract_id != first_call.contract_id, (
Expand All @@ -331,15 +341,19 @@ def handle_contract_unexecution(self, tx: BaseTransaction) -> None:
case SyscallUpdateTokensRecord():
# Undo the tokens update.
if self.tokens:
self.tokens.add_to_total(syscall.token_uid, -syscall.token_amount)
self.tokens.add_to_total(HATHOR_TOKEN_UID, -syscall.htr_amount)
self.tokens.add_to_total(record.token_uid, -record.token_amount)
self.tokens.add_to_total(HATHOR_TOKEN_UID, -record.htr_amount)

from hathor.nanocontracts.runner.types import SyscallRecordType
if syscall.type is SyscallRecordType.CREATE_TOKEN:
self.tokens.destroy_token(syscall.token_uid)
from hathor.nanocontracts.runner.types import IndexUpdateRecordType
if record.type is IndexUpdateRecordType.CREATE_TOKEN:
self.tokens.destroy_token(record.token_uid)

case UpdateAuthoritiesRecord():
if self.tokens:
self.tokens.update_authorities_from_contract(record, undo=True)

case _:
assert_never(syscall)
assert_never(record)

def add_tx(self, tx: BaseTransaction) -> bool:
""" Add a transaction to the indexes
Expand Down
84 changes: 79 additions & 5 deletions hathor/indexes/rocksdb_tokens_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
to_internal_token_uid,
)
from hathor.indexes.tokens_index import TokenIndexInfo, TokensIndex, TokenUtxoInfo
from hathor.nanocontracts.runner.types import UpdateAuthoritiesRecord, UpdateAuthoritiesRecordType
from hathor.nanocontracts.types import (
NCAcquireAuthorityAction,
NCDepositAction,
Expand Down Expand Up @@ -67,6 +68,8 @@ class _InfoDict(TypedDict):
name: str
symbol: str
total: int
n_contracts_can_mint: int
n_contracts_can_melt: int


class _TxIndex(NamedTuple):
Expand Down Expand Up @@ -173,19 +176,51 @@ def _to_value_info(self, info: _InfoDict) -> bytes:
return json_dumpb(info)

def _from_value_info(self, value: bytes) -> _InfoDict:
return cast(_InfoDict, json_loadb(value))

def create_token_info(self, token_uid: bytes, name: str, symbol: str, total: int = 0) -> None:
info = json_loadb(value)
if info.get('n_contracts_can_mint') is None:
assert info.get('n_contracts_can_melt') is None
info['n_contracts_can_mint'] = 0
info['n_contracts_can_melt'] = 0

return cast(_InfoDict, info)

def create_token_info(
self,
token_uid: bytes,
name: str,
symbol: str,
total: int = 0,
n_contracts_can_mint: int = 0,
n_contracts_can_melt: int = 0,
) -> None:
key = self._to_key_info(token_uid)
old_value = self._db.get((self._cf, key))
assert old_value is None
value = self._to_value_info({
'name': name,
'symbol': symbol,
'total': total,
'n_contracts_can_mint': n_contracts_can_mint,
'n_contracts_can_melt': n_contracts_can_melt,
})
self._db.put((self._cf, key), value)

def create_token_info_from_contract(
self,
token_uid: bytes,
name: str,
symbol: str,
total: int = 0,
) -> None:
self.create_token_info(
token_uid=token_uid,
name=name,
symbol=symbol,
total=total,
n_contracts_can_mint=1,
n_contracts_can_melt=1,
)

def destroy_token(self, token_uid: bytes) -> None:
import rocksdb

Expand Down Expand Up @@ -316,8 +351,9 @@ def add_tx(self, tx: BaseTransaction) -> None:
case NCWithdrawalAction():
self.add_to_total(action.token_uid, -action.amount)
case NCGrantAuthorityAction() | NCAcquireAuthorityAction():
# These actions don't affect the nc token balance,
# so no need for any special handling on the index.
# These actions don't affect the token balance but do affect the counters
# of contracts holding token authorities. They are handled directly by
# the IndexesManager via index update records created by the Runner.
pass
case _:
assert_never(action)
Expand Down Expand Up @@ -381,6 +417,36 @@ def get_token_info(self, token_uid: bytes) -> TokenIndexInfo:
info = self._from_value_info(value)
return RocksDBTokenIndexInfo(self, token_uid, info)

@override
def update_authorities_from_contract(self, record: UpdateAuthoritiesRecord, undo: bool = False) -> None:
assert record.token_uid != self._settings.HATHOR_TOKEN_UID
key_info = self._to_key_info(record.token_uid)
old_value_info = self._db.get((self._cf, key_info))
assert old_value_info is not None
dict_info = self._from_value_info(old_value_info)

increment: int
match record.sub_type:
case UpdateAuthoritiesRecordType.GRANT:
increment = 1
case UpdateAuthoritiesRecordType.REVOKE:
increment = -1
case _:
assert_never(record.sub_type)

if undo:
increment *= -1

if record.mint:
dict_info['n_contracts_can_mint'] += increment
if record.melt:
dict_info['n_contracts_can_melt'] += increment

assert dict_info['n_contracts_can_mint'] >= 0
assert dict_info['n_contracts_can_melt'] >= 0
new_value_info = self._to_value_info(dict_info)
self._db.put((self._cf, key_info), new_value_info)

def _iter_transactions(self, token_uid: bytes, from_tx: Optional[_TxIndex] = None,
*, reverse: bool = False) -> Iterator[bytes]:
""" Iterate over all transactions of a token, by default from oldest to newest.
Expand Down Expand Up @@ -471,3 +537,11 @@ def iter_mint_utxos(self) -> Iterator[TokenUtxoInfo]:

def iter_melt_utxos(self) -> Iterator[TokenUtxoInfo]:
return self._iter_authority_utxos(is_mint=False)

@override
def can_mint(self) -> bool:
return any(self.iter_mint_utxos()) or self._info['n_contracts_can_mint'] > 0

@override
def can_melt(self) -> bool:
return any(self.iter_melt_utxos()) or self._info['n_contracts_can_melt'] > 0
45 changes: 43 additions & 2 deletions hathor/indexes/tokens_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Iterator, NamedTuple, Optional
from typing import TYPE_CHECKING, Iterator, NamedTuple, Optional

from hathor.indexes.base_index import BaseIndex
from hathor.indexes.scope import Scope
from hathor.transaction import BaseTransaction

if TYPE_CHECKING:
from hathor.nanocontracts.runner.types import UpdateAuthoritiesRecord

SCOPE = Scope(
include_blocks=False,
include_txs=True,
Expand Down Expand Up @@ -64,6 +69,16 @@ def iter_melt_utxos(self) -> Iterator[TokenUtxoInfo]:
"""Iterate over melt-authority UTXOs"""
raise NotImplementedError

@abstractmethod
def can_mint(self) -> bool:
"""Return whether this token can be minted, that is, whether any UTXO or contract holds a mint authority."""
raise NotImplementedError

@abstractmethod
def can_melt(self) -> bool:
"""Return whether this token can be melted, that is, whether any UTXO or contract holds a melt authority."""
raise NotImplementedError


class TokensIndex(BaseIndex):
""" Index of tokens by token uid
Expand Down Expand Up @@ -115,15 +130,41 @@ def get_token_info(self, token_uid: bytes) -> TokenIndexInfo:
raise NotImplementedError

@abstractmethod
def create_token_info(self, token_uid: bytes, name: str, symbol: str, total: int = 0) -> None:
def create_token_info(
self,
token_uid: bytes,
name: str,
symbol: str,
total: int = 0,
n_contracts_can_mint: int = 0,
n_contracts_can_melt: int = 0,
) -> None:
"""Create a token info for a new token."""
raise NotImplementedError

@abstractmethod
def create_token_info_from_contract(
self,
token_uid: bytes,
name: str,
symbol: str,
total: int = 0,
) -> None:
"""Create a token info for a new token created in a contract."""
raise NotImplementedError

@abstractmethod
def destroy_token(self, token_uid: bytes) -> None:
"""Destroy a token."""
raise NotImplementedError

@abstractmethod
def update_authorities_from_contract(self, record: UpdateAuthoritiesRecord, undo: bool = False) -> None:
"""
Handle an UpdateAuthoritiesRecord by incrementing/decrementing the counters of contracts holding authorities.
"""
raise NotImplementedError

@abstractmethod
def get_transactions_count(self, token_uid: bytes) -> int:
""" Get quantity of transactions from requested token
Expand Down
Loading