Skip to content

Commit a79939a

Browse files
committed
feat: fee header support
1 parent c5a014c commit a79939a

File tree

9 files changed

+167
-28
lines changed

9 files changed

+167
-28
lines changed

hathorlib/base_transaction.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ def is_nano_contract(self) -> bool:
147147
"""Return True if this transaction is a nano contract or not."""
148148
return False
149149

150+
def has_fees(self) -> bool:
151+
"""Return True if this transaction has fees or not."""
152+
return False
153+
150154
def _get_formatted_fields_dict(self, short: bool = True) -> Dict[str, str]:
151155
""" Used internally on __repr__ and __str__, returns a dict of `field_name: formatted_value`.
152156
"""

hathorlib/headers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from hathorlib.headers.base import VertexBaseHeader
1616
from hathorlib.headers.deprecated_nano_header import DeprecatedNanoHeader
17+
from hathorlib.headers.fee_header import FeeEntry, FeeHeader, FeeHeaderEntry
1718
from hathorlib.headers.nano_header import NC_INITIALIZE_METHOD, NanoHeader
1819
from hathorlib.headers.types import VertexHeaderId
1920

@@ -22,5 +23,8 @@
2223
'VertexHeaderId',
2324
'NanoHeader',
2425
'DeprecatedNanoHeader',
26+
'FeeHeader',
27+
'FeeHeaderEntry',
28+
'FeeEntry',
2529
'NC_INITIALIZE_METHOD',
2630
]

hathorlib/headers/fee_header.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copyright 2023 Hathor Labs
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from dataclasses import dataclass
18+
from typing import TYPE_CHECKING
19+
20+
from hathorlib.headers.base import VertexBaseHeader
21+
from hathorlib.headers.types import VertexHeaderId
22+
from hathorlib.utils import int_to_bytes, unpack
23+
24+
if TYPE_CHECKING:
25+
from hathorlib.base_transaction import BaseTransaction
26+
from hathorlib.transaction import Transaction
27+
28+
29+
@dataclass(frozen=True)
30+
class FeeHeaderEntry:
31+
token_index: int
32+
amount: int
33+
34+
35+
@dataclass(frozen=True)
36+
class FeeEntry:
37+
token_uid: bytes
38+
amount: int
39+
40+
41+
@dataclass(frozen=True)
42+
class FeeHeader(VertexBaseHeader):
43+
tx: Transaction
44+
fees: list[FeeHeaderEntry]
45+
46+
@classmethod
47+
def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[FeeHeader, bytes]:
48+
from hathorlib.base_transaction import bytes_to_output_value
49+
50+
header_id, buf = buf[:1], buf[1:]
51+
assert header_id == VertexHeaderId.FEE_HEADER.value
52+
53+
fees: list[FeeHeaderEntry] = []
54+
(fees_len,), buf = unpack('!B', buf)
55+
56+
for _ in range(fees_len):
57+
(token_index,), buf = unpack('!B', buf)
58+
amount, buf = bytes_to_output_value(buf)
59+
fees.append(FeeHeaderEntry(
60+
token_index=token_index,
61+
amount=amount,
62+
))
63+
from hathorlib.transaction import Transaction
64+
assert isinstance(tx, Transaction)
65+
return cls(
66+
tx=tx,
67+
fees=fees,
68+
), bytes(buf)
69+
70+
def serialize(self) -> bytes:
71+
from hathorlib.base_transaction import output_value_to_bytes
72+
73+
ret = [
74+
VertexHeaderId.FEE_HEADER.value,
75+
int_to_bytes(len(self.fees), 1)
76+
]
77+
78+
for fee in self.fees:
79+
ret.append(int_to_bytes(fee.token_index, 1))
80+
ret.append(output_value_to_bytes(fee.amount))
81+
82+
return b''.join(ret)
83+
84+
def get_sighash_bytes(self) -> bytes:
85+
return self.serialize()
86+
87+
def get_fees(self) -> list[FeeEntry]:
88+
return [
89+
FeeEntry(
90+
token_uid=self.tx.get_token_uid(fee.token_index),
91+
amount=fee.amount
92+
)
93+
for fee in self.fees
94+
]

hathorlib/headers/nano_header.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020

2121
from hathorlib.headers.base import VertexBaseHeader
2222
from hathorlib.headers.types import VertexHeaderId
23-
from hathorlib.nanocontracts import DeprecatedNanoContract
24-
from hathorlib.nanocontracts.types import NCActionType
2523
from hathorlib.utils import decode_unsigned, encode_unsigned, int_to_bytes, unpack, unpack_len
2624

2725
if TYPE_CHECKING:
2826
from hathorlib.base_transaction import BaseTransaction
27+
from hathorlib.nanocontracts.types import NCActionType
2928

3029
NC_INITIALIZE_METHOD = 'initialize'
3130
ADDRESS_LEN_BYTES = 25
@@ -35,7 +34,7 @@
3534

3635
@dataclass(frozen=True)
3736
class NanoHeaderAction:
38-
type: NCActionType
37+
type: 'NCActionType'
3938
token_index: int
4039
amount: int
4140

