-
Notifications
You must be signed in to change notification settings - Fork 56
highload wallet v3 implementation #32
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
base: master
Are you sure you want to change the base?
Changes from all commits
d91dd60
ae9fa18
2395912
f11d3e1
9348c15
37b2275
9aff8c5
030544d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 |
| 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: | ||
| 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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. may be |
||
| :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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion, this is good place to set default values. |
||
| 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] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whats wrong with empty lines at the end of files ? |
||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
query_id: HighloadQueryId = 0looks like the default value is invalid