Skip to content

Commit 383938a

Browse files
authored
Merge branch 'yungwine:master' into safe_close
2 parents c3f9815 + ad7cfa1 commit 383938a

File tree

9 files changed

+395
-15
lines changed

9 files changed

+395
-15
lines changed

examples/wallets/wallet_v5.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import asyncio
2+
from pytoniq import LiteBalancer
3+
from pytoniq_core import begin_cell, Address
4+
from pytoniq.contract.wallets.wallet_v5 import WalletV5R1
5+
6+
7+
async def main():
8+
async with LiteBalancer.from_testnet_config(trust_level=2) as client:
9+
network_global_id = -3 # -3 for testnet, -239 for mainnet
10+
mnemo = []
11+
wallet = await WalletV5R1.from_mnemonic(client, mnemo, network_global_id=network_global_id)
12+
13+
# deploy wallet if needed
14+
await wallet.deploy_via_external()
15+
16+
await wallet.transfer(
17+
destination='EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c',
18+
body='comment',
19+
amount=1 * 10**8 # 0.1 TON
20+
)
21+
22+
# or create message separately and then sign and send it
23+
message = wallet.create_wallet_internal_message(
24+
destination=Address('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'),
25+
send_mode=3,
26+
body=begin_cell().store_uint(0, 32).store_snake_string('comment').end_cell(),
27+
value=1 * 10**8 # 0.1 TON
28+
)
29+
30+
await wallet.raw_transfer(msgs=[message])
31+
32+
33+
if __name__ == '__main__':
34+
asyncio.run(main())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .wallet import WalletError, Wallet, BaseWallet, WalletV3, WalletV4, WalletV3R1, WalletV3R2, WalletV4R2, WalletV3Data, WalletV4Data, WalletMessage
2+
from .wallet_v5 import WalletV5R1, WalletV5WalletID
23
from .highload import HighloadWallet
34
from .highload_v3 import HighloadWalletV3