@@ -66,6 +65,8 @@ class NanoHeader(VertexBaseHeader):
6665
@classmethod
6766
def _deserialize_action(cls, buf: bytes) -> tuple[NanoHeaderAction, bytes]:
6867
from hathorlib.base_transaction import bytes_to_output_value
68+
from hathorlib.nanocontracts.types import NCActionType
69+
6970
type_bytes, buf = buf[:1], buf[1:]
7071
action_type = NCActionType.from_bytes(type_bytes)
7172
(token_index,), buf = unpack('!B', buf)
@@ -78,6 +79,8 @@ def _deserialize_action(cls, buf: bytes) -> tuple[NanoHeaderAction, bytes]:
7879

7980
@classmethod
8081
def deserialize(cls, tx: BaseTransaction, buf: bytes) -> tuple[NanoHeader, bytes]:
82+
from hathorlib.nanocontracts import DeprecatedNanoContract
83+
8184
header_id, buf = buf[:1], buf[1:]
8285
assert header_id == VertexHeaderId.NANO_HEADER.value
8386

@@ -124,6 +127,8 @@ def _serialize_action(action: NanoHeaderAction) -> bytes:
124127

125128
def _serialize_without_header_id(self, *, skip_signature: bool) -> deque[bytes]:
126129
"""Serialize the header with the option to skip the signature."""
130+
from hathorlib.nanocontracts import DeprecatedNanoContract
131+
127132
encoded_method = self.nc_method.encode('ascii')
128133

129134
ret: deque[bytes] = deque()

hathorlib/headers/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
@unique
1919
class VertexHeaderId(Enum):
2020
NANO_HEADER = b'\x10'
21+
FEE_HEADER = b'\x11'

hathorlib/nanocontracts/nanocontract.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ def get_funds_fields_from_struct(self, buf: bytes) -> bytes:
4949
return buf
5050

5151
def get_funds_struct(self) -> bytes:
52+
from hathorlib.headers import DeprecatedNanoHeader
5253
struct_bytes = super().get_funds_struct()
53-
nano_header_bytes = self.get_nano_header().serialize()
54+
nano_header_bytes = self._get_header(DeprecatedNanoHeader).serialize()
5455
struct_bytes += nano_header_bytes[1:]
5556
return struct_bytes
5657

hathorlib/transaction.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
import struct
1111
from collections import namedtuple
1212
from struct import pack
13-
from typing import TYPE_CHECKING, List
13+
from typing import TYPE_CHECKING, List, TypeVar
1414

1515
from hathorlib.base_transaction import TX_HASH_SIZE, BaseTransaction, TxInput, TxOutput
1616
from hathorlib.conf import HathorSettings
1717
from hathorlib.exceptions import InvalidOutputValue, InvalidToken
18+
from hathorlib.headers import VertexBaseHeader
1819
from hathorlib.utils import unpack, unpack_len
1920

2021
if TYPE_CHECKING:
21-
from hathorlib.headers import DeprecatedNanoHeader, NanoHeader
22+
from hathorlib.headers import FeeHeader, NanoHeader
23+
24+
T = TypeVar('T', bound=VertexBaseHeader)
2225

2326
settings = HathorSettings()
2427

@@ -62,13 +65,31 @@ def is_nano_contract(self) -> bool:
6265
else:
6366
return True
6467

65-
def get_nano_header(self) -> NanoHeader | DeprecatedNanoHeader:
68+
def has_fees(self) -> bool:
69+
"""Returns true if this transaction has a fee header"""
70+
try:
71+
self.get_fee_header()
72+
except ValueError:
73+
return False
74+
else:
75+
return True
76+
77+
def get_nano_header(self) -> NanoHeader:
78+
from hathorlib.headers import NanoHeader
6679
"""Return the NanoHeader or raise ValueError."""
67-
from hathorlib.headers import DeprecatedNanoHeader, NanoHeader
80+
return self._get_header(NanoHeader)
81+
82+
def get_fee_header(self) -> FeeHeader:
83+
from hathorlib.headers import FeeHeader
84+
"""Return the FeeHeader or raise ValueError."""
85+
return self._get_header(FeeHeader)
86+
87+
def _get_header(self, header_type: type[T]) -> T:
88+
"""Return the header of the given type or raise ValueError."""
6889
for header in self.headers:
69-
if isinstance(header, (NanoHeader, DeprecatedNanoHeader)):
90+
if isinstance(header, header_type):
7091
return header
71-
raise ValueError('nano header not found')
92+
raise ValueError(f'{header_type.__name__.lower()} not found')
7293

7394
@classmethod
7495
def create_from_struct(cls, struct_bytes: bytes) -> 'Transaction':

