Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Folders stuff
experiment/
.venv/
venv/
.idea/
.tox/
.vs/
Expand Down
14 changes: 13 additions & 1 deletion docs/addresses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Addresses

>>> from hdwallet.addresses import ADDRESSES
>>> ADDRESSES.names()
['Algorand', 'Aptos', 'Avalanche', 'Cardano', 'Cosmos', 'EOS', 'Ergo', 'Ethereum', 'Filecoin', 'Harmony', 'Icon', 'Injective', 'Monero', 'MultiversX', 'Nano', 'Near', 'Neo', 'OKT-Chain', 'P2PKH', 'P2SH', 'P2TR', 'P2WPKH', 'P2WPKH-In-P2SH', 'P2WSH', 'P2WSH-In-P2SH', 'Ripple', 'Solana', 'Stellar', 'Sui', 'Tezos', 'Tron', 'XinFin', 'Zilliqa']
['Algorand', 'Aptos', 'Avalanche', 'Cardano', 'Cosmos', 'EOS', 'Ergo', 'Ethereum', 'Filecoin', 'Harmony', 'Icon', 'Injective', 'Monero', 'MultiversX', 'Nano', 'Near', 'Neo', 'OKT-Chain', 'P2PKH', 'P2SH', 'P2TR', 'P2WPKH', 'P2WPKH-In-P2SH', 'P2WSH', 'P2WSH-In-P2SH', 'Ripple', 'Solana', 'Stellar', 'Sui', 'Tezos', 'Ton', 'Tron', 'XinFin', 'Zilliqa']
>>> ADDRESSES.classes()
[<class 'hdwallet.addresses.algorand.AlgorandAddress'>, <class 'hdwallet.addresses.aptos.AptosAddress'>, <class 'hdwallet.addresses.avalanche.AvalancheAddress'>, <class 'hdwallet.addresses.cardano.CardanoAddress'>, <class 'hdwallet.addresses.cosmos.CosmosAddress'>, <class 'hdwallet.addresses.eos.EOSAddress'>, <class 'hdwallet.addresses.ergo.ErgoAddress'>, <class 'hdwallet.addresses.ethereum.EthereumAddress'>, <class 'hdwallet.addresses.filecoin.FilecoinAddress'>, <class 'hdwallet.addresses.harmony.HarmonyAddress'>, <class 'hdwallet.addresses.icon.IconAddress'>, <class 'hdwallet.addresses.injective.InjectiveAddress'>, <class 'hdwallet.addresses.monero.MoneroAddress'>, <class 'hdwallet.addresses.multiversx.MultiversXAddress'>, <class 'hdwallet.addresses.nano.NanoAddress'>, <class 'hdwallet.addresses.near.NearAddress'>, <class 'hdwallet.addresses.neo.NeoAddress'>, <class 'hdwallet.addresses.okt_chain.OKTChainAddress'>, <class 'hdwallet.addresses.p2pkh.P2PKHAddress'>, <class 'hdwallet.addresses.p2sh.P2SHAddress'>, <class 'hdwallet.addresses.p2tr.P2TRAddress'>, <class 'hdwallet.addresses.p2wpkh.P2WPKHAddress'>, <class 'hdwallet.addresses.p2wpkh_in_p2sh.P2WPKHInP2SHAddress'>, <class 'hdwallet.addresses.p2wsh.P2WSHAddress'>, <class 'hdwallet.addresses.p2wsh_in_p2sh.P2WSHInP2SHAddress'>, <class 'hdwallet.addresses.ripple.RippleAddress'>, <class 'hdwallet.addresses.solana.SolanaAddress'>, <class 'hdwallet.addresses.stellar.StellarAddress'>, <class 'hdwallet.addresses.sui.SuiAddress'>, <class 'hdwallet.addresses.tezos.TezosAddress'>, <class 'hdwallet.addresses.tron.TronAddress'>, <class 'hdwallet.addresses.xinfin.XinFinAddress'>, <class 'hdwallet.addresses.zilliqa.ZilliqaAddress'>]
>>> from hdwallet.addresses.p2wsh import P2WSHAddress
Expand Down Expand Up @@ -431,6 +431,18 @@ True
>>> TezosAddress.decode(address=address)
'26f0dbb538759db4e8256a6f11ba88ec9c214f11'

.. autoclass:: hdwallet.addresses.ton.TonAddress
:members:

>>> from hdwallet.addresses.ton import TonAddress
>>> TonAddress.name()
'Ton'
>>> address: str = TonAddress.encode(public_key="00d14696583ee9144878635b557d515a502b04366818dfe7765737746b4f57978d")
>>> address
'UQAdGH9zh3uTnsckECChKLGOEmHbOqBJM6P3d2GPHhAtuzS2'
>>> TonAddress.decode(address=address)
'1d187f73877b939ec7241020a128b18e1261db3aa04933a3f777618f1e102dbb'

