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
76 changes: 75 additions & 1 deletion pytoniq/contract/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,81 @@
import time


def generate_query_id(offset: int = 7200):
return int(time.time() + offset) << 32

BIT_NUMBER_SIZE = 10 # 10 bit
SHIFT_SIZE = 13 # 13 bit
MAX_BIT_NUMBER = 1022
MAX_SHIFT = 8191 # 2^13 = 8192

class HighloadQueryId:
def __init__(self) -> None:
self._shift = 0
self._bit_number = 0

@staticmethod
def from_shift_and_bit_number(
shift: int, bit_number: int
) -> "HighloadQueryId":

if not (0 <= shift <= MAX_SHIFT):
raise ValueError("invalid shift")
if not (0 <= bit_number <= MAX_BIT_NUMBER):
raise ValueError("invalid bitnumber")

q = HighloadQueryId()
q._shift = shift
q._bit_number = bit_number
return q

def get_next(self) -> "HighloadQueryId":
new_bit_number = self._bit_number + 1
new_shift = self._shift

if new_shift == MAX_SHIFT and new_bit_number > (MAX_BIT_NUMBER - 1):
# we left one queryId for emergency withdraw
raise ValueError("Overload")

if new_bit_number > MAX_BIT_NUMBER:
new_bit_number = 0
new_shift += 1
if new_shift > MAX_SHIFT:
raise ValueError("Overload")

return HighloadQueryId.from_shift_and_bit_number(
new_shift, new_bit_number
)

def has_next(self) -> bool:
is_end = (
self._bit_number >= (MAX_BIT_NUMBER - 1)
and self._shift == MAX_SHIFT
)
return not is_end

@property
def shift(self) -> int:
return self._shift

@property
def bit_number(self) -> int:
return self._bit_number

@property
def query_id(self) -> int:
return (self._shift << BIT_NUMBER_SIZE) + self._bit_number

@staticmethod
def from_query_id(query_id: int) -> "HighloadQueryId":
shift = query_id >> BIT_NUMBER_SIZE
bit_number = query_id & 1023
return HighloadQueryId.from_shift_and_bit_number(shift, bit_number)

@staticmethod
def from_seqno(i: int) -> "HighloadQueryId":
shift = i // 1023
bit_number = i % 1023
return HighloadQueryId.from_shift_and_bit_number(shift, bit_number)

def to_seqno(self) -> int:
return self._bit_number + self._shift * 1023
1 change: 1 addition & 0 deletions pytoniq/contract/wallets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .wallet import WalletError, Wallet, BaseWallet, WalletV3, WalletV4, WalletV3R1, WalletV3R2, WalletV4R2, WalletV3Data, WalletV4Data, WalletMessage
from .highload import HighloadWallet
from .highload_v3 import HighloadWalletV3
143 changes: 143 additions & 0 deletions pytoniq/contract/wallets/highload_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import typing
from .wallet import Wallet, WalletError
from ..utils import HighloadQueryId
from ..contract import ContractError
from ...liteclient import LiteClientLike
from pytoniq_core.crypto.keys import private_key_to_public_key, mnemonic_to_private_key, mnemonic_is_valid, mnemonic_new
from pytoniq_core.crypto.signature import sign_message
from pytoniq_core.boc import Cell, Builder
from pytoniq_core.boc.address import Address
from pytoniq_core.tlb.account import StateInit
from pytoniq_core.tlb.custom.wallet import WalletMessage, HighloadWalletV3Data