hathorlib/vertex_parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ class VertexParser:
2626
@staticmethod
2727
def get_supported_headers() -> dict[VertexHeaderId, type[VertexBaseHeader]]:
2828
"""Return a dict of supported headers."""
29-
from hathorlib.headers import NanoHeader, VertexHeaderId
29+
from hathorlib.headers import FeeHeader, NanoHeader, VertexHeaderId
3030
return {
3131
VertexHeaderId.NANO_HEADER: NanoHeader,
32+
VertexHeaderId.FEE_HEADER: FeeHeader,
3233
}
3334

3435
@staticmethod

tests/test_basic.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,19 @@ def test_token_creation_basics(self):
107107
tx.update_hash()
108108
self.assertFalse(tx.verify_pow())
109109

110-
def test_token_creation_with_fee_version(self):
111-
"""Test TokenCreationTransaction with token_version=2 (FEE)"""
112-
from hathorlib.token_creation_tx import TokenVersion
110+
def test_token_creation_with_fee_header(self):
111+
"""Test TokenCreationTransaction with fee header"""
112+
from hathorlib.token_creation_tx import TokenCreationTransaction, TokenVersion
113+
114+
data = bytes.fromhex(
115+
'0002010400000672c17c8fcf7277eece0b8cbe3f0efbdf6205e5e8554ccff5ca85ec8e49000069463044022070c5bfcd3b2f177'
116+
'c842de1937c8a089bec64ea2d27754056fb7d7882e731aad7022073b6811313a52f74a88cedbbb2d951ddd5c6d2bba97332eea74'
117+
'2e020d7717f04210299138e77a8039c31a112941480231cccefc9e627fef5ff4a391e7a2689b319d40000000900001976a914ba6'
118+
'a16b0ab2c2bf132e1cfbdc01ef86a8c749a7188ac0000006401001976a914ba6a16b0ab2c2bf132e1cfbdc01ef86a8c749a7188a'
119+
'c0000000181001976a914ba6a16b0ab2c2bf132e1cfbdc01ef86a8c749a7188ac0000000281001976a914ba6a16b0ab2c2bf132e'
120+
'1cfbdc01ef86a8c749a7188ac0209546f6b656e4e616d6503544b4e4031b96d6968b53e690472ad000000000011010000000001'
121+
)
113122

114-
# Using the same structure as test_token_creation_basics but with token_version=2
115-
data = bytes.fromhex('00020104000005551d7740fd7d3c0acc50b5677fdd844f1225985aa431e1712af2a2fd'
116-
'8900006a473045022100a445edb5cd6c79a0a7b5ed837582fd65b8d511ee60b64fd076'
117-
'e07bd8f63f75a202202dca24320bffc4c3ca2a07cdfff38f7c839bde70ed49ef634ac6'
118-
'588972836cab2103bfa995d676e3c0ed7b863c74cfef9683fab3163b42b6f21442326a'
119-
'023fc57fba0000264800001976a9146876f9578221fdb678d4e8376503098a9228b132'
120-
'88ac00004e2001001976a914031761ef85a24603203c97e75af355b83209f08f88ac00'
121-
'00000181001976a9149f091256cb98649c7c35df0aad44d7805710691e88ac00000002'
122-
'81001976a914b1d7a5ee505ad4d3b93ea1a5162ba83d5049ec4e88ac0209546f546865'
123-
'4d6f6f6e04f09f9a804034a52aec6cece75e0fc0e30200001a72272f48339fcc5d5ec5'
124-
'deaf197855964b0eb912e8c6eefe00928b6cf600001055641c20b71871ed2c5c7d4096'
125-
'a34f40888d79c25bce74421646e732dc01ff730d')
126123
tx = TokenCreationTransaction.create_from_struct(data)
127124

128125
# Verify the token version is FEE (2)
@@ -134,12 +131,23 @@ def test_token_creation_with_fee_version(self):
134131
# Verify basic transaction properties
135132
self.assertTrue(tx.is_transaction)
136133
self.assertFalse(tx.is_block)
134+
self.assertTrue(tx.has_fees())
135+
136+
# Verify the fee header contains the expected fee entry
137+
fee_header = tx.get_fee_header()
138+
self.assertEqual(len(fee_header.fees), 1)
139+
self.assertEqual(fee_header.fees[0].token_index, 0)
140+
self.assertEqual(fee_header.fees[0].amount, 1)
141+
142+
self.assertEqual(len(fee_header.get_fees()), 1)
143+
self.assertEqual(fee_header.get_fees()[0].amount, 1)
144+
self.assertEqual(fee_header.get_fees()[0].token_uid, settings.HATHOR_TOKEN_UID)
137145

138146
# Verify the string representation includes token_version=2
139147
str_repr = str(tx)
140148
self.assertIn('token_version=2', str_repr)
141-
self.assertIn('token_name=ToTheMoon', str_repr)
142-
self.assertIn('token_symbol=🚀', str_repr)
149+
self.assertIn('token_name=TokenName', str_repr)
150+
self.assertIn('token_symbol=TKN', str_repr)
143151

144152
def test_script_basics(self):
145153
create_output_script(decode_address('HVZjvL1FJ23kH3buGNuttVRsRKq66WHUVZ'))

0 commit comments

Comments
 (0)