Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HDwallet in python for ton blockchain #24

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ venv.bak/
.DS_Store

/sandbox

/tonsdk__venv
115 changes: 115 additions & 0 deletions examples/wallets/test_hd_wallet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from tonsdk.crypto import mnemonic_to_hd_seed
from tonsdk.crypto.hd import derive_mnemonics_path, path_for_account, tg_user_id_to_account
from tonsdk.contract.wallet import Wallets, WalletVersionEnum
from tonsdk.utils import bytes_to_b64str

mnemonics = [
'squeeze', 'afford', 'brother',
'era', 'remain', 'upper',
'region', 'goat', 'dog',
'gain', 'ketchup', 'drip',
'honey', 'coil', 'interest',
'walk', 'merit', 'hidden',
'real', 'puzzle', 'toward',
'penalty', 'answer', 'meat'
]
# mnemonics = mnemonic_new()
version = WalletVersionEnum.v4r2
wc = 0

mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=mnemonics, version=version, workchain=wc)


print("mnemonics", mnemonics)
print("pub_k", bytes_to_b64str(pub_k))
print("priv_k", bytes_to_b64str(priv_k))
print("wallet Bounceble", wallet.address.to_string(True, True, True))
print("wallet UnBounceble", wallet.address.to_string(True, True, False))

root_mnemonic = [
"stock", "spin", "miss",
"term", "actual", "auto",
"ozone", "mass", "labor",
"middle", "grab", "task",
"cool", "tenant", "close",
"invest", "common", "hire",
"aware", "valley", "scene",
"seven", "observe", "trend"
]
root_seed = 'aWyXi7Singg64DJfwlU9JRfZsFMRSgQfJamUOSZl8ggllunSb8ocgqL/ydbxrrfEQP22p3SFn3lzbEv4dnJbng=='
path_0 = [0]
mnemonics_path_0 = [
'crush', 'guitar', 'depth',
'metal', 'social', 'pause',
'angle', 'spread', 'real',
'sphere', 'garbage', 'crime',
'device', 'ostrich', 'keep',
'embody', 'fire', 'plug',
'water', 'stand', 'execute',
'race', 'cattle', 'capable'
]

path_10 = [0, 10, 1000000000]
mnemonics_path_0_10 = [
'venture', 'december', 'exile',
'shell', 'venture', 'chaos',
'edge', 'fiber', 'core',
'woman', 'glance', 'length',
'token', 'sunset', 'cost',
'ankle', 'bird', 'pudding',
'power', 'minimum', 'conduct',
'release', 'easy', 'giraffe'
]

root_hd_seed_from_mnemonic = bytes_to_b64str(mnemonic_to_hd_seed(root_mnemonic))
print("seed", root_hd_seed_from_mnemonic)
print("mnemonic_to_hd_seed Equal root_seed", root_seed == root_hd_seed_from_mnemonic)


derive_mnemonics_path_0 = derive_mnemonics_path(seed=mnemonic_to_hd_seed(root_mnemonic), path=path_10)
print("deriveMnemonicsPath", derive_mnemonics_path_0)
print("deriveMnemonicsPath_0 Equal mnemonics_path_0", derive_mnemonics_path_0 == mnemonics_path_0_10)

mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=derive_mnemonics_path_0, version=version, workchain=wc)
print("pub_k", bytes_to_b64str(pub_k))
print("priv_k", bytes_to_b64str(priv_k))
print("wallet Bounceble", wallet.address.to_string(True, True, True))
print("wallet UnBounceble", wallet.address.to_string(True, True, False))


# for tg integration
tg_user_id = 5432100000
print("tg_userid", tg_user_id)
network, account = tg_user_id_to_account(tg_user_id)
path = path_for_account(network=network, account=account)
print("path", path)

derive_mnemonics_path_tg_userid = derive_mnemonics_path(seed=mnemonic_to_hd_seed(root_mnemonic), path=path)
print("deriveMnemonicsPath", derive_mnemonics_path_tg_userid)

mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=derive_mnemonics_path_tg_userid, version=version, workchain=wc)
print("pub_k", bytes_to_b64str(pub_k))
print("priv_k", bytes_to_b64str(priv_k))
print("wallet Bounceble", wallet.address.to_string(True, True, True))
print("wallet NonBounceble", wallet.address.to_string(True, True, False))

"""to external deploy"""
# boc = wallet.create_init_external_message()


"""to internal deploy"""
# query = my_wallet.create_transfer_message(to_addr=new_wallet.address.to_string(),
# amount=to_nano(0.02, 'ton'),
# state_init=new_wallet.create_state_init()['state_init'],
# seqno=int('wallet seqno'))