pytoniq/contract/wallets/wallet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ async def create(cls, provider: LiteClientLike, wc: int = 0, wallet_id: typing.O
104104
@staticmethod
105105
def raw_create_transfer_msg(private_key: bytes, seqno: int, wallet_id: int, messages: typing.List[WalletMessage],
106106
valid_until: typing.Optional[int] = None) -> Cell:
107+
assert len(messages) <= 4, 'for common wallet maximum messages amount is 4'
107108
signing_message = Builder().store_uint(wallet_id, 32)
108109
if seqno == 0:
109110
signing_message.store_bits('1' * 32) # bin(2**32 - 1)
@@ -127,7 +128,6 @@ async def raw_transfer(self, msgs: typing.List[WalletMessage], seqno_from_get_me
127128
:param msgs: list of WalletMessages. to create one call create_wallet_internal_message meth
128129
:param seqno_from_get_meth: if True LiteClient will request seqno get method and use it, otherwise seqno from contract data will be taken
129130
"""
130-
assert len(msgs) <= 4, 'for common wallet maximum messages amount is 4'
131131
if 'private_key' not in self.__dict__:
132132
raise WalletError('must specify wallet private key!')
133133

@@ -152,6 +152,7 @@ async def send_init_external(self):
152152
if 'private_key' not in self.__dict__:
153153
raise WalletError('must specify wallet private key!')
154154
body = self.raw_create_transfer_msg(private_key=self.private_key, seqno=0, wallet_id=self.wallet_id, messages=[])
155+
155156
return await self.send_external(state_init=self.state_init, body=body)
156157

157158
async def get_seqno(self) -> int:
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import time
2+
import typing
3+
4+
from .wallet import WalletError, BaseWallet
5+
from ...liteclient import LiteClientLike
6+
from pytoniq_core.crypto.keys import private_key_to_public_key, mnemonic_to_private_key, mnemonic_is_valid, mnemonic_new
7+
from pytoniq_core.crypto.signature import sign_message
8+
from pytoniq_core.boc import Cell, Builder, begin_cell
9+
from pytoniq_core.boc.address import Address
10+
from pytoniq_core.tlb.account import StateInit
11+
from pytoniq_core.tlb.custom.wallet import WalletV5R1Data, WalletMessage
12+
13+
WALLET_V5_R1_CODE = Cell.one_from_boc(
14+
b'\xb5\xee\x9crA\x02\x14\x01\x00\x02\x81\x00\x01\x14\xff\x00\xf4\xa4\x13\xf4\xbc\xf2\xc8\x0b\x01\x02\x01 \x02\r\x02\x01H\x03\x04\x02\xdc\xd0 \xd7I\xc1 \x91[\x8fc \xd7\x0b\x1f \x82\x10extn\xbd!\x82\x10sint\xbd\xb0\x92_\x03\xe0\x82\x10extn\xba\x8e\xb4\x80 \xd7!\x01\xd0t\xd7!\xfa@0\xfaD\xf8(\xfaD0X\xbd\x91[\xe0\xedD\xd0\x81\x01A\xd7!\xf4\x05\x83\x07\xf4\x0eo\xa11\x910\xe1\x80@\xd7!p\x7f\xdb<\xe01 \xd7I\x81\x02\x80\xb9\x910\xe0p\xe2\x10\x0f\x02\x01 \x05\x0c\x02\x01 \x06\t\x02\x01n\x07\x08\x00\x19\xad\xcev\xa2h@ \xeb\x90\xeb\x85\xff\xc0\x00\x19\xaf\x1d\xf6\xa2h@\x10\xeb\x90\xeb\x85\x8f\xc0\x02\x01H\n\x0b\x00\x17\xb3%\xfbQ4\x1cu\xc8u\xc2\xc7\xe0\x00\x11\xb2b\xfbQ45\xc2\x80 \x00\x19\xbe_\x0fj&\x84\x08\n\x0e\xb9\x0f\xa0,\x01\x02\xf2\x0e\x01\x1e \xd7\x0b\x1f\x82\x10sign\xba\xf2\xe0\x8a\x7f\x0f\x01\xe6\x8e\xf0\xed\xa2\xed\xfb!\x83\x08\xd7"\x02\x83\x08\xd7# \x80 \xd7!\xd3\x1f\xd3\x1f\xd3\x1f\xedD\xd0\xd2\x00\xd3\x1f \xd3\x1f\xd3\xff\xd7\n\x00\n\xf9\x01@\xcc\xf9\x10\x9a(\x94_\n\xdb1\xe1\xf2\xc0\x87\xdf\x02\xb3P\x07\xb0\xf2\xd0\x84Q%\xba\xf2\xe0\x85P6\xba\xf2\xe0\x86\xf8#\xbb\xf2\xd0\x88"\x92\xf8\x00\xde\x01\xa4\x7f\xc8\xca\x00\xcb\x1f\x01\xcf\x16\xc9\xedT \x92\xf8\x0f\xdep\xdb<\xd8\x10\x03\xf6\xed\xa2\xed\xfb\x02\xf4\x04!n\x92l!\x8eL\x02!\xd790p\x94!\xc7\x00\xb3\x8e-\x01\xd7( v\x1eCl \xd7I\xc0\x08\xf2\xe0\x93 \xd7J\xc0\x02\xf2\xe0\x93 \xd7\x1d\x06\xc7\x12\xc2\x00R0\xb0\xf2\xd0\x89\xd7L\xd790\x01\xa4\xe8l\x12\x84\x07\xbb\xf2\xe0\x93\xd7J\xc0\x00\xf2\xe0\x93\xedU\xe2\xd2\x00\x01\xc0\x00\x91[\xe0\xeb\xd7,\x08\x14 \x91p\x96\x01\xd7,\x08\x1c\x12\xe2R\x10\xb1\xe3\x0f \xd7J\x11\x12\x13\x00\x96\x01\xfa@\x01\xfaD\xf8(\xfaD0X\xba\xf2\xe0\x91\xedD\xd0\x81\x01A\xd7\x18\xf4\x05\x04\x9d\x7f\xc8\xca\x00@\x04\x83\x07\xf4S\xf2\xe0\x8b\x8e\x14\x03\x83\x07\xf4[\xf2\xe0\x8c"\xd7\n\x00!n\x01\xb3\xb0\xf2\xd0\x90\xe2\xc8P\x03\xcf\x16\x12\xf4\x00\xc9\xedT\x00r0\xd7,\x08$\x8e-!\xf2\xe0\x92\xd2\x00\xedD\xd0\xd2\x00Q\x13\xba\xf2\xd0\x8fTP0\x911\x9c\x01\x81\x01@\xd7!\xd7\n\x00\xf2\xe0\x8e\xe2\xc8\xca\x00X\xcf\x16\xc9\xedT\x93\xf2\xc0\x8d\xe2\x00\x10\x93[\xdb1\xe1\xd7L\xd0\xb4\xd6\xc3^')
15+
16+
17+
class WalletV5WalletID:
18+
"""
19+
wallet_id = network_global_id ^ context_id
20+
context_id_client$1 = wc:int8 version:uint8 counter:uint15
21+
context_id_backoffice$0 = counter:uint31
22+
"""
23+
def __init__(self,
24+
network_global_id: int,
25+
workchain: int = None,
26+
version: int = 0,
27+
subwallet_number: int = 0,
28+
context: int = None,
29+
) -> None:
30+
"""
31+
:param network_global_id: global id version taken from 19 config param. -239 for mainnet and -3 for testnet
32+
:param workchain: wallet's workchain (mostly -1 or 0)
33+
:param version: 8-bit uint, current v5r1 version is considered 0
34+
:param subwallet_number: any 15-bit uint, default is 0
35+
:param context: full custom wallet id, 31-bit uint
36+
"""
37+
self.network_global_id = network_global_id
38+
self.subwallet_number = subwallet_number
39+
self.workchain = workchain
40+
self.version = version
41+
self.context = context
42+
if self.context is not None and not (0 <= self.context <= 0x7FFFFFFF):
43+
raise ValueError("context must be a 31-bit unsigned integer")
44+
45+
def pack(self) -> int:
46+
if self.context is not None:
47+
return (self.context ^ self.network_global_id) & 0xFFFFFFFF
48+
ctx = 0
49+
ctx |= 1 << 31 # client context flag
50+
ctx |= (self.workchain & 0xFF) << 23
51+
ctx |= (self.version & 0xFF) << 15
52+
ctx |= self.subwallet_number & 0xFFFF
53+
return (ctx ^ self.network_global_id) & 0xFFFFFFFF
54+
55+
@classmethod
56+
def unpack(
57+
cls,
58+
value: int,
59+
network_global_id: int,
60+
) -> "WalletV5WalletID":
61+
ctx = (value ^ network_global_id) & 0xFFFFFFFF
62+
if not ctx & 0x80000000:
63+
return cls(context=ctx, network_global_id=network_global_id)
64+
subwallet_number = ctx & 0x7FFF
65+
version = (ctx >> 15) & 0xFF
66+
workchain = (ctx >> 23) & 0xFF
67+
if workchain & 0x80: # wc uint -> int
68+
workchain -= 0x100
69+
70+
return cls(
71+
subwallet_number=subwallet_number,
72+
workchain=workchain,
73+
version=version,
74+
network_global_id=network_global_id,
75+
)
76+
77+
def __repr__(self) -> str:
78+
return f"{self.__class__.__name__}<{self.pack()!r}>"
79+
80+
81+
82+
class WalletV5R1(BaseWallet):
83+
84+
@classmethod
85+
async def from_data(cls, provider: LiteClientLike, public_key: bytes, wc: int = 0,
86+
wallet_id: typing.Union[WalletV5WalletID, int] = None, network_global_id: typing.Optional[int] = None,
87+
subwallet_number: int = 0, is_signature_allowed: typing.Optional[bool] = True, **kwargs) -> "WalletV5R1":
88+
data = cls.create_data_cell(public_key, wc=wc, wallet_id=wallet_id, network_global_id=network_global_id, subwallet_number=subwallet_number, is_signature_allowed=is_signature_allowed)
89+
return await super().from_code_and_data(provider, wc, WALLET_V5_R1_CODE, data, **kwargs)
90+
91+
@staticmethod
92+
def create_data_cell(public_key: bytes, wc: typing.Optional[int] = 0,
93+
wallet_id: typing.Union[WalletV5WalletID, int] = None,
94+
network_global_id: typing.Optional[int] = None,
95+
subwallet_number: int = 0, is_signature_allowed: bool = True,
96+
extensions: typing.Optional[Cell] = None) -> Cell:
97+
if wallet_id is None and network_global_id is None:
98+
raise WalletError("provide either wallet_id or network_global_id param")
99+
if wallet_id is None:
100+
wallet_id = WalletV5WalletID(workchain=wc, subwallet_number=subwallet_number, network_global_id=network_global_id).pack()
101+
return WalletV5R1Data(seqno=0, wallet_id=wallet_id, public_key=public_key, extensions=extensions, is_signature_allowed=is_signature_allowed).serialize()
102+
103+
@classmethod
104+
async def from_private_key(cls, provider: LiteClientLike, private_key: bytes, wc: int = 0,
105+
wallet_id: typing.Union[WalletV5WalletID, int] = None,
106+
network_global_id: typing.Optional[int] = None,
107+
subwallet_number: int = 0, is_signature_allowed: bool = True):
108+
public_key = private_key_to_public_key(private_key)
109+
return await cls.from_data(provider=provider, public_key=public_key, network_global_id=network_global_id, wc=wc, wallet_id=wallet_id,
110+
private_key=private_key, subwallet_number=subwallet_number, is_signature_allowed=is_signature_allowed)
111+
112+
@classmethod
113+
async def from_mnemonic(cls, provider: LiteClientLike, mnemonics: typing.Union[list, str], wc: int = 0,
114+
wallet_id: typing.Union[WalletV5WalletID, int] = None,
115+
network_global_id: typing.Optional[int] = None,
116+
subwallet_number: int = 0, is_signature_allowed: bool = True):
117+
if isinstance(mnemonics, str):
118+
mnemonics = mnemonics.split()
119+
assert mnemonic_is_valid(mnemonics), 'mnemonics are invalid!'
120+
_, private_key = mnemonic_to_private_key(mnemonics)
121+
return await cls.from_private_key(provider, private_key=private_key, wc=wc, wallet_id=wallet_id,
122+
network_global_id=network_global_id, subwallet_number=subwallet_number,
123+
is_signature_allowed=is_signature_allowed)
124+
125+
@classmethod
126+
async def create(cls, provider: LiteClientLike, wc: int = 0, wallet_id: typing.Optional[int] = None,
127+
network_global_id: typing.Optional[int] = None,
128+
subwallet_number: int = 0, is_signature_allowed: bool = True):
129+
mnemo = mnemonic_new(24)
130+
return mnemo, await cls.from_mnemonic(provider, mnemonics=mnemo, wc=wc, wallet_id=wallet_id,
131+
network_global_id=network_global_id, subwallet_number=subwallet_number,
132+
is_signature_allowed=is_signature_allowed)
133+
134+
@classmethod
135+
def pack_actions(cls, messages: typing.List[WalletMessage]) -> Cell:
136+
actions_cell = Cell.empty()
137+
for msg in messages:
138+
action = Builder() \
139+
.store_uint(0x0ec3c86d, 32) \
140+
.store_uint(msg.send_mode, 8) \
141+
.store_ref(msg.message.serialize()) \
142+
.end_cell()
143+
actions_cell = Builder() \
144+
.store_ref(actions_cell) \
145+
.store_cell(action) \
146+
.end_cell()
147+
148+
return Builder() \
149+
.store_uint(1, 1) \
150+
.store_ref(actions_cell) \
151+
.store_uint(0, 1) \
152+
.end_cell()
153+
154+
def raw_create_transfer_msg(self, private_key: bytes, seqno: int, wallet_id: int, messages: typing.List[WalletMessage],
155+
valid_until: typing.Optional[int] = None) -> Cell:
156+
assert len(messages) <= 255, 'For wallet v5, maximum messages amount is 255'
157+
158+
op_code = 0x7369676e # signed external op code
159+
160+
signing_message = begin_cell().store_uint(op_code, 32)
161+
signing_message.store_uint(wallet_id, 32)
162+
if seqno == 0:
163+
signing_message.store_uint(2**32 - 1, 32)
164+
else:
165+
if valid_until is not None:
166+
signing_message.store_uint(valid_until, 32)
167+
else:
168+
signing_message.store_uint(int(time.time()) + 60, 32)
169+
signing_message.store_uint(seqno, 32)
170+
signing_message.store_cell(self.pack_actions(messages))
171+
signing_message = signing_message.end_cell()
172+
signature = sign_message(signing_message.hash, private_key)
173+
174+
return Builder() \
175+
.store_cell(signing_message) \
176+
.store_bytes(signature) \
177+
.end_cell()
178+
179+
async def transfer(self, destination: typing.Union[Address, str], amount: int, body: Cell = Cell.empty(),
180+
state_init: StateInit = None):
181+
if isinstance(destination, str):
182+
destination = Address(destination)
183+
wallet_message = self.create_wallet_internal_message(destination=destination, value=amount, body=body, state_init=state_init)
184+
return await self.raw_transfer(msgs=[wallet_message])
185+
186+
@property
187+
def seqno(self) -> int:
188+
"""
189+
:return: seqno taken from contract data
190+
"""
191+
return WalletV5R1Data.deserialize(
192+
self.state.data.begin_parse(),
193+
).seqno
194+
195+
@property
196+
def wallet_id(self) -> int:
197+
"""
198+
:return: wallet_id taken from contract data
199+
"""
200+
return WalletV5R1Data.deserialize(
201+
self.state.data.begin_parse(),
202+
).wallet_id
203+
204+
def unpacked_wallet_id(self, network_global_id: int) -> WalletV5WalletID:
205+
"""
206+
:param network_global_id: network global id taken from blockchain's config #19. -239 for mainnet, -3 for testnet
207+
:return: unpacked wallet_id taken from contract data
208+
"""
209+
return WalletV5WalletID.unpack(WalletV5R1Data.deserialize(
210+
self.state.data.begin_parse()
211+
).wallet_id, network_global_id)
212+
213+
@property
214+
def public_key(self) -> bytes:
215+
"""
216+
:return: public_key taken from contract data
217+
"""
218+
return WalletV5R1Data.deserialize(
219+
self.state.data.begin_parse(),
220+
).public_key
221+
222+
@property
223+
def extensions(self) -> Cell:
224+
"""
225+
:return: extensions list taken from contract data
226+
"""
227+
return WalletV5R1Data.deserialize(
228+
self.state.data.begin_parse(),
229+
).extensions
230+
231+
async def get_seqno(self):
232+
"""
233+
:return: seqno from wallet's get method
234+
"""
235+
return (await super().run_get_method("seqno"))[0]
236+
237+
async def get_subwallet_id(self):
238+
"""
239+
:return: subwallet_id from wallet's get method
240+
"""
241+
return (await super().run_get_method("get_subwallet_id"))[0]
242+
243+
async def get_unpacked_wallet_id(self, network_global_id: int):
244+
"""
245+
:return: unpacked subwallet_id from wallet's get method
246+
"""
247+
wallet_id = (await super().run_get_method("get_subwallet_id"))[0]
248+
return WalletV5WalletID.unpack(wallet_id, network_global_id)
249+
250+
251+
async def get_extensions(self):
252+
"""
253+
:return: extensions list from wallet's get method
254+
"""
255+
return (await super().run_get_method("get_extensions"))[0]
256+
257+
async def is_signature_allowed(self) -> bool:
258+
"""
259+
:return: is signature allowed from wallet's get method
260+
"""
261+
return (await super().run_get_method("is_signature_allowed"))[0]

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="pytoniq",
8-
version="0.1.41",
8+
version="0.1.42",
99
author="Maksim Kurbatov",
1010
author_email="cyrbatoff@gmail.com",
1111
description="TON Blockchain SDK",
@@ -22,7 +22,7 @@
2222
python_requires='>=3.9',
2323
py_modules=["pytoniq"],
2424
install_requires=[
25-
"pytoniq-core>=0.1.42",
25+
"pytoniq-core>=0.1.45",
2626
"requests>=2.31.0",
2727
"setuptools>=65.5.1",
2828
],

tests/test_adnl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async def test_connection():
1414
await adnl.start()
1515

1616
# take peer from public config
17-
peer = Node('172.104.59.125', 14432, "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=", adnl)
17+
peer = Node('144.76.36.181', 21533, "LFnKVKTO+GYsOBrTH2xaVAGsOGEgSNGo0TRdDZmBeL4=", adnl)
1818
await adnl.connect_to_peer(peer)
1919

2020
# ask peer for something

tests/test_balancer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ async def test_account_state(client: LiteBalancer):
3737
await client.close_all()
3838

3939

40-
@pytest.mark.asyncio
41-
async def test_archival(client: LiteBalancer):
42-
blk, _ = await client.lookup_block(-1, -2 ** 63, 10, only_archive=True)
43-
assert blk.root_hash.hex() == 'c1b8e9cb4c3d886d91764d243693119f4972d284ce7be01e739b67fdcbb84ca1'
40+
# @pytest.mark.asyncio
41+
# async def test_archival(client: LiteBalancer):
42+
# blk, _ = await client.lookup_block(-1, -2 ** 63, 10, only_archive=True)
43+
# assert blk.root_hash.hex() == 'c1b8e9cb4c3d886d91764d243693119f4972d284ce7be01e739b67fdcbb84ca1'
4444

4545

4646
@pytest.mark.asyncio

tests/test_liteclient.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
@pytest_asyncio.fixture
1313
async def client():
1414
while True:
15-
client = LiteClient.from_mainnet_config(random.randint(0, 15), trust_level=1)
15+
client = LiteClient.from_testnet_config(random.randint(0, 7), trust_level=1)
1616
try:
1717
await client.connect()
1818
yield client
@@ -24,12 +24,12 @@ async def client():
2424

2525
@pytest.mark.asyncio
2626
async def test_init():
27-
client = LiteClient.from_mainnet_config(ls_i=0, trust_level=2)
28-
await client.connect()
29-
await client.reconnect()
30-
await client.close()
27+
# client = LiteClient.from_mainnet_config(ls_i=0, trust_level=2)
28+
# await client.connect()
29+
# await client.reconnect()
30+
# await client.close()
3131

32-
client = LiteClient.from_testnet_config(ls_i=12, trust_level=2)
32+
client = LiteClient.from_testnet_config(ls_i=random.randint(0, 7), trust_level=2)
3333
await client.connect()
3434
await client.reconnect()
3535
await client.close()

0 commit comments

Comments
 (0)