.. autoclass:: hdwallet.addresses.tron.TronAddress
:members:

Expand Down
8 changes: 8 additions & 0 deletions docs/cryptocurrencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,14 @@ This library simplifies the process of generating a new HDWallet's for:
- ``BIP44`` ``BIP32``
- ✅
- ``P2PKH`` ``P2SH``
* - `Ton <https://github.com/ton-blockchain>`_
- TON
- 607
- ``mainnet``
- SLIP10-Ed25519
- ``BIP32`` ``BIP44``
- ❌
- ``Ton``
* - `Tron <https://github.com/tronprotocol/java-tron>`_
- TRX
- 195
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ Python-based library implementing a Hierarchical Deterministic (HD) Wallet gener
* - Derivations
- ``BIP44``, ``BIP49``, ``BIP84``, ``BIP86``, ``CIP1852``, ``Custom``, ``Electrum``, ``Monero``, ``HDW (Our own custom derivation)``
* - Addresses
- ``Algorand``, ``Aptos``, ``Avalanche``, ``Cardano``, ``Cosmos``, ``EOS``, ``Ergo``, ``Ethereum``, ``Filecoin``, ``Harmony``, ``Icon``, ``Injective``, ``Monero``, ``MultiversX``, ``Nano``, ``Near``, ``Neo``, ``OKT-Chain``, ``P2PKH``, ``P2SH``, ``P2TR``, ``P2WPKH``, ``P2WPKH-In-P2SH``, ``P2WSH``, ``P2WSH-In-P2SH``, ``Ripple``, ``Solana``, ``Stellar``, ``Sui``, ``Tezos``, ``Tron``, ``XinFin``, ``Zilliqa``
- ``Algorand``, ``Aptos``, ``Avalanche``, ``Cardano``, ``Cosmos``, ``EOS``, ``Ergo``, ``Ethereum``, ``Filecoin``, ``Harmony``, ``Icon``, ``Injective``, ``Monero``, ``MultiversX``, ``Nano``, ``Near``, ``Neo``, ``OKT-Chain``, ``P2PKH``, ``P2SH``, ``P2TR``, ``P2WPKH``, ``P2WPKH-In-P2SH``, ``P2WSH``, ``P2WSH-In-P2SH``, ``Ripple``, ``Solana``, ``Stellar``, ``Sui``, ``Tezos``, ``Ton``,``Tron``, ``XinFin``, ``Zilliqa``
* - Others
- ``BIP38``, ``Wallet Import Format``, ``Serialization``
4 changes: 4 additions & 0 deletions hdwallet/addresses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .stellar import StellarAddress
from .sui import SuiAddress
from .tezos import TezosAddress
from .ton import TonAddress
from .tron import TronAddress
from .xinfin import XinFinAddress
from .zilliqa import ZilliqaAddress
Expand Down Expand Up @@ -120,6 +121,8 @@ class ADDRESSES:
+-----------------+------------------------------------------------------------------+
| Tezos | :class:`hdwallet.addresses.tezos.TezosAddress` |
+-----------------+------------------------------------------------------------------+
| Ton | :class:`hdwallet.addresses.ton.TonAddress` |
+-----------------+------------------------------------------------------------------+
| Tron | :class:`hdwallet.addresses.tron.TronAddress` |
+-----------------+------------------------------------------------------------------+
| XinFin | :class:`hdwallet.addresses.xinfin.XinFinAddress` |
Expand Down Expand Up @@ -161,6 +164,7 @@ class ADDRESSES:
StellarAddress.name(): StellarAddress,
SuiAddress.name(): SuiAddress,
TezosAddress.name(): TezosAddress,
TonAddress.name(): TonAddress,
TronAddress.name(): TronAddress,
XinFinAddress.name(): XinFinAddress,
ZilliqaAddress.name(): ZilliqaAddress
Expand Down
158 changes: 158 additions & 0 deletions hdwallet/addresses/ton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3

# Copyright © 2020-2026, Abenezer Lulseged Wube <itsm3abena@gmail.com>
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/license/mit

from typing import (
Any, Union
)

import base64

from ..eccs import (
IPublicKey, SLIP10Ed25519PublicKey, validate_and_get_public_key
)
from ..cryptocurrencies import Ton
from ..crypto import crc16_ccitt
from ..libs.ton.wallet import wallet_v4r2_address_hash, wallet_v5r1_address_hash
from ..utils import (
bytes_to_string
)
from .iaddress import IAddress


class TonAddress(IAddress):

bounceable_tag: int = Ton.PARAMS.BOUNCEABLE_TAG
non_bounceable_tag: int = Ton.PARAMS.NON_BOUNCEABLE_TAG
test_flag: int = Ton.PARAMS.TEST_FLAG

@staticmethod
def name() -> str:
"""
Returns the name of the cryptocurrency.

:return: The name of the cryptocurrency.
:rtype: str
"""

return "Ton"

@classmethod
def encode(cls, public_key: Union[bytes, str, IPublicKey], **kwargs: Any) -> str:
"""
Encodes a public key into a TON user-friendly address (Wallet V5R1 by default).

:param public_key: The public key to encode. Can be of type bytes, str, or IPublicKey.
:type public_key: Union[bytes, str, IPublicKey]
:param kwargs: Additional arguments such as bounceable, testnet, wallet_version,
wallet_id, network_global_id, subwallet_number, and workchain.
:type kwargs: Any

:return: The encoded TON address.
:rtype: str
"""

public_key: IPublicKey = validate_and_get_public_key(
public_key=public_key, public_key_cls=SLIP10Ed25519PublicKey
)
bounceable: bool = kwargs.get("bounceable", Ton.PARAMS.DEFAULT_BOUNCEABLE)
testnet: bool = kwargs.get("testnet", False)
workchain: int = kwargs.get("workchain", Ton.PARAMS.WORKCHAIN)
wallet_version: str = kwargs.get("wallet_version", Ton.PARAMS.DEFAULT_WALLET_VERSION)
public_key_bytes: bytes = public_key.raw_compressed()[1:]

if wallet_version == "v4r2":
wallet_id: int = kwargs.get("wallet_id", Ton.PARAMS.WALLET_ID)
address_hash: bytes = wallet_v4r2_address_hash(
public_key=public_key_bytes,
workchain=workchain,
wallet_id=wallet_id
)
elif wallet_version == "v5r1":
network_global_id: int = kwargs.get(
"network_global_id",
Ton.PARAMS.TESTNET_NETWORK_GLOBAL_ID if testnet else Ton.PARAMS.MAINNET_NETWORK_GLOBAL_ID
)
address_hash = wallet_v5r1_address_hash(
public_key=public_key_bytes,
workchain=workchain,
network_global_id=network_global_id,
subwallet_number=kwargs.get("subwallet_number", Ton.PARAMS.SUBWALLET_NUMBER),
wallet_id=kwargs.get("wallet_id")
)
else:
raise ValueError(f"Unsupported wallet version (got: {wallet_version})")
return cls._encode_friendly_address(
address_hash=address_hash,
workchain=workchain,
bounceable=bounceable,
testnet=testnet
)

@classmethod
def decode(cls, address: str, **kwargs: Any) -> str:
"""
Decodes a TON user-friendly address into its raw account hash.

:param address: The TON address to decode.
:type address: str
:param kwargs: Additional arguments.
:type kwargs: Any

:return: The decoded account hash in its raw form.
:rtype: str
"""

parsed_address = cls._decode_friendly_address(address=address)
return bytes_to_string(parsed_address["hash_part"])

@classmethod
def _encode_friendly_address(
cls,
address_hash: bytes,
workchain: int,
bounceable: bool,
testnet: bool
) -> str:
tag = cls.bounceable_tag if bounceable else cls.non_bounceable_tag
if testnet:
tag |= cls.test_flag

payload = bytes([tag, workchain & 0xFF]) + address_hash
checksum = crc16_ccitt(payload)
encoded = base64.b64encode(payload + checksum).decode("utf-8")
return encoded.replace("+", "-").replace("/", "_")

@classmethod
def _decode_friendly_address(cls, address: str) -> dict:
if len(address) != 48:
raise ValueError(f"Invalid length (expected: 48, got: {len(address)})")

normalized_address = address.replace("-", "+").replace("_", "/")
data = base64.b64decode(normalized_address + "==")
if len(data) != 36:
raise ValueError(f"Invalid address payload length (expected: 36, got: {len(data)})")

payload, checksum = data[:34], data[34:36]
calculated_checksum = crc16_ccitt(payload)
if checksum != calculated_checksum:
raise ValueError("Invalid address checksum")

tag = payload[0]
is_test_only = bool(tag & cls.test_flag)
tag ^= cls.test_flag if is_test_only else 0
if tag not in (cls.bounceable_tag, cls.non_bounceable_tag):
raise ValueError(f"Invalid address tag (got: {hex(tag)})")

workchain = -1 if payload[1] == 0xFF else payload[1]
if workchain not in (0, -1):
raise ValueError(f"Invalid workchain (got: {workchain})")

return {
"is_test_only": is_test_only,
"is_bounceable": tag == cls.bounceable_tag,
"workchain": workchain,
"hash_part": payload[2:34]
}
29 changes: 29 additions & 0 deletions hdwallet/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,35 @@ def crc32(data: Union[bytes, str]) -> bytes:
)


def crc16_ccitt(data: Union[bytes, str]) -> bytes:
"""
Calculate the CRC-16-CCITT checksum of the given data.

:param data: The data to calculate the CRC-16-CCITT checksum for, as bytes or a string.
:type data: Union[bytes, str]

:return: The CRC-16-CCITT checksum as a 2-byte bytes object.
:rtype: bytes
"""

poly = 0x1021
reg = 0
message = encode(data) + b"\x00\x00"

for byte in message:
mask = 0x80
while mask > 0:
reg <<= 1
if byte & mask:
reg += 1
mask >>= 1
if reg > 0xFFFF:
reg &= 0xFFFF
reg ^= poly

return bytes([(reg >> 8) & 0xFF, reg & 0xFF])


def xmodem_crc(data: Union[bytes, str]) -> bytes:
"""
Calculate the XMODEM CRC checksum of the given data.
Expand Down
2 changes: 2 additions & 0 deletions hdwallet/cryptocurrencies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
from .theta import Theta
from .thoughtai import ThoughtAI
from .toacoin import TOACoin
from .ton import Ton
from .tron import Tron
from .twins import TWINS
from .ultimatesecurecash import UltimateSecureCash
Expand Down Expand Up @@ -416,6 +417,7 @@ class CRYPTOCURRENCIES:
Theta.NAME: Theta,
ThoughtAI.NAME: ThoughtAI,
TOACoin.NAME: TOACoin,
Ton.NAME: Ton,
Tron.NAME: Tron,
TWINS.NAME: TWINS,
UltimateSecureCash.NAME: UltimateSecureCash,
Expand Down
79 changes: 79 additions & 0 deletions hdwallet/cryptocurrencies/ton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3

# Copyright © 2020-2026, Abenezer Lulseged Wube <itsm3abena@gmail.com>
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/license/mit

from ..slip44 import CoinTypes
from ..eccs import SLIP10Ed25519ECC
from ..consts import (
Info, Entropies, Mnemonics, Seeds, HDs, Addresses, Networks, Params, XPrivateKeyVersions, XPublicKeyVersions
)
from .icryptocurrency import (
ICryptocurrency, INetwork
)


class Mainnet(INetwork):

NAME = "mainnet"
XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({
"P2PKH": 0x0488ade4
})
XPUBLIC_KEY_VERSIONS = XPublicKeyVersions({
"P2PKH": 0x0488b21e
})


class Ton(ICryptocurrency):

NAME = "Ton"
SYMBOL = "TON"
INFO = Info({
"SOURCE_CODE": "https://github.com/ton-blockchain",
"WHITEPAPER": "https://docs.ton.org/learn/docs",
"WEBSITES": [
"https://ton.org"
]
})
ECC = SLIP10Ed25519ECC
COIN_TYPE = CoinTypes.Ton
SUPPORT_BIP38 = False
NETWORKS = Networks({
"MAINNET": Mainnet
})
DEFAULT_NETWORK = NETWORKS.MAINNET
ENTROPIES = Entropies({
"BIP39"
})
MNEMONICS = Mnemonics({
"BIP39"
})
SEEDS = Seeds({
"BIP39"
})
HDS = HDs({
"BIP32", "BIP44"
})
DEFAULT_HD = HDS.BIP44
DEFAULT_PATH = f"m/44'/{COIN_TYPE}'/0'"
ADDRESSES = Addresses({
"TON": "Ton"
})
DEFAULT_ADDRESS = ADDRESSES.TON
SEMANTICS = [
"p2pkh"
]
DEFAULT_SEMANTIC = "p2pkh"
PARAMS = Params({
"BOUNCEABLE_TAG": 0x11,
"NON_BOUNCEABLE_TAG": 0x51,
"TEST_FLAG": 0x80,
"DEFAULT_BOUNCEABLE": False,
"WORKCHAIN": 0,
"WALLET_ID": 698983191,
"DEFAULT_WALLET_VERSION": "v5r1",
"MAINNET_NETWORK_GLOBAL_ID": -239,
"TESTNET_NETWORK_GLOBAL_ID": -3,
"SUBWALLET_NUMBER": 0
})
8 changes: 7 additions & 1 deletion hdwallet/hds/bip44.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..addresses import P2PKHAddress
from ..exceptions import DerivationError
from ..derivations import (
IDerivation, BIP44Derivation
IDerivation, BIP44Derivation, CustomDerivation
)
from .bip32 import BIP32HD

Expand Down Expand Up @@ -141,6 +141,12 @@ def from_derivation(self, derivation: IDerivation) -> "BIP44HD":
:rtype: BIP44HD
"""

if isinstance(derivation, CustomDerivation):
self.clean_derivation()
self._derivation = derivation
for index in self._derivation.indexes():
self.drive(index)
return self
if not isinstance(derivation, BIP44Derivation):
raise DerivationError(
"Invalid derivation instance", expected=BIP44Derivation, got=type(derivation)
Expand Down
Loading
Loading