HIGHLOAD_WALLET_V3_CODE = Cell.one_from_boc('b5ee9c7241021001000228000114ff00f4a413f4bcf2c80b01020120020d02014803040078d020d74bc00101c060b0915be101d0d3030171b0915be0fa4030f828c705b39130e0d31f018210ae42e5a4ba9d8040d721d74cf82a01ed55fb04e030020120050a02027306070011adce76a2686b85ffc00201200809001aabb6ed44d0810122d721d70b3f0018aa3bed44d08307d721d70b1f0201200b0c001bb9a6eed44d0810162d721d70b15800e5b8bf2eda2edfb21ab09028409b0ed44d0810120d721f404f404d33fd315d1058e1bf82325a15210b99f326df82305aa0015a112b992306dde923033e2923033e25230800ef40f6fa19ed021d721d70a00955f037fdb31e09130e259800ef40f6fa19cd001d721d70a00937fdb31e0915be270801f6f2d48308d718d121f900ed44d0d3ffd31ff404f404d33fd315d1f82321a15220b98e12336df82324aa00a112b9926d32de58f82301de541675f910f2a106d0d31fd4d307d30dd309d33fd315d15168baf2a2515abaf2a6f8232aa15250bcf2a304f823bbf2a35304800ef40f6fa199d024d721d70a00f2649130e20e01fe5309800ef40f6fa18e13d05004d718d20001f264c858cf16cf8301cf168e1030c824cf40cf8384095005a1a514cf40e2f800c94039800ef41704c8cbff13cb1ff40012f40012cb3f12cb15c9ed54f80f21d0d30001f265d3020171b0925f03e0fa4001d70b01c000f2a5fa4031fa0031f401fa0031fa00318060d721d300010f0020f265d2000193d431d19130e272b1fb00f984f3eb')

class HighloadWalletV3(Wallet):

@classmethod
async def from_data(cls, provider: LiteClientLike, public_key: bytes, timeout: typing.Optional[int] = None, wc: int = 0,
wallet_id: typing.Optional[int] = None, **kwargs) -> "HighloadWalletV3":
data = cls.create_data_cell(public_key, wallet_id, wc, timeout)
return await super().from_code_and_data(provider, wc, HIGHLOAD_WALLET_V3_CODE, data, **kwargs)

@staticmethod
def create_data_cell(public_key: bytes, wallet_id: typing.Optional[int] = None, wc: typing.Optional[int] = 0,
old_queries: typing.Optional[dict] = None, queries: typing.Optional[dict] = None, timeout: typing.Optional[int] = 128) -> Cell:
if wallet_id is None:
wallet_id = 698983191 + wc
return HighloadWalletV3Data(public_key=public_key, wallet_id=wallet_id, old_queries=old_queries, queries=queries, last_cleaned=0, timeout=timeout).serialize()

@classmethod
async def from_private_key(cls, provider: LiteClientLike, private_key: bytes, wc: int = 0, wallet_id: typing.Optional[int] = None, timeout: typing.Optional[int] = None):
public_key = private_key_to_public_key(private_key)
return await cls.from_data(provider=provider, public_key=public_key, timeout=timeout, wc=wc, wallet_id=wallet_id, private_key=private_key)

@classmethod
async def from_mnemonic(cls, provider: LiteClientLike, mnemonics: typing.Union[list, str], wc: int = 0, wallet_id: typing.Optional[int] = None, timeout: typing.Optional[int] = None):
if isinstance(mnemonics, str):
mnemonics = mnemonics.split()
assert mnemonic_is_valid(mnemonics), 'mnemonics are invalid!'
_, private_key = mnemonic_to_private_key(mnemonics)
return await cls.from_private_key(provider=provider, private_key=private_key, wc=wc, wallet_id=wallet_id, timeout=timeout)

@classmethod
async def create(cls, provider: LiteClientLike, wc: int = 0, wallet_id: typing.Optional[int] = None, timeout: typing.Optional[int] = None):
"""
:param provider: provider
:param wc: wallet workchain
:param wallet_id: subwallet_id
:param timeout: timeout
:return: mnemonics and Wallet instance of provided version
"""
mnemo = mnemonic_new(24)
return mnemo, await cls.from_mnemonic(provider=provider, mnemonics=mnemo, wc=wc, wallet_id=wallet_id, timeout=timeout)

@staticmethod
def raw_create_transfer_msg(private_key: bytes, wallet_id: int, sendmode: int, created_at: int, timeout: int, message_to_send: WalletMessage, query_id: HighloadQueryId = 0) -> Cell:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

query_id: HighloadQueryId = 0
looks like the default value is invalid

signing_message = Builder() \
.store_uint(wallet_id, 32) \
.store_ref(message_to_send) \
.store_uint(sendmode, 8) \
.store_uint(query_id.shift, 13) \
.store_uint(query_id.bit_number, 10) \
.store_uint(created_at, 64) \
.store_uint(timeout, 22) \
.end_cell()
signature = sign_message(signing_message.hash, private_key)
return Builder() \
.store_bytes(signature) \
.store_cell(signing_message) \
.end_cell()

async def raw_transfer(self, sendmode: int, created_at: int, timeout: int, msg: WalletMessage, query_id: HighloadQueryId = 0):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to be able to send only one message per transaction?

"""
:param sendmode: sendmode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be send_mode instead ?

:param created_at: created at
:param timeout: timeout
:param msg: WalletMessage. to create one call create_wallet_internal_message method
:param query_id: query id
"""
if 'private_key' not in self.__dict__:
raise WalletError('must specify wallet private key!')

transfer_msg = self.raw_create_transfer_msg(private_key=self.private_key, wallet_id=self.wallet_id,
sendmode=sendmode, created_at=created_at, timeout=timeout,
message_to_send=msg, query_id=query_id)

return await self.send_external(body=transfer_msg)

async def transfer(self, destination: typing.Union[Address, str], amount: int, sendmode: int, created_at: int, timeout: int, body: Cell = Cell.empty(), state_init: StateInit = None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

body: Cell = Cell.empty()
Do not create objects inside function signature except the cases you exactly understand what are you doing.

In [1]: from pytoniq_core import Cell
   ...: 
   ...: 
   ...: def f(b: Cell = Cell.empty()):
   ...:     b.bits.append(1)
   ...:     print(b.bits)
   ...: 
   ...: f()
   ...: f()
   ...: 
bitarray('1')
bitarray('11')

if isinstance(destination, str):
destination = Address(destination)

result_msg = self.create_wallet_internal_message(destination=destination, value=amount, body=body, state_init=state_init)
return await self.raw_transfer(sendmode=sendmode, created_at=created_at, timeout=timeout, msg=result_msg)

async def send_init_external(self, sendmode: int, created_at: int, timeout: int, message_to_send: WalletMessage):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this is good place to set default values.
For example, I don't think many users will really understand meaning of timeout var and how it works. Let's advice some value.

if not self.state_init:
raise ContractError('contract does not have state_init attribute')
if 'private_key' not in self.__dict__:
raise WalletError('must specify wallet private key!')
body = self.raw_create_transfer_msg(private_key=self.private_key, wallet_id=self.wallet_id, sendmode=sendmode, created_at=created_at, timeout=timeout, message_to_send=message_to_send)
return await self.send_external(state_init=self.state_init, body=body)

@property
def wallet_id(self) -> int:
"""
:return: wallet_id taken from contract data
"""
return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).wallet_id

@property
def last_cleaned(self) -> int:
"""
:return: last_cleaned taken from contract data
"""
return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).last_cleaned

@property
def timeout(self) -> int:
"""
:return: timeout taken from contract data
"""
return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).timeout

@property
def public_key(self) -> bytes:
"""
:return: public_key taken from contract data
"""
return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).public_key

@property
def old_queries(self) -> dict:
"""
:return: old_queries taken from contract data
"""
return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).old_queries

async def processed(self, query_id: int) -> bool:
"""
:return: is query processed from wallet's get method
"""
return (await super().run_get_method(method='processed?', stack=[query_id]))[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats wrong with empty lines at the end of files ?

2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
requests>=2.31.0
setuptools>=65.5.1
pytoniq-core>=0.1.32
pytoniq-core @ git+https://github.com/r-pine/pytoniq-core.git
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
python_requires='>=3.9',
py_modules=["pytoniq"],
install_requires=[
"pytoniq-core>=0.1.35",
"pytoniq-core @ git+https://github.com/r-pine/pytoniq-core.git",
"requests>=2.31.0",
"setuptools>=65.5.1",
],
Expand Down