|
| 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] |
0 commit comments