From 33702d7016552d27fd242568170aae378a7b4531 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 Jul 2026 19:57:58 +0800 Subject: [PATCH 1/3] refactor: migrate all encode/decode to stream-based io.Writer/io.BufferedReader API Replace the functional bytes-based encoding approach with a stream-based API using io.Writer (write) and io.BufferedReader (read). This eliminates O(n^2) byte concatenation and simplifies call sites. Changes: - All encode() methods now take io.Writer and return None - All decode() classmethods now take io.BufferedReader and return the value directly (no more tuple unwrapping) - Updated abstract base classes (Signature, PublicKey) - Updated crypto types (Hash, Address, BLS/Ed25519/Secp256k1 sig/pub) - Updated simple types (Height, Round, Amount) - Updated transaction payloads - Updated Transaction, Block, Header, Certificate - Updated all tests to match the new API - Fixed lint issues with ruff --- pactus/block/block.py | 15 ++-- pactus/block/certificate.py | 47 ++++++----- pactus/block/header.py | 38 ++++----- pactus/crypto/address.py | 26 +++--- pactus/crypto/bls/public_key.py | 14 ++-- pactus/crypto/bls/signature.py | 15 ++-- pactus/crypto/ed25519/public_key.py | 14 ++-- pactus/crypto/ed25519/signature.py | 15 ++-- pactus/crypto/hash.py | 20 +++-- pactus/crypto/public_key.py | 6 +- pactus/crypto/secp256k1/public_key.py | 14 ++-- pactus/crypto/secp256k1/signature.py | 15 ++-- pactus/crypto/signature.py | 8 +- pactus/encoding/encoding.py | 104 ++++++++++-------------- pactus/transaction/payload/bond.py | 33 ++++---- pactus/transaction/payload/sortition.py | 16 ++-- pactus/transaction/payload/transfer.py | 20 +++-- pactus/transaction/payload/unbond.py | 12 +-- pactus/transaction/payload/withdraw.py | 20 +++-- pactus/transaction/transaction.py | 78 +++++++++--------- pactus/types/amount.py | 16 ++-- pactus/types/height.py | 17 ++-- pactus/types/round.py | 17 ++-- tests/test_amount.py | 39 +++++++++ tests/test_encoding.py | 51 ++++++------ tests/test_transaction.py | 5 +- 26 files changed, 373 insertions(+), 302 deletions(-) diff --git a/pactus/block/block.py b/pactus/block/block.py index a0c0006..956a7c8 100644 --- a/pactus/block/block.py +++ b/pactus/block/block.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import io import struct from pactus.crypto.hash import Hash @@ -25,20 +26,22 @@ def __init__( @classmethod def decode(cls, data: bytes) -> Block: """Decode a Block from raw bytes.""" - header, data = Header.decode(data) + buf = io.BytesIO(data) + + header = Header.decode(buf) # Genesis block has no certificate is_genesis = header.prev_block_hash.is_undef() prev_cert = None if not is_genesis: - prev_cert, data = Certificate.decode(data) + prev_cert = Certificate.decode(buf) - num_txs, data = encoding.read_var_int(data) + num_txs = encoding.read_var_int(buf) transactions = [] for _ in range(num_txs): - tx, data = Transaction.decode(data) + tx = Transaction.decode(buf) transactions.append(tx) return cls(header, prev_cert, transactions) @@ -54,7 +57,9 @@ def id(self) -> Hash: return Hash(hashlib.blake2b(buf, digest_size=32).digest()) def _header_bytes(self) -> bytes: - return self.header.encode(b"") + buf = io.BytesIO() + self.header.encode(buf) + return buf.getvalue() def _txs_root(self) -> bytes: return Block._merkle_root([tx.id().data for tx in self.transactions]) diff --git a/pactus/block/certificate.py b/pactus/block/certificate.py index f5bab3e..aa59d7a 100644 --- a/pactus/block/certificate.py +++ b/pactus/block/certificate.py @@ -1,4 +1,5 @@ import hashlib +import io from pactus.crypto.bls.signature import Signature from pactus.encoding import encoding @@ -22,42 +23,40 @@ def __init__( self.signature = signature @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Certificate from bytes. - Returns (Certificate, remaining_buf). - """ - height, buf = Height.decode(buf) - round_, buf = Round.decode(buf) - - num_committers, buf = encoding.read_var_int(buf) + def decode(cls, buf: io.BufferedReader) -> "Certificate": + """Decode a Certificate from the stream.""" + height = Height.decode(buf) + round_ = Round.decode(buf) + + num_committers = encoding.read_var_int(buf) committers = [] for _ in range(num_committers): - n, buf = encoding.read_var_int(buf) + n = encoding.read_var_int(buf) committers.append(n) - num_absentees, buf = encoding.read_var_int(buf) + num_absentees = encoding.read_var_int(buf) absentees = [] for _ in range(num_absentees): - n, buf = encoding.read_var_int(buf) + n = encoding.read_var_int(buf) absentees.append(n) - signature, buf = Signature.decode(buf) + signature = Signature.decode(buf) - cert = cls(height, round_, committers, absentees, signature) - return cert, buf + return cls(height, round_, committers, absentees, signature) - def encode(self, buf: bytes) -> bytes: - buf = self.height.encode(buf) - buf = self.round.encode(buf) - buf = encoding.append_var_int(buf, len(self.committers)) + def encode(self, buf: io.Writer) -> None: + self.height.encode(buf) + self.round.encode(buf) + encoding.append_var_int(buf, len(self.committers)) for n in self.committers: - buf = encoding.append_var_int(buf, n) - buf = encoding.append_var_int(buf, len(self.absentees)) + encoding.append_var_int(buf, n) + encoding.append_var_int(buf, len(self.absentees)) for n in self.absentees: - buf = encoding.append_var_int(buf, n) - return self.signature.encode(buf) + encoding.append_var_int(buf, n) + self.signature.encode(buf) def hash(self) -> bytes: """Return the certificate hash (blake2b-256 of encoded bytes).""" - return hashlib.blake2b(self.encode(b""), digest_size=32).digest() + buf = io.BytesIO() + self.encode(buf) + return hashlib.blake2b(buf.getvalue(), digest_size=32).digest() diff --git a/pactus/block/header.py b/pactus/block/header.py index 92e0cf9..2f64afe 100644 --- a/pactus/block/header.py +++ b/pactus/block/header.py @@ -1,3 +1,5 @@ +import io + from pactus.crypto.address import Address from pactus.crypto.hash import Hash from pactus.encoding import encoding @@ -21,19 +23,16 @@ def __init__( self.proposer_address = proposer_address @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Header from bytes. - Returns (Header, remaining_buf). - """ - version, buf = encoding.read_uint8(buf) - unix_time, buf = encoding.read_uint32(buf) - prev_block_hash, buf = Hash.decode(buf) - state_root, buf = Hash.decode(buf) - sortition_seed, buf = encoding.read_fixed_bytes(buf, 48) - proposer_address, buf = Address.decode(buf) + def decode(cls, buf: io.BufferedReader) -> "Header": + """Decode a Header from the stream.""" + version = encoding.read_uint8(buf) + unix_time = encoding.read_uint32(buf) + prev_block_hash = Hash.decode(buf) + state_root = Hash.decode(buf) + sortition_seed = encoding.read_fixed_bytes(buf, 48) + proposer_address = Address.decode(buf) - header = cls( + return cls( version, unix_time, prev_block_hash, @@ -41,12 +40,11 @@ def decode(cls, buf: bytes) -> tuple: sortition_seed, proposer_address, ) - return header, buf - def encode(self, buf: bytes) -> bytes: - buf = encoding.append_uint8(buf, self.version) - buf = encoding.append_uint32(buf, self.unix_time) - buf = self.prev_block_hash.encode(buf) - buf = self.state_root.encode(buf) - buf = encoding.append_fixed_bytes(buf, self.sortition_seed) - return self.proposer_address.encode(buf) + def encode(self, buf: io.Writer) -> None: + encoding.append_uint8(buf, self.version) + encoding.append_uint32(buf, self.unix_time) + self.prev_block_hash.encode(buf) + self.state_root.encode(buf) + encoding.append_fixed_bytes(buf, self.sortition_seed) + self.proposer_address.encode(buf) diff --git a/pactus/crypto/address.py b/pactus/crypto/address.py index 4738c8e..51a7537 100644 --- a/pactus/crypto/address.py +++ b/pactus/crypto/address.py @@ -1,11 +1,15 @@ from __future__ import annotations from enum import Enum +from typing import TYPE_CHECKING from pactus.crypto.hrp import HRP from pactus.encoding import encoding from pactus.utils import utils +if TYPE_CHECKING: + import io + # Address format: hrp + `1` + type + data + checksum ADDRESS_SIZE = 21 @@ -86,20 +90,18 @@ def is_account_address(self) -> bool: def is_validator_address(self) -> bool: return self.address_type() == AddressType.VALIDATOR - def encode(self, buf: bytes) -> bytes: + def encode(self, buf: io.Writer) -> None: if self.is_treasury_address(): - return encoding.append_uint8(buf, AddressType.TREASURY.value) - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + encoding.append_uint8(buf, AddressType.TREASURY.value) + else: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode an Address from bytes. - Returns (Address, remaining_buf). - """ - addr_type, buf = encoding.read_uint8(buf) + def decode(cls, buf: io.BufferedReader) -> Address: + """Decode an Address from the stream.""" + addr_type = encoding.read_uint8(buf) if addr_type == AddressType.TREASURY.value: - return cls(AddressType.TREASURY, bytes(20)), buf + return cls(AddressType.TREASURY, bytes(20)) - data, buf = encoding.read_fixed_bytes(buf, 20) - return cls(AddressType(addr_type), data), buf + data = encoding.read_fixed_bytes(buf, 20) + return cls(AddressType(addr_type), data) diff --git a/pactus/crypto/bls/public_key.py b/pactus/crypto/bls/public_key.py index 16e03b7..22d055f 100644 --- a/pactus/crypto/bls/public_key.py +++ b/pactus/crypto/bls/public_key.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +from typing import TYPE_CHECKING from ripemd.ripemd160 import ripemd160 @@ -13,6 +14,9 @@ from .bls12_381.serdesZ import deserialize, serialize from .signature import DST, SIGNATURE_TYPE_BLS, Signature +if TYPE_CHECKING: + import io + PUBLIC_KEY_SIZE = 96 @@ -61,13 +65,13 @@ def string(self) -> str: self.raw_bytes(), ) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) - return cls.from_bytes(data), buf + def decode(cls, buf: io.BufferedReader) -> PublicKey: + data = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) + return cls.from_bytes(data) def account_address(self) -> Address: return self._make_address(AddressType.BLS_ACCOUNT) diff --git a/pactus/crypto/bls/signature.py b/pactus/crypto/bls/signature.py index 3441ef9..f2ad220 100644 --- a/pactus/crypto/bls/signature.py +++ b/pactus/crypto/bls/signature.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding from .bls12_381.bls_sig_g1 import aggregate_sigs from .bls12_381.serdesZ import deserialize, serialize +if TYPE_CHECKING: + import io + SIGNATURE_SIZE = 48 SIGNATURE_TYPE_BLS = 1 DST = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" @@ -39,11 +44,11 @@ def raw_bytes(self) -> bytes: def string(self) -> str: return self.raw_bytes().hex() - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) + def decode(cls, buf: io.BufferedReader) -> Signature: + data = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) point_g1 = deserialize(data, is_ell2=False) - return cls(point_g1), buf + return cls(point_g1) diff --git a/pactus/crypto/ed25519/public_key.py b/pactus/crypto/ed25519/public_key.py index 7af7055..f1238da 100644 --- a/pactus/crypto/ed25519/public_key.py +++ b/pactus/crypto/ed25519/public_key.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +from typing import TYPE_CHECKING from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ed25519 @@ -13,6 +14,9 @@ from .signature import SIGNATURE_TYPE_ED25519, Signature +if TYPE_CHECKING: + import io + PUBLIC_KEY_SIZE = 32 @@ -50,13 +54,13 @@ def string(self) -> str: self.raw_bytes(), ) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) - return cls(ed25519.Ed25519PublicKey.from_public_bytes(data)), buf + def decode(cls, buf: io.BufferedReader) -> PublicKey: + data = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) + return cls(ed25519.Ed25519PublicKey.from_public_bytes(data)) def account_address(self) -> Address: blake2b = hashlib.blake2b(digest_size=32) diff --git a/pactus/crypto/ed25519/signature.py b/pactus/crypto/ed25519/signature.py index 68f7e3c..e828e0e 100644 --- a/pactus/crypto/ed25519/signature.py +++ b/pactus/crypto/ed25519/signature.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding +if TYPE_CHECKING: + import io + SIGNATURE_SIZE = 64 SIGNATURE_TYPE_ED25519 = 3 @@ -26,10 +31,10 @@ def raw_bytes(self) -> bytes: def string(self) -> str: return self.sig.hex() - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.sig) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.sig) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) - return cls(data), buf + def decode(cls, buf: io.BufferedReader) -> Signature: + data = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) + return cls(data) diff --git a/pactus/crypto/hash.py b/pactus/crypto/hash.py index f7e5451..5369c3a 100644 --- a/pactus/crypto/hash.py +++ b/pactus/crypto/hash.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding +if TYPE_CHECKING: + import io + HASH_SIZE = 32 @@ -28,14 +33,11 @@ def __str__(self) -> str: def is_undef(self) -> bool: return self.data == bytes(HASH_SIZE) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.data) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.data) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Hash from bytes. - Returns (Hash, remaining_buf). - """ - data, buf = encoding.read_fixed_bytes(buf, HASH_SIZE) - return cls(data), buf + def decode(cls, buf: io.BufferedReader) -> Hash: + """Decode a Hash from the stream.""" + data = encoding.read_fixed_bytes(buf, HASH_SIZE) + return cls(data) diff --git a/pactus/crypto/public_key.py b/pactus/crypto/public_key.py index f50cf33..40a7851 100644 --- a/pactus/crypto/public_key.py +++ b/pactus/crypto/public_key.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + import io + from .signature import Signature @@ -22,12 +24,12 @@ def string(self) -> str: pass @abstractmethod - def encode(self, buf: bytes) -> bytes: + def encode(self, buf: io.Writer) -> None: pass @classmethod @abstractmethod - def decode(cls, buf: bytes) -> tuple: + def decode(cls, buf: io.BufferedReader) -> PublicKey: pass @abstractmethod diff --git a/pactus/crypto/secp256k1/public_key.py b/pactus/crypto/secp256k1/public_key.py index 95cfafc..3ea36d3 100644 --- a/pactus/crypto/secp256k1/public_key.py +++ b/pactus/crypto/secp256k1/public_key.py @@ -2,6 +2,7 @@ import hashlib from functools import partial +from typing import TYPE_CHECKING import secp256k1 from ripemd.ripemd160 import ripemd160 @@ -13,6 +14,9 @@ from .signature import SIGNATURE_TYPE_SECP256K1, Signature +if TYPE_CHECKING: + import io + PUBLIC_KEY_SIZE = 33 # Compressed public key @@ -50,13 +54,13 @@ def string(self) -> str: self.raw_bytes(), ) - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.raw_bytes()) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.raw_bytes()) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) - return cls(secp256k1.PublicKey(data, raw=True)), buf + def decode(cls, buf: io.BufferedReader) -> PublicKey: + data = encoding.read_fixed_bytes(buf, PUBLIC_KEY_SIZE) + return cls(secp256k1.PublicKey(data, raw=True)) def account_address(self) -> Address: blake2b = hashlib.blake2b(digest_size=32) diff --git a/pactus/crypto/secp256k1/signature.py b/pactus/crypto/secp256k1/signature.py index 6b18c5e..dc5ec95 100644 --- a/pactus/crypto/secp256k1/signature.py +++ b/pactus/crypto/secp256k1/signature.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.encoding import encoding +if TYPE_CHECKING: + import io + SIGNATURE_SIZE = 64 SIGNATURE_TYPE_SECP256K1 = 4 @@ -26,10 +31,10 @@ def raw_bytes(self) -> bytes: def string(self) -> str: return self.sig.hex() - def encode(self, buf: bytes) -> bytes: - return encoding.append_fixed_bytes(buf, self.sig) + def encode(self, buf: io.Writer) -> None: + encoding.append_fixed_bytes(buf, self.sig) @classmethod - def decode(cls, buf: bytes) -> tuple: - data, buf = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) - return cls(data), buf + def decode(cls, buf: io.BufferedReader) -> Signature: + data = encoding.read_fixed_bytes(buf, SIGNATURE_SIZE) + return cls(data) diff --git a/pactus/crypto/signature.py b/pactus/crypto/signature.py index ac50935..f2d0ec7 100644 --- a/pactus/crypto/signature.py +++ b/pactus/crypto/signature.py @@ -1,6 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io class Signature(ABC): @@ -18,10 +22,10 @@ def string(self) -> str: pass @abstractmethod - def encode(self, buf: bytes) -> bytes: + def encode(self, buf: io.Writer) -> None: pass @classmethod @abstractmethod - def decode(cls, buf: bytes) -> tuple: + def decode(cls, buf: io.BufferedReader) -> Signature: pass diff --git a/pactus/encoding/encoding.py b/pactus/encoding/encoding.py index 91c0a92..f865538 100644 --- a/pactus/encoding/encoding.py +++ b/pactus/encoding/encoding.py @@ -1,93 +1,77 @@ + from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io -def append_uint8(buf: bytes, val: int) -> bytes: - buf += val.to_bytes(1, "little") - return buf +def append_uint8(buf: io.Writer, val: int) -> None: + buf.write(val.to_bytes(1, "little")) -def append_uint16(buf: bytes, val: int) -> bytes: - buf += val.to_bytes(2, "little") - return buf +def append_uint16(buf: io.Writer, val: int) -> None: + buf.write(val.to_bytes(2, "little")) -def append_uint32(buf: bytes, val: int) -> bytes: - buf += val.to_bytes(4, "little") - return buf +def append_uint32(buf: io.Writer, val: int) -> None: + buf.write(val.to_bytes(4, "little")) -def append_str(buf: bytes, val: str) -> bytes: - buf = append_var_int(buf, len(val)) - return append_fixed_bytes(buf, bytes(val, "utf-8")) +def append_str(buf: io.Writer, val: str) -> None: + append_var_int(buf, len(val)) + append_fixed_bytes(buf, bytes(val, "utf-8")) -def append_var_int(buf: bytes, val: int) -> bytes: + +def append_var_int(buf: io.Writer, val: int) -> None: while val >= 0x80: n = (val & 0x7F) | 0x80 - buf += bytes([n]) + buf.write(bytes([n])) val >>= 7 - buf += bytes([val]) - return buf - + buf.write(bytes([val])) -def append_fixed_bytes(buf: bytes, data: bytes) -> bytes: - buf += data - return buf - - -def read_uint8(buf: bytes) -> tuple[int, bytes]: - val = int.from_bytes(buf[0:1], "little", signed=False) - return val, buf[1:] +def append_fixed_bytes(buf: io.Writer, data: bytes) -> None: + buf.write(data) +def _read(buf: io.BufferedReader, size: int) -> bytes: + data = buf.read(size) + if len(data) != size: + msg = "unexpected end of data while reading" + raise ValueError(msg) -def read_uint16(buf: bytes) -> tuple[int, bytes]: - val = int.from_bytes(buf[0:2], "little", signed=False) - return val, buf[2:] + return data +def read_uint8(buf: io.BufferedReader) -> int: + return int.from_bytes(_read(buf, 1), "little", signed=False) -def read_uint32(buf: bytes) -> tuple[int, bytes]: - val = int.from_bytes(buf[0:4], "little", signed=False) - return val, buf[4:] +def read_uint16(buf: io.BufferedReader) -> int: + return int.from_bytes(_read(buf, 2), "little", signed=False) +def read_uint32(buf: io.BufferedReader) -> int: + return int.from_bytes(_read(buf, 4), "little", signed=False) -def read_var_int(buf: bytes) -> tuple[int, bytes]: - """ - Read a variable-length integer from bytes at given offset. - Returns (value, new_offset). - """ +def read_var_int(buf: io.BytesIO) -> int: + """Read a variable-length integer from the stream.""" result = 0 shift = 0 - offset = 0 while True: - if offset >= len(buf): - msg = "unexpected end of data while reading varint" - raise ValueError(msg) - byte = buf[offset] - offset += 1 + byte = read_uint8(buf) result |= (byte & 0x7F) << shift if (byte & 0x80) == 0: break shift += 7 - return result, buf[offset:] + return result -def read_fixed_bytes(buf: bytes, size: int) -> tuple[bytes, bytes]: - """ - Read a fixed number of bytes from data at given offset. - Returns (bytes_read, new_offset). - """ - if size > len(buf): - msg = "unexpected end of data while reading fixed bytes" - raise ValueError(msg) - return buf[0:size], buf[size:] +def read_fixed_bytes(buf: io.BytesIO, size: int) -> bytes: + """Read a fixed number of bytes from the stream.""" + return _read(buf, size) -def read_str(buf: bytes) -> tuple[str, bytes]: - """ - Read a variable-length string (varint length + utf8 data). - Returns (string, new_offset). - """ - length, buf = read_var_int(buf) - raw, buf = read_fixed_bytes(buf, length) - return raw.decode("utf-8"), buf +def read_str(buf: io.BytesIO) -> str: + """Read a variable-length string (varint length + utf8 data) from the stream.""" + length = read_var_int(buf) + raw = read_fixed_bytes(buf, length) + return raw.decode("utf-8") diff --git a/pactus/transaction/payload/bond.py b/pactus/transaction/payload/bond.py index 67c4664..be44fed 100644 --- a/pactus/transaction/payload/bond.py +++ b/pactus/transaction/payload/bond.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pactus.crypto.address import Address from pactus.crypto.bls.public_key import PublicKey as BLSPublicKey from pactus.encoding import encoding @@ -7,6 +9,9 @@ from ._payload import PayloadType +if TYPE_CHECKING: + import io + class BondPayload: def __init__( @@ -21,15 +26,15 @@ def __init__( self.public_key = public_key self.stake = stake - def encode(self, buf: bytes) -> bytes: - buf = self.sender.encode(buf) - buf = self.receiver.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.sender.encode(buf) + self.receiver.encode(buf) if self.public_key is not None: - buf = encoding.append_var_int(buf, 96) - buf = self.public_key.encode(buf) + encoding.append_var_int(buf, 96) + self.public_key.encode(buf) else: - buf = encoding.append_var_int(buf, 0) - return self.stake.encode(buf) + encoding.append_var_int(buf, 0) + self.stake.encode(buf) def get_type(self) -> PayloadType: return PayloadType.BOND @@ -38,17 +43,17 @@ def signer(self) -> Address: return self.sender @classmethod - def decode(cls, buf: bytes) -> tuple: - sender, buf = Address.decode(buf) - receiver, buf = Address.decode(buf) + def decode(cls, buf: io.BufferedReader) -> BondPayload: + sender = Address.decode(buf) + receiver = Address.decode(buf) - pub_key_size, buf = encoding.read_var_int(buf) + pub_key_size = encoding.read_var_int(buf) public_key = None if pub_key_size == 96: - public_key, buf = BLSPublicKey.decode(buf) + public_key = BLSPublicKey.decode(buf) elif pub_key_size != 0: msg = f"invalid public key size: {pub_key_size}" raise ValueError(msg) - stake, buf = Amount.decode(buf) - return cls(sender, receiver, public_key, stake), buf + stake = Amount.decode(buf) + return cls(sender, receiver, public_key, stake) diff --git a/pactus/transaction/payload/sortition.py b/pactus/transaction/payload/sortition.py index f8a007d..1c99a94 100644 --- a/pactus/transaction/payload/sortition.py +++ b/pactus/transaction/payload/sortition.py @@ -1,3 +1,5 @@ +import io + from pactus.crypto.address import Address from pactus.encoding import encoding @@ -9,9 +11,9 @@ def __init__(self, address: Address, proof: bytes) -> None: self.address = address self.proof = proof - def encode(self, buf: bytes) -> bytes: - buf = self.address.encode(buf) - return encoding.append_fixed_bytes(buf, self.proof) + def encode(self, buf: io.Writer) -> None: + self.address.encode(buf) + encoding.append_fixed_bytes(buf, self.proof) def get_type(self) -> PayloadType: return PayloadType.SORTITION @@ -20,7 +22,7 @@ def signer(self) -> Address: return self.address @classmethod - def decode(cls, buf: bytes) -> tuple: - address, buf = Address.decode(buf) - proof, buf = encoding.read_fixed_bytes(buf, 48) - return cls(address, proof), buf + def decode(cls, buf: io.BufferedReader) -> "SortitionPayload": + address = Address.decode(buf) + proof = encoding.read_fixed_bytes(buf, 48) + return cls(address, proof) diff --git a/pactus/transaction/payload/transfer.py b/pactus/transaction/payload/transfer.py index 44c06a8..9d53747 100644 --- a/pactus/transaction/payload/transfer.py +++ b/pactus/transaction/payload/transfer.py @@ -1,3 +1,5 @@ +import io + from pactus.crypto.address import Address from pactus.types.amount import Amount @@ -10,10 +12,10 @@ def __init__(self, sender: Address, receiver: Address, amount: Amount) -> None: self.receiver = receiver self.amount = amount - def encode(self, buf: bytes) -> bytes: - buf = self.sender.encode(buf) - buf = self.receiver.encode(buf) - return self.amount.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.sender.encode(buf) + self.receiver.encode(buf) + self.amount.encode(buf) def get_type(self) -> PayloadType: return PayloadType.TRANSFER @@ -22,8 +24,8 @@ def signer(self) -> Address: return self.sender @classmethod - def decode(cls, buf: bytes) -> tuple: - sender, buf = Address.decode(buf) - receiver, buf = Address.decode(buf) - amount, buf = Amount.decode(buf) - return cls(sender, receiver, amount), buf + def decode(cls, buf: io.BufferedReader) -> "TransferPayload": + sender = Address.decode(buf) + receiver = Address.decode(buf) + amount = Amount.decode(buf) + return cls(sender, receiver, amount) diff --git a/pactus/transaction/payload/unbond.py b/pactus/transaction/payload/unbond.py index f98a49c..40bd91e 100644 --- a/pactus/transaction/payload/unbond.py +++ b/pactus/transaction/payload/unbond.py @@ -1,3 +1,5 @@ +import io + from pactus.crypto.address import Address from ._payload import PayloadType @@ -7,8 +9,8 @@ class UnbondPayload: def __init__(self, validator: Address) -> None: self.validator = validator - def encode(self, buf: bytes) -> bytes: - return self.validator.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.validator.encode(buf) def get_type(self) -> PayloadType: return PayloadType.UNBOND @@ -17,6 +19,6 @@ def signer(self) -> Address: return self.validator @classmethod - def decode(cls, buf: bytes) -> tuple: - validator, buf = Address.decode(buf) - return cls(validator), buf + def decode(cls, buf: io.BufferedReader) -> "UnbondPayload": + validator = Address.decode(buf) + return cls(validator) diff --git a/pactus/transaction/payload/withdraw.py b/pactus/transaction/payload/withdraw.py index 50e9525..1e548ce 100644 --- a/pactus/transaction/payload/withdraw.py +++ b/pactus/transaction/payload/withdraw.py @@ -1,3 +1,5 @@ +import io + from pactus.crypto.address import Address from pactus.types.amount import Amount @@ -10,10 +12,10 @@ def __init__(self, from_addr: Address, to_addr: Address, amount: Amount) -> None self.to_addr = to_addr self.amount = amount - def encode(self, buf: bytes) -> bytes: - buf = self.from_addr.encode(buf) - buf = self.to_addr.encode(buf) - return self.amount.encode(buf) + def encode(self, buf: io.Writer) -> None: + self.from_addr.encode(buf) + self.to_addr.encode(buf) + self.amount.encode(buf) def get_type(self) -> PayloadType: return PayloadType.WITHDRAW @@ -22,8 +24,8 @@ def signer(self) -> Address: return self.from_addr @classmethod - def decode(cls, buf: bytes) -> tuple: - from_addr, buf = Address.decode(buf) - to_addr, buf = Address.decode(buf) - amount, buf = Amount.decode(buf) - return cls(from_addr, to_addr, amount), buf + def decode(cls, buf: io.BufferedReader) -> "WithdrawPayload": + from_addr = Address.decode(buf) + to_addr = Address.decode(buf) + amount = Amount.decode(buf) + return cls(from_addr, to_addr, amount) diff --git a/pactus/transaction/transaction.py b/pactus/transaction/transaction.py index 6ee4902..321e46d 100644 --- a/pactus/transaction/transaction.py +++ b/pactus/transaction/transaction.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import io from pactus.crypto.address import Address, AddressType from pactus.crypto.bls.public_key import PublicKey as BLSPublicKey @@ -49,28 +50,25 @@ def __init__( self.signature: Signature = None @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Transaction from bytes. - Returns (Transaction, remaining_buf). - """ - flags, buf = encoding.read_uint8(buf) - version, buf = encoding.read_uint8(buf) - lock_time, buf = Height.decode(buf) - fee, buf = Amount.decode(buf) - memo, buf = encoding.read_str(buf) - payload_type, buf = encoding.read_uint8(buf) + def decode(cls, buf: io.BufferedReader) -> Transaction: + """Decode a Transaction from the stream.""" + flags = encoding.read_uint8(buf) + version = encoding.read_uint8(buf) + lock_time = Height.decode(buf) + fee = Amount.decode(buf) + memo = encoding.read_str(buf) + payload_type = encoding.read_uint8(buf) if payload_type == PayloadType.TRANSFER.value: - payload, buf = TransferPayload.decode(buf) + payload = TransferPayload.decode(buf) elif payload_type == PayloadType.BOND.value: - payload, buf = BondPayload.decode(buf) + payload = BondPayload.decode(buf) elif payload_type == PayloadType.SORTITION.value: - payload, buf = SortitionPayload.decode(buf) + payload = SortitionPayload.decode(buf) elif payload_type == PayloadType.UNBOND.value: - payload, buf = UnbondPayload.decode(buf) + payload = UnbondPayload.decode(buf) elif payload_type == PayloadType.WITHDRAW.value: - payload, buf = WithdrawPayload.decode(buf) + payload = WithdrawPayload.decode(buf) else: msg = f"unknown payload type: {payload_type}" raise ValueError(msg) @@ -86,20 +84,18 @@ def decode(cls, buf: bytes) -> tuple: tx.signature = None if flags & FLAG_NOT_SIGNED: - return tx, buf + return tx signer_type = payload.signer().address_type() - sig, buf = Transaction._decode_signature(buf, signer_type) - tx.signature = sig + tx.signature = Transaction._decode_signature(buf, signer_type) if (flags & FLAG_STRIPPED_PUBLIC_KEY) == 0: - pub, buf = Transaction._decode_public_key(buf, signer_type) - tx.public_key = pub + tx.public_key = Transaction._decode_public_key(buf, signer_type) - return tx, buf + return tx @staticmethod - def _decode_signature(buf: bytes, signer_type: AddressType) -> tuple: + def _decode_signature(buf: io.BufferedReader, signer_type: AddressType) -> Signature: if signer_type in (AddressType.VALIDATOR, AddressType.BLS_ACCOUNT): return BLSSignature.decode(buf) if signer_type == AddressType.ED25519_ACCOUNT: @@ -110,7 +106,7 @@ def _decode_signature(buf: bytes, signer_type: AddressType) -> tuple: raise ValueError(msg) @staticmethod - def _decode_public_key(buf: bytes, signer_type: AddressType) -> tuple: + def _decode_public_key(buf: io.BufferedReader, signer_type: AddressType) -> PublicKey: if signer_type in (AddressType.VALIDATOR, AddressType.BLS_ACCOUNT): return BLSPublicKey.decode(buf) if signer_type == AddressType.ED25519_ACCOUNT: @@ -170,20 +166,21 @@ def create_withdraw_tx( payload = WithdrawPayload(from_addr, to_addr, amount) return cls(lock_time, fee, memo, payload) - def _get_unsigned_bytes(self, buf: bytes) -> bytes: - """Generate the unsigned bytes of the transaction for signing.""" - buf = encoding.append_uint8(buf, self.flags) - buf = encoding.append_uint8(buf, self.version) - buf = self.lock_time.encode(buf) - buf = self.fee.encode(buf) - buf = encoding.append_str(buf, self.memo) - buf = encoding.append_uint8(buf, self.payload.get_type().value) - return self.payload.encode(buf) + def _get_unsigned_bytes(self, buf: io.Writer) -> None: + """Write the unsigned bytes of the transaction to the buffer.""" + encoding.append_uint8(buf, self.flags) + encoding.append_uint8(buf, self.version) + self.lock_time.encode(buf) + self.fee.encode(buf) + encoding.append_str(buf, self.memo) + encoding.append_uint8(buf, self.payload.get_type().value) + self.payload.encode(buf) def sign_bytes(self) -> bytes: """Return the bytes to be signed (everything except flags).""" - buf = self._get_unsigned_bytes(b"") - return buf[1:] + buf = io.BytesIO() + self._get_unsigned_bytes(buf) + return buf.getvalue()[1:] def id(self) -> Hash: """Return the transaction ID (blake2b-256 of sign bytes).""" @@ -191,15 +188,16 @@ def id(self) -> Hash: def sign(self, private_key: PrivateKey) -> bytes: """Sign the transaction and return signed bytes.""" - buf = self._get_unsigned_bytes(b"") - sig = private_key.sign(buf[1:]) + buf = io.BytesIO() + self._get_unsigned_bytes(buf) + sig = private_key.sign(buf.getvalue()[1:]) pub = private_key.public_key() - buf = sig.encode(buf) - buf = pub.encode(buf) + sig.encode(buf) + pub.encode(buf) self.public_key = pub self.signature = sig self.flags |= FLAG_NOT_SIGNED - return buf + return buf.getvalue() diff --git a/pactus/types/amount.py b/pactus/types/amount.py index 6fe044b..7548f8e 100644 --- a/pactus/types/amount.py +++ b/pactus/types/amount.py @@ -1,3 +1,4 @@ +import io import math from pactus.encoding import encoding @@ -79,17 +80,14 @@ def from_string(cls, s: str) -> "Amount": return cls.from_pac(f) - def encode(self, buf: bytes) -> bytes: - return encoding.append_var_int(buf, self.value) + def encode(self, buf: io.Writer) -> None: + encoding.append_var_int(buf, self.value) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode an Amount from bytes. - Returns (Amount, remaining_buf). - """ - val, buf = encoding.read_var_int(buf) - return cls(val), buf + def decode(cls, buf: io.BufferedReader) -> "Amount": + """Decode an Amount from the stream.""" + val = encoding.read_var_int(buf) + return cls(val) @staticmethod def _round(f: float) -> float: diff --git a/pactus/types/height.py b/pactus/types/height.py index 192b3a9..522b828 100644 --- a/pactus/types/height.py +++ b/pactus/types/height.py @@ -1,3 +1,5 @@ +import io + from pactus.encoding import encoding @@ -24,14 +26,11 @@ def __hash__(self) -> int: def __str__(self) -> str: return str(self.value) - def encode(self, buf: bytes) -> bytes: - return encoding.append_uint32(buf, self.value) + def encode(self, buf: io.Writer) -> None: + encoding.append_uint32(buf, self.value) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Height from bytes. - Returns (Height, remaining_buf). - """ - val, buf = encoding.read_uint32(buf) - return cls(val), buf + def decode(cls, buf: io.BufferedReader) -> "Height": + """Decode a Height from the stream.""" + val = encoding.read_uint32(buf) + return cls(val) diff --git a/pactus/types/round.py b/pactus/types/round.py index 47f7343..21f1417 100644 --- a/pactus/types/round.py +++ b/pactus/types/round.py @@ -1,3 +1,5 @@ +import io + from pactus.encoding import encoding @@ -23,14 +25,11 @@ def __hash__(self) -> int: def __str__(self) -> str: return str(self.value) - def encode(self, buf: bytes) -> bytes: - return encoding.append_uint16(buf, self.value) + def encode(self, buf: io.Writer) -> None: + encoding.append_uint16(buf, self.value) @classmethod - def decode(cls, buf: bytes) -> tuple: - """ - Decode a Round from bytes. - Returns (Round, remaining_buf). - """ - val, buf = encoding.read_uint16(buf) - return cls(val), buf + def decode(cls, buf: io.BufferedReader) -> "Round": + """Decode a Round from the stream.""" + val = encoding.read_uint16(buf) + return cls(val) diff --git a/tests/test_amount.py b/tests/test_amount.py index e92e954..fa62293 100644 --- a/tests/test_amount.py +++ b/tests/test_amount.py @@ -1,3 +1,4 @@ +import io import unittest from pactus.types.amount import NANO_PAC_PER_PAC, Amount @@ -220,5 +221,43 @@ def test_round_trip_conversion(self): self.assertAlmostEqual(back_to_pac, pac_value, places=9) + def test_encode_decode(self): + """Test that encoding and decoding an Amount round-trips correctly.""" + test_cases = [ + Amount(0), + Amount.from_pac(42.5), + Amount.from_pac(1.0), + Amount.from_pac(0.5), + Amount.from_pac(0.000000001), + Amount.from_nano_pac(1000000000), + Amount.from_nano_pac(500000000), + Amount.from_nano_pac(1), + Amount.from_nano_pac(123456789), + ] + + for original in test_cases: + buf = io.BytesIO() + original.encode(buf) + + buf.seek(0) + decoded = Amount.decode(buf) + self.assertEqual(decoded, original) + + def test_encode_known_values(self): + """Test that encoding produces expected byte sequences.""" + test_cases = [ + (Amount(0), b"\x00"), + (Amount(1), b"\x01"), + (Amount(0xFF), b"\xff\x01"), + (Amount.from_nano_pac(1000000000), b"\x80\x94\xeb\xdc\x03"), + (Amount.from_nano_pac(42_000_000_000), b"\x80\xc8\x94\xbb\x9c\x01"), + ] + + for amount, expected_bytes in test_cases: + buf = io.BytesIO() + amount.encode(buf) + self.assertEqual(buf.getvalue(), expected_bytes) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_encoding.py b/tests/test_encoding.py index 19637bb..53eb221 100644 --- a/tests/test_encoding.py +++ b/tests/test_encoding.py @@ -1,4 +1,5 @@ import unittest +import io from pactus.encoding.encoding import ( append_uint8, @@ -23,13 +24,13 @@ def test_uint8(self): ] for _, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_uint8(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_uint8(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_uint8(buf) + buf.seek(0) + read = read_uint8(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_uint16(self): tests = [ @@ -41,13 +42,13 @@ def test_uint16(self): ] for _, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_uint16(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_uint16(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_uint16(buf) + buf.seek(0) + read = read_uint16(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_uint32(self): tests = [ @@ -59,13 +60,13 @@ def test_uint32(self): ] for _, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_uint32(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_uint32(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_uint32(buf) + buf.seek(0) + read = read_uint32(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_var_int(self): tests = [ @@ -89,13 +90,13 @@ def test_var_int(self): ] for i, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_var_int(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_var_int(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_var_int(buf) + buf.seek(0) + read = read_var_int(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) def test_str(self): # str256 is a string that takes a 2-byte varint to encode. @@ -108,13 +109,13 @@ def test_str(self): ] for i, (value, expected_bytes) in enumerate(tests): - buf = b"" - buf = append_str(buf, value) - self.assertEqual(buf, expected_bytes) + buf = io.BytesIO() + append_str(buf, value) + self.assertEqual(buf.getvalue(), expected_bytes) - read, buf = read_str(buf) + buf.seek(0) + read = read_str(buf) self.assertEqual(read, value) - self.assertEqual(len(buf), 0) if __name__ == "__main__": diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 4f0286f..6aeaae3 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -1,3 +1,4 @@ +import io import unittest from pactus.crypto.address import Address @@ -76,7 +77,7 @@ def test_decode_transfer_tx(self): ) raw = bytes.fromhex(signed_data_hex) - tx, _ = Transaction.decode(raw) + tx = Transaction.decode(io.BytesIO(raw)) self.assertEqual(tx.version, 1) self.assertEqual(tx.lock_time.value, 2335524) @@ -128,7 +129,7 @@ def test_decode_bond_tx(self): ) raw = bytes.fromhex(signed_data_hex) - tx, _ = Transaction.decode(raw) + tx = Transaction.decode(io.BytesIO(raw)) self.assertEqual(tx.version, 1) self.assertEqual(tx.lock_time.value, 2335580) From 94d3493435bee47c5ed0818959ac2b7671b64131 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 Jul 2026 23:09:08 +0800 Subject: [PATCH 2/3] fix: make Block.decode accept io.BufferedReader like all other decoders Block.decode was the only remaining decode() method that took raw bytes and wrapped them internally. Now it takes io.BufferedReader directly, consistent with every other decode() in the codebase. --- examples/example_decode_block.py | 3 ++- pactus/block/block.py | 6 ++---- tests/test_block.py | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/example_decode_block.py b/examples/example_decode_block.py index d172f05..63b63e5 100644 --- a/examples/example_decode_block.py +++ b/examples/example_decode_block.py @@ -11,6 +11,7 @@ """ import asyncio +import io import random from pactus.block import Block @@ -34,7 +35,7 @@ async def fetch_and_decode_block(): print(f"\nFetching block at height {height}...") res = await client.pactus.blockchain.get_block(height=height, verbosity=0) - block = Block.decode(bytes.fromhex(res["data"])) + block = Block.decode(io.BytesIO(bytes.fromhex(res["data"]))) print(f" - Hash: {block.id}") print(f" - Version: {block.header.version}") diff --git a/pactus/block/block.py b/pactus/block/block.py index 956a7c8..dab5b5d 100644 --- a/pactus/block/block.py +++ b/pactus/block/block.py @@ -24,10 +24,8 @@ def __init__( self.transactions = transactions @classmethod - def decode(cls, data: bytes) -> Block: - """Decode a Block from raw bytes.""" - buf = io.BytesIO(data) - + def decode(cls, buf: io.BufferedReader) -> Block: + """Decode a Block from the stream.""" header = Header.decode(buf) # Genesis block has no certificate diff --git a/tests/test_block.py b/tests/test_block.py index 8cad8d8..63b7834 100644 --- a/tests/test_block.py +++ b/tests/test_block.py @@ -1,3 +1,4 @@ +import io import unittest from pactus.block import Block @@ -34,7 +35,7 @@ def test_decode_block_88888(self): ) raw = bytes.fromhex(raw_hex) - block = Block.decode(raw) + block = Block.decode(io.BytesIO(raw)) # --- Header --- h = block.header From db428124d997222360857c8437cbe3e8c1753b89 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 2 Jul 2026 23:22:24 +0800 Subject: [PATCH 3/3] chore: fix linting issues --- examples/example_decode_block.py | 3 ++- pactus/block/certificate.py | 4 +++- pactus/block/header.py | 9 +++++++-- pactus/encoding/encoding.py | 7 ++++++- pactus/transaction/payload/sortition.py | 9 +++++++-- pactus/transaction/payload/transfer.py | 9 +++++++-- pactus/transaction/payload/unbond.py | 9 +++++++-- pactus/transaction/payload/withdraw.py | 9 +++++++-- pactus/types/amount.py | 18 ++++++++++++------ pactus/types/height.py | 11 ++++++++--- pactus/types/round.py | 11 ++++++++--- tests/test_amount.py | 1 - tests/test_transaction.py | 8 ++++++-- 13 files changed, 80 insertions(+), 28 deletions(-) diff --git a/examples/example_decode_block.py b/examples/example_decode_block.py index 63b63e5..8d24871 100644 --- a/examples/example_decode_block.py +++ b/examples/example_decode_block.py @@ -19,6 +19,7 @@ TESTNET_RPC = "https://testnet1.pactus.org/jsonrpc" + async def fetch_and_decode_block(): client = PactusOpenRPCClient( headers={}, @@ -49,10 +50,10 @@ async def fetch_and_decode_block(): print(f" - Absentees: {block.prev_cert.absentees}") print(f" - Signature: {block.prev_cert.signature.string()}") - print(" - Transactions:") for i, tx in enumerate(block.transactions): print(f" - {i}: {tx.id()}") + if __name__ == "__main__": asyncio.run(fetch_and_decode_block()) diff --git a/pactus/block/certificate.py b/pactus/block/certificate.py index aa59d7a..5ed669d 100644 --- a/pactus/block/certificate.py +++ b/pactus/block/certificate.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import hashlib import io @@ -23,7 +25,7 @@ def __init__( self.signature = signature @classmethod - def decode(cls, buf: io.BufferedReader) -> "Certificate": + def decode(cls, buf: io.BufferedReader) -> Certificate: """Decode a Certificate from the stream.""" height = Height.decode(buf) round_ = Round.decode(buf) diff --git a/pactus/block/header.py b/pactus/block/header.py index 2f64afe..57d3426 100644 --- a/pactus/block/header.py +++ b/pactus/block/header.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.crypto.address import Address from pactus.crypto.hash import Hash @@ -23,7 +28,7 @@ def __init__( self.proposer_address = proposer_address @classmethod - def decode(cls, buf: io.BufferedReader) -> "Header": + def decode(cls, buf: io.BufferedReader) -> Header: """Decode a Header from the stream.""" version = encoding.read_uint8(buf) unix_time = encoding.read_uint32(buf) diff --git a/pactus/encoding/encoding.py b/pactus/encoding/encoding.py index f865538..78f23ed 100644 --- a/pactus/encoding/encoding.py +++ b/pactus/encoding/encoding.py @@ -1,4 +1,3 @@ - from __future__ import annotations from typing import TYPE_CHECKING @@ -32,9 +31,11 @@ def append_var_int(buf: io.Writer, val: int) -> None: buf.write(bytes([val])) + def append_fixed_bytes(buf: io.Writer, data: bytes) -> None: buf.write(data) + def _read(buf: io.BufferedReader, size: int) -> bytes: data = buf.read(size) if len(data) != size: @@ -43,15 +44,19 @@ def _read(buf: io.BufferedReader, size: int) -> bytes: return data + def read_uint8(buf: io.BufferedReader) -> int: return int.from_bytes(_read(buf, 1), "little", signed=False) + def read_uint16(buf: io.BufferedReader) -> int: return int.from_bytes(_read(buf, 2), "little", signed=False) + def read_uint32(buf: io.BufferedReader) -> int: return int.from_bytes(_read(buf, 4), "little", signed=False) + def read_var_int(buf: io.BytesIO) -> int: """Read a variable-length integer from the stream.""" result = 0 diff --git a/pactus/transaction/payload/sortition.py b/pactus/transaction/payload/sortition.py index 1c99a94..f22f131 100644 --- a/pactus/transaction/payload/sortition.py +++ b/pactus/transaction/payload/sortition.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.crypto.address import Address from pactus.encoding import encoding @@ -22,7 +27,7 @@ def signer(self) -> Address: return self.address @classmethod - def decode(cls, buf: io.BufferedReader) -> "SortitionPayload": + def decode(cls, buf: io.BufferedReader) -> SortitionPayload: address = Address.decode(buf) proof = encoding.read_fixed_bytes(buf, 48) return cls(address, proof) diff --git a/pactus/transaction/payload/transfer.py b/pactus/transaction/payload/transfer.py index 9d53747..2fda5ec 100644 --- a/pactus/transaction/payload/transfer.py +++ b/pactus/transaction/payload/transfer.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.crypto.address import Address from pactus.types.amount import Amount @@ -24,7 +29,7 @@ def signer(self) -> Address: return self.sender @classmethod - def decode(cls, buf: io.BufferedReader) -> "TransferPayload": + def decode(cls, buf: io.BufferedReader) -> TransferPayload: sender = Address.decode(buf) receiver = Address.decode(buf) amount = Amount.decode(buf) diff --git a/pactus/transaction/payload/unbond.py b/pactus/transaction/payload/unbond.py index 40bd91e..69bc18c 100644 --- a/pactus/transaction/payload/unbond.py +++ b/pactus/transaction/payload/unbond.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.crypto.address import Address @@ -19,6 +24,6 @@ def signer(self) -> Address: return self.validator @classmethod - def decode(cls, buf: io.BufferedReader) -> "UnbondPayload": + def decode(cls, buf: io.BufferedReader) -> UnbondPayload: validator = Address.decode(buf) return cls(validator) diff --git a/pactus/transaction/payload/withdraw.py b/pactus/transaction/payload/withdraw.py index 1e548ce..e56494c 100644 --- a/pactus/transaction/payload/withdraw.py +++ b/pactus/transaction/payload/withdraw.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.crypto.address import Address from pactus.types.amount import Amount @@ -24,7 +29,7 @@ def signer(self) -> Address: return self.from_addr @classmethod - def decode(cls, buf: io.BufferedReader) -> "WithdrawPayload": + def decode(cls, buf: io.BufferedReader) -> WithdrawPayload: from_addr = Address.decode(buf) to_addr = Address.decode(buf) amount = Amount.decode(buf) diff --git a/pactus/types/amount.py b/pactus/types/amount.py index 7548f8e..662d9c7 100644 --- a/pactus/types/amount.py +++ b/pactus/types/amount.py @@ -1,4 +1,10 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io + import math from pactus.encoding import encoding @@ -23,7 +29,7 @@ class Amount: def __init__(self, value: int = 0) -> None: self.value = value - def __eq__(self, other: "Amount") -> bool: + def __eq__(self, other: Amount) -> bool: if isinstance(other, Amount): return self.value == other.value @@ -46,12 +52,12 @@ def to_nano_pac(self) -> int: return self.value @classmethod - def from_nano_pac(cls, a: int) -> "Amount": + def from_nano_pac(cls, a: int) -> Amount: """Store the value as NanoPAC in the Amount instance.""" return cls(a) @classmethod - def from_pac(cls, f: float) -> "Amount": + def from_pac(cls, f: float) -> Amount: """ Convert a floating-point value in PAC to NanoPAC and store it in the Amount instance. @@ -65,7 +71,7 @@ def from_pac(cls, f: float) -> "Amount": return cls.from_nano_pac(int(cls._round(f * NANO_PAC_PER_PAC))) @classmethod - def from_string(cls, s: str) -> "Amount": + def from_string(cls, s: str) -> Amount: """ Parse a string representing a value in PAC, converts it to NanoPAC, and updates the Amount object. @@ -84,7 +90,7 @@ def encode(self, buf: io.Writer) -> None: encoding.append_var_int(buf, self.value) @classmethod - def decode(cls, buf: io.BufferedReader) -> "Amount": + def decode(cls, buf: io.BufferedReader) -> Amount: """Decode an Amount from the stream.""" val = encoding.read_var_int(buf) return cls(val) diff --git a/pactus/types/height.py b/pactus/types/height.py index 522b828..f2e6b10 100644 --- a/pactus/types/height.py +++ b/pactus/types/height.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.encoding import encoding @@ -14,7 +19,7 @@ class Height: def __init__(self, value: int = 0) -> None: self.value = value - def __eq__(self, other: "Height") -> bool: + def __eq__(self, other: Height) -> bool: if isinstance(other, Height): return self.value == other.value @@ -30,7 +35,7 @@ def encode(self, buf: io.Writer) -> None: encoding.append_uint32(buf, self.value) @classmethod - def decode(cls, buf: io.BufferedReader) -> "Height": + def decode(cls, buf: io.BufferedReader) -> Height: """Decode a Height from the stream.""" val = encoding.read_uint32(buf) return cls(val) diff --git a/pactus/types/round.py b/pactus/types/round.py index 21f1417..3be4c25 100644 --- a/pactus/types/round.py +++ b/pactus/types/round.py @@ -1,4 +1,9 @@ -import io +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import io from pactus.encoding import encoding @@ -13,7 +18,7 @@ class Round: def __init__(self, value: int = 0) -> None: self.value = value - def __eq__(self, other: "Round") -> bool: + def __eq__(self, other: Round) -> bool: if isinstance(other, Round): return self.value == other.value @@ -29,7 +34,7 @@ def encode(self, buf: io.Writer) -> None: encoding.append_uint16(buf, self.value) @classmethod - def decode(cls, buf: io.BufferedReader) -> "Round": + def decode(cls, buf: io.BufferedReader) -> Round: """Decode a Round from the stream.""" val = encoding.read_uint16(buf) return cls(val) diff --git a/tests/test_amount.py b/tests/test_amount.py index fa62293..476233f 100644 --- a/tests/test_amount.py +++ b/tests/test_amount.py @@ -220,7 +220,6 @@ def test_round_trip_conversion(self): self.assertEqual(amount, amount_from_nano) self.assertAlmostEqual(back_to_pac, pac_value, places=9) - def test_encode_decode(self): """Test that encoding and decoding an Amount round-trips correctly.""" test_cases = [ diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 6aeaae3..bd120e9 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -68,7 +68,9 @@ def test_decode_transfer_tx(self): "8dea6d79ee7ec60f66433f189ed9b3c50b2ad6fa004e26790ee736693eda8506" "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa" ) - expected_txid = "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f" + expected_txid = ( + "1b6b7226f7935a15f05371d1a1fefead585a89704ce464b7cc1d453d299d235f" + ) expected_sign_bytes = ( "0124a3230080ade2040b77616c6c65742d636f726501" "037098338e0b6808119dfd4457ab806b9c2059b89b" @@ -119,7 +121,9 @@ def test_decode_bond_tx(self): "715b9149afbd94c5d8ee6b37c787ec63e963cbb38be513ebc436aa58f9a8f00d" "95794161374b22c696dabb98e93f6ca9300b22f3b904921fbf560bb72145f4fa" ) - expected_txid = "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80" + expected_txid = ( + "f83f583a5c40adf93a90ea536a7e4b467d30ca4f308d5da52624d80c42adec80" + ) expected_sign_bytes = ( "015ca3230080ade2040b77616c6c65742d636f726502" "037098338e0b6808119dfd4457ab806b9c2059b89b"