"""transfer"""
# query = wallet.create_transfer_message(to_addr='destination address',
# amount=to_nano(float('amount to transfer'), 'ton'),
# payload='message',
# seqno=int('wallet seqno'))


"""then send boc to blockchain"""
# boc = bytes_to_b64str(query["message"].to_boc(False))
3 changes: 2 additions & 1 deletion tonsdk/crypto/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from ._keystore import generate_new_keystore, generate_keystore_key
from ._mnemonic import mnemonic_new, mnemonic_to_wallet_key, mnemonic_is_valid
from ._mnemonic import mnemonic_new, mnemonic_to_wallet_key, mnemonic_is_valid, mnemonic_to_hd_seed
from ._utils import private_key_to_public_key, verify_sign
__all__ = [
'mnemonic_new',
'mnemonic_to_wallet_key',
'mnemonic_is_valid',
'mnemonic_to_hd_seed',

'generate_new_keystore',
'generate_keystore_key',
Expand Down
7 changes: 7 additions & 0 deletions tonsdk/crypto/_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ def mnemonic_to_private_key(mnemo_words: List[str], password: Optional[str] = No
mnemo_words, 'TON default seed'.encode('utf-8'), password)
return crypto_sign_seed_keypair(seed[:32])

def mnemonic_to_hd_seed(mnemo_words: List[str], password: Optional[str] = None) -> bytes:
"""
:rtype: (bytes(public_key), bytes(secret_key))
"""
seed = mnemonic_to_seed(
mnemo_words, 'TON HD Keys seed'.encode('utf-8'), password)
return seed

def mnemonic_to_wallet_key(mnemo_words: List[str], password: Optional[str] = None) -> Tuple[bytes, bytes]:
"""
Expand Down
26 changes: 26 additions & 0 deletions tonsdk/crypto/hd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Derivation path

```python
def path_for_account(network: int = 0, workchain: int = 0, account: int = 0, wallet_version: int = 0):
# network default mainnet 0 and testnet 1
chain = 255 if worckchain === -1 else 0
return [44, 607, network, chain, account, wallet_version] # Last zero is reserved for alternative wallet contracts
```
We propose to use user's id in telegram for creating personal wallets in centralized apps.
For creating a tree of accounts we will use user's telegram id. At the fact that id's value can be much more bigger than HARDENED_OFFSET (0x80000000 in hex, 2147483647 in decimal)
we estimate it multiple of 2000000000 and add 2 to ```network```:

```python
def tg_user_id_to_account(userid: int) -> Tuple[int, int]:
start_limit = 0
step = 2000000000
network = 0
account_id = user_id

while start_limit <= user_id:
start_limit += step
network = (start_limit - step) // step * 2
account_id = user_id - start_limit + step
return [network, account_id]
```
See examples/wallet/test_hd_wallet.py
21 changes: 21 additions & 0 deletions tonsdk/crypto/hd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from .mnemonic import mnemonic_from_random_seed, derive_mnemonic_hardened_key, derive_mnemonics_path, get_mnemonics_master_key_from_seed
from .utils import mnemonic_validate, is_password_needed, is_password_seed, normalize_mnemonic, mnemonic_to_entropy, is_basic_seed, bytes_to_mnemonics, bytes_to_mnemonic_indexes, bytes_to_bits, lpad, path_for_account, tg_user_id_to_account
__all__ = [
'mnemonic_validate',
'is_password_needed',
'is_password_seed',
'normalize_mnemonic',
'mnemonic_to_entropy',
'is_basic_seed',
'bytes_to_mnemonics',
'bytes_to_mnemonic_indexes',
'bytes_to_bits',
'lpad',
'path_for_account',
'tg_user_id_to_account',

'mnemonic_from_random_seed',
'derive_mnemonic_hardened_key',
'derive_mnemonics_path',
'get_mnemonics_master_key_from_seed'
]
49 changes: 49 additions & 0 deletions tonsdk/crypto/hd/mnemonic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import List, Optional, Tuple
import math, hmac, struct, hashlib
from hashlib import pbkdf2_hmac
from .utils import bytes_to_mnemonics, mnemonic_validate

HARDENED_OFFSET = 0x80000000
MNEMONICS_SEED = 'TON Mnemonics HD seed'
PBKDF_ITERATIONS = 100000

def mnemonic_from_random_seed(seed: bytes, words_count: int = 24, password: Optional[str] = None):
bytes_length = math.ceil(words_count * 11 / 8)
current_seed = seed
while True:
entropy = pbkdf2_hmac('sha512', current_seed, 'TON mnemonic seed'.encode('utf-8'), max(1, math.floor(PBKDF_ITERATIONS / 256)), bytes_length)
mnemonics = bytes_to_mnemonics(entropy, words_count)
if mnemonic_validate(mnemonics, password):
return mnemonics
current_seed = entropy

def get_mnemonics_master_key_from_seed(seed: bytes) -> Tuple[bytes, bytes]:
I = hmac.new(MNEMONICS_SEED.encode(
'utf-8'), seed, hashlib.sha512).digest()
IL = I[:32]
IR = I[32:]
return [IL, IR]

def derive_mnemonic_hardened_key(parent: Tuple[bytes, bytes], index: int) -> Tuple[bytes, bytes]:
if index >= HARDENED_OFFSET:
raise ValueError('Key index must be less than offset')

index += HARDENED_OFFSET
buffer = bytearray(4)
struct.pack_into('>I', buffer, 0, index)
data = bytes([0]) + parent[0] + buffer

I = hmac.new(parent[1], data, hashlib.sha512).digest()
IL = I[:32]
IR = I[32:]
return [IL, IR]

def derive_mnemonics_path(seed: bytes, path: List[int], words_count: int = 24, password: Optional[str] = None):
state = get_mnemonics_master_key_from_seed(seed)

while len(path) > 0:
index = path[0]
path = path[1:]
state = derive_mnemonic_hardened_key(state, index)

return mnemonic_from_random_seed(state[0], words_count, password)
78 changes: 78 additions & 0 deletions tonsdk/crypto/hd/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from .wordlist import wordlist
from hashlib import pbkdf2_hmac
from typing import List, Optional, Tuple
import math, hashlib, hmac

PBKDF_ITERATIONS = 100000

def lpad(string, pad_string, length):
while len(string) < length:
string = pad_string + string
return string

def bytes_to_bits(byte_array):
res = ''
for byte in byte_array:
x = byte
res += lpad(bin(x)[2:], '0', 8)
return res

def bytes_to_mnemonic_indexes(src: bytes, words_count: int = 24):
bits = bytes_to_bits(src)
indexes = []
for i in range(words_count):
sl = bits[i * 11: i * 11 + 11]
indexes.append(int(sl, 2))
return indexes

def bytes_to_mnemonics(src: bytes, words_count: int = 24):
mnemonics = bytes_to_mnemonic_indexes(src, words_count)
res = [wordlist[m] for m in mnemonics]
return res

def is_basic_seed(entropy: str | bytes) -> bool:
seed = pbkdf2_hmac("sha512", entropy, 'TON seed version'.encode('utf-8'), max(1, math.floor(PBKDF_ITERATIONS / 256)))
return seed[0] == 0

def mnemonic_to_entropy(mnemo_words: List[str], password: Optional[str] = None):
sign = hmac.new((" ".join(mnemo_words)).encode('utf-8'), bytes(0), hashlib.sha512).digest()
return sign

def normalize_mnemonic(src: List[str]) -> list:
return list(map(lambda v: v.lower().strip(), src))

def is_password_seed(entropy: str | bytes) -> bool:
seed = pbkdf2_hmac("sha512", entropy, 'TON fastseed version', 1, 64)
return seed[0] == 1

def is_password_needed(mnemonic_array: List[str]):
passless_entropy = mnemonic_to_entropy(mnemonic_array)
return (is_password_seed(passless_entropy)) and not (is_basic_seed(passless_entropy))

def mnemonic_validate(mnemonic_array: List[str], password: Optional[str] = None):
mnemonic_array = normalize_mnemonic(mnemonic_array)
for word in mnemonic_array:
if word not in wordlist:
return False

if password and len(password) > 0:
if not is_password_needed(mnemonic_array):
return False
return is_basic_seed(mnemonic_to_entropy(mnemonic_array, password))

def path_for_account(network: int = 0, workchain: int = 0, account: int = 0, wallet_version: int = 0):
# network default mainnet 0 and testnet 1
chain = 255 if workchain == -1 else workchain
return [44, 607, network, chain, account, wallet_version] # Last zero is reserved for alternative wallet contracts

def tg_user_id_to_account(user_id: int) -> Tuple[int, int]:
start_limit = 0
step = 2000000000
network = 0
account_id = user_id

while start_limit <= user_id:
start_limit += step
network = (start_limit - step) // step * 2
account_id = user_id - start_limit + step
return [network, account_id]
Loading