diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4315edd..69d1da5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -71,7 +71,7 @@ jobs: platform: - runner: macos-latest target: x86_64 - - runner: macos-13 + - runner: macos-14 target: aarch64 python-version: ["3.10", "3.11", "3.12", "3.13"] steps: diff --git a/docs/src/pages/guides/keys-and-signatures.md b/docs/src/pages/guides/keys-and-signatures.md index e121b14..852febf 100644 --- a/docs/src/pages/guides/keys-and-signatures.md +++ b/docs/src/pages/guides/keys-and-signatures.md @@ -221,7 +221,7 @@ Signing data is a fundamental cryptographic operation that provides both authent In this section, we'll explore two primary ways of signing data: with hashing (using the `sign()` method) and without hashing (using the `sign_raw()` method). -Additionally, we will touch upon the optional incorporation of a `signature_id` to further enhance the identification of the signed data. +Additionally, we will touch upon the optional incorporation of a `signature_context` to further enhance the identification of the signed data. ### Data with Hashing @@ -230,9 +230,9 @@ When signing data using the `sign()` method, the data is first hashed before bei ```python data = b"Hello, World 42!" -signature_id = await transport.get_signature_id() # Optional +signature_context = await transport.get_signature_context() # Optional -signature = keypair.sign(data, signature_id) +signature = keypair.sign(data, signature_context) print(signature) ``` @@ -244,7 +244,7 @@ Signature('b2bd3045b3ec3872bcccc96f58b71fe0fd60cba104249cc5e72c1a2ebad35cbbf1d82 ``` :::tip -The `signature_id` is an optional identifier for a signature, sourced directly from the transport layer using the `get_signature_id()` method. If this is your first time encountering `signature_id` or you're unfamiliar with our transport layer, it's recommended to [read our guide on working with the transport](./working-with-transport.md) to get started. +The `signature_context` is an optional context for a signature, sourced directly from the transport layer using the `get_signature_context()` method. If this is your first time encountering `signature_context` or you're unfamiliar with our transport layer, it's recommended to [read our guide on working with the transport](./working-with-transport.md) to get started. ::: ### Raw Data @@ -271,7 +271,7 @@ To verify a signature, you need to use the correct input depending on the signin If it was signed with `sign_raw()`, you should either use the hash of the original data (if you want to verify the signature against hashed data) or the original data itself (if you want to verify the signature against raw data). -When a `signature_id` was used during signing, the same `signature_id` should be used for verification. +When a `signature_context` was used during signing, the same `signature_context` should be used for verification. ### Hashed Signature @@ -282,7 +282,7 @@ import hashlib data = hashlib.sha256(b"Hello, World 42!").digest() -is_valid = public_key.check_signature(data, signature, signature_id) +is_valid = public_key.check_signature(data, signature, signature_context) print(is_valid) # True ``` diff --git a/docs/src/pages/guides/working-with-messages.md b/docs/src/pages/guides/working-with-messages.md index 53c0495..e0de31e 100644 --- a/docs/src/pages/guides/working-with-messages.md +++ b/docs/src/pages/guides/working-with-messages.md @@ -306,11 +306,11 @@ print(expire_at) # 1695429486 ### Signing an Unsigned External Message -The `sign` method allows you to sign an unsigned external message. This requires a `KeyPair` and an optional `signature_id`. +The `sign` method allows you to sign an unsigned external message. This requires a `KeyPair` and an optional `signature_context`. ```python # Sign the unsigned message -signed_message = unsigned_message.sign(keypair, signature_id) +signed_message = unsigned_message.sign(keypair, signature_context) print(signed_message) ``` @@ -383,11 +383,11 @@ print(without_signature_message) ### Signing the External Message Body -The `sign` method of the `UnsignedBody` class allows you to sign the body of an external message. This requires a `KeyPair` and an optional `signature_id`. +The `sign` method of the `UnsignedBody` class allows you to sign the body of an external message. This requires a `KeyPair` and an optional `signature_context`. ```python # Sign the body of the unsigned message -signed_body = unsigned_body.sign(keypair, signature_id) +signed_body = unsigned_body.sign(keypair, signature_context) print(signed_body) ``` diff --git a/docs/src/pages/guides/working-with-transport.md b/docs/src/pages/guides/working-with-transport.md index 51ee8e9..97af722 100644 --- a/docs/src/pages/guides/working-with-transport.md +++ b/docs/src/pages/guides/working-with-transport.md @@ -130,14 +130,14 @@ print(raw_param) ``` -## Fetching Signature ID +## Fetching Signature Context -You can fetch the signature ID for the selected network using the `get_signature_id` method. This method does not take any parameters. +You can fetch the signature context for the selected network using the `get_signature_context` method. This method does not take any parameters. ```python -signature_id = await transport.get_signature_id() +signature_context = await transport.get_signature_context() -print(signature_id) +print(signature_context) ``` ##### Result diff --git a/python/nekoton/contracts/__init__.py b/python/nekoton/contracts/__init__.py index b97e54d..71d8923 100644 --- a/python/nekoton/contracts/__init__.py +++ b/python/nekoton/contracts/__init__.py @@ -4,3 +4,4 @@ from .giver_v2 import GiverV2 as GiverV2 from .highload_wallet_v2 import HighloadWalletV2 as HighloadWalletV2 from .walletv3 import WalletV3 as WalletV3 +from .walletv5 import WalletV5 as WalletV5 diff --git a/python/nekoton/contracts/ever_wallet.py b/python/nekoton/contracts/ever_wallet.py index 0d0c0bd..bfd9409 100644 --- a/python/nekoton/contracts/ever_wallet.py +++ b/python/nekoton/contracts/ever_wallet.py @@ -144,8 +144,7 @@ async def send( bounce: bool = False, ) -> _nt.Transaction: state_init = await self.__get_state_init() - - signature_id = await self._transport.get_signature_id() + context = await self._transport.get_signature_context() external_message = _send_transaction.encode_external_message( self._address, @@ -158,7 +157,7 @@ async def send( }, public_key=self._keypair.public_key, state_init=state_init, - ).sign(self._keypair, signature_id) + ).sign(self._keypair, context) tx = await self._transport.send_external_message(external_message) if tx is None: @@ -173,7 +172,7 @@ async def send_raw( raise RuntimeError("Too many messages at once") state_init = await self.__get_state_init() - signature_id = await self._transport.get_signature_id() + context = await self._transport.get_signature_context() abi = _send_transaction_raw[len(messages)] input = dict() @@ -186,7 +185,7 @@ async def send_raw( input, public_key=self._keypair.public_key, state_init=state_init, - ).sign(self._keypair, signature_id) + ).sign(self._keypair, context) tx = await self._transport.send_external_message(external_message) if tx is None: diff --git a/python/nekoton/contracts/giver_v2.py b/python/nekoton/contracts/giver_v2.py index 9ae9617..8ccdbc8 100644 --- a/python/nekoton/contracts/giver_v2.py +++ b/python/nekoton/contracts/giver_v2.py @@ -95,13 +95,13 @@ async def deploy( raise RuntimeError("Message expired") await transport.trace_transaction(tx).wait() - signature_id = await transport.get_signature_id() + context = await transport.get_signature_context() external_message = _giver_v2_constructor.encode_external_message( address, input={}, public_key=keypair.public_key, state_init=state_init, - ).sign(keypair, signature_id) + ).sign(keypair, context) tx = await transport.send_external_message(external_message) if tx is None: raise RuntimeError("Message expired") @@ -121,7 +121,7 @@ def address(self) -> _nt.Address: return self._address async def give(self, target: _nt.Address, amount: _nt.Tokens): - signature_id = await self._transport.get_signature_id() + context = await self._transport.get_signature_context() # Prepare external message message = _giver_v2_send_grams.encode_external_message( @@ -132,7 +132,7 @@ async def give(self, target: _nt.Address, amount: _nt.Tokens): "bounce": False, }, public_key=self._keypair.public_key, - ).sign(self._keypair, signature_id) + ).sign(self._keypair, context) # Send external message tx = await self._transport.send_external_message(message) diff --git a/python/nekoton/contracts/highload_wallet_v2.py b/python/nekoton/contracts/highload_wallet_v2.py index 624d9d9..564caa5 100644 --- a/python/nekoton/contracts/highload_wallet_v2.py +++ b/python/nekoton/contracts/highload_wallet_v2.py @@ -122,7 +122,7 @@ async def send_raw( raise RuntimeError("Too many messages at once") state_init = await self.__get_state_init() - signature_id = await self._transport.get_signature_id() + context = await self._transport.get_signature_context() expire_at = self._transport.clock.now_sec + _default_ttl if ttl is None else ttl @@ -141,7 +141,7 @@ async def send_raw( payload_builder.store_slice(messages_dict_cell.as_slice()) hash_to_sign = payload_builder.build().repr_hash - signature = self._keypair.sign_raw(hash_to_sign, signature_id) + signature = self._keypair.sign_raw(hash_to_sign, context) body_builder = _nt.CellBuilder() body_builder.store_signature(signature) diff --git a/python/nekoton/contracts/walletv3.py b/python/nekoton/contracts/walletv3.py index 2f511f9..9acf65a 100644 --- a/python/nekoton/contracts/walletv3.py +++ b/python/nekoton/contracts/walletv3.py @@ -108,7 +108,7 @@ async def send_raw( raise RuntimeError("Too many messages at once") seqno, state_init = await self.__get_seqno_and_state_init() - signature_id = await self._transport.get_signature_id() + context = await self._transport.get_signature_context() expire_at = self._transport.clock.now_sec + _default_ttl if ttl is None else ttl @@ -122,7 +122,7 @@ async def send_raw( payload_builder.store_reference(message.build_cell()) hash_to_sign = payload_builder.build().repr_hash - signature = self._keypair.sign_raw(hash_to_sign, signature_id) + signature = self._keypair.sign_raw(hash_to_sign, context) body_builder = _nt.CellBuilder() body_builder.store_signature(signature) diff --git a/python/nekoton/contracts/walletv5.py b/python/nekoton/contracts/walletv5.py new file mode 100644 index 0000000..6495c5d --- /dev/null +++ b/python/nekoton/contracts/walletv5.py @@ -0,0 +1,193 @@ +from typing import List, Optional + +import nekoton.nekoton as _nt + +from .base import IGiver + +_wallet_code = _nt.Cell.decode( + "b5ee9c7241021401000281000114ff00f4a413f4bcf2c80b01020120020d020148030402dcd020d749c120915b8f6320d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e011e20d70b1f82107369676ebaf2e08a7f0f01e68ef0eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109a28945f0adb31e1f2c087df02b35007b0f2d0845125baf2e0855036baf2e086f823bbf2d0882292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde70db3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0b4d6c35e", + "hex", +) +_default_wallet_id = 0x7FFFFF11 +_default_ttl = 60 + +_op_signed_external = 0x7369676E +_op_action_send_msg = 0x0EC3C86D + + +class WalletV5(IGiver): + @classmethod + def compute_address( + cls, + public_key: _nt.PublicKey, + workchain: int = 0, + wallet_id: int = _default_wallet_id, + ) -> _nt.Address: + return cls.compute_state_init(public_key, wallet_id).compute_address(workchain) + + @staticmethod + def compute_state_init( + public_key: _nt.PublicKey, wallet_id: int = _default_wallet_id + ) -> _nt.StateInit: + builder = _nt.CellBuilder() + builder.store_bit_one() + builder.store_u32(0) + builder.store_u32(wallet_id) + builder.store_public_key(public_key) + builder.store_bit_zero() + return _nt.StateInit(_wallet_code, builder.build()) + + @staticmethod + def from_address( + transport: _nt.Transport, keypair: _nt.KeyPair, address: _nt.Address + ) -> "WalletV5": + wallet = WalletV5(transport, keypair) + wallet._address = address + return wallet + + def __init__( + self, + transport: _nt.Transport, + keypair: _nt.KeyPair, + workchain: int = 0, + wallet_id: int = _default_wallet_id, + ): + state_init = self.compute_state_init(keypair.public_key, wallet_id) + + self._wallet_id = wallet_id + self._transport = transport + self._keypair = keypair + self._state_init = state_init + self._address = state_init.compute_address(workchain) + + @property + def address(self) -> _nt.Address: + return self._address + + @property + def wallet_id(self) -> int: + return self._wallet_id + + async def give(self, target: _nt.Address, amount: _nt.Tokens): + internal_message = _nt.Message( + header=_nt.InternalMessageHeader( + value=amount, + dst=target, + bounce=False, + ), + ) + + tx = await self.send_raw([(internal_message, 3)]) + + await self._transport.trace_transaction(tx).wait() + + async def send( + self, + dst: _nt.Address, + value: _nt.Tokens, + payload: Optional[_nt.Cell] = None, + bounce: bool = False, + state_init: Optional[_nt.StateInit] = None, + ttl: Optional[int] = None, + ) -> _nt.Transaction: + internal_message = _nt.Message( + header=_nt.InternalMessageHeader( + value=value, + dst=dst, + bounce=bounce, + ), + body=payload, + state_init=state_init, + ) + + tx = await self.send_raw([(internal_message, 3)], ttl) + return tx + + async def send_raw( + self, + messages: List[tuple[_nt.Message, int]], + ttl: Optional[int] = None, + ) -> _nt.Transaction: + if len(messages) > 255: + raise RuntimeError("Too many messages at once") + + seqno, state_init = await self.__get_seqno_and_state_init() + context = await self._transport.get_signature_context() + + expire_at = self._transport.clock.now_sec + _default_ttl if ttl is None else ttl + + payload_builder = _nt.CellBuilder() + payload_builder.store_u32(_op_signed_external) + payload_builder.store_u32(self._wallet_id) + payload_builder.store_u32(expire_at) + payload_builder.store_u32(seqno) + self.__store_inner_request(payload_builder, messages) + + hash_to_sign = payload_builder.build().repr_hash + signature = self._keypair.sign_raw(hash_to_sign, context) + + body_builder = _nt.CellBuilder() + body_builder.store_builder(payload_builder) + body_builder.store_signature(signature) + + body = body_builder.build() + + external_message = _nt.SignedExternalMessage( + dst=self._address, expire_at=expire_at, body=body, state_init=state_init + ) + + tx = await self._transport.send_external_message(external_message) + if tx is None: + raise RuntimeError("Message expired") + return tx + + async def get_account_state(self) -> Optional[_nt.AccountState]: + return await self._transport.get_account_state(self._address) + + async def get_balance(self) -> _nt.Tokens: + state = await self.get_account_state() + if state is None: + return _nt.Tokens(0) + else: + return state.balance + + @staticmethod + def __store_inner_request( + builder: _nt.CellBuilder, messages: List[tuple[_nt.Message, int]] + ) -> None: + if len(messages) > 0: + builder.store_bit_one() + builder.store_reference(WalletV5.__build_out_list(messages)) + else: + builder.store_bit_zero() + + builder.store_bit_zero() + + @staticmethod + def __build_out_list(messages: List[tuple[_nt.Message, int]]) -> _nt.Cell: + out_list = _nt.Cell() + for message, flags in messages: + builder = _nt.CellBuilder() + builder.store_u32(_op_action_send_msg) + builder.store_u8(flags) + builder.store_reference(out_list) + builder.store_reference(message.build_cell()) + out_list = builder.build() + return out_list + + async def __get_seqno_and_state_init(self) -> tuple[int, Optional[_nt.StateInit]]: + account_state = await self.get_account_state() + if account_state is not None and account_state.state_init is not None: + if account_state.state_init.data is None: + raise RuntimeError("Account state does not contain data") + + data = account_state.state_init.data.as_slice() + data.load_bit() + seqno = data.load_u32() + + # NOTE: Update wallet_id just in case + self._wallet_id = data.load_u32() + + return seqno, None + else: + return 0, self._state_init diff --git a/python/nekoton/generator/__init__.py b/python/nekoton/generator/__init__.py index 2a6d40e..30c1f16 100644 --- a/python/nekoton/generator/__init__.py +++ b/python/nekoton/generator/__init__.py @@ -2,7 +2,7 @@ import re from typing import Dict, List, Optional -from nekoton import ContractAbi +from nekoton.nekoton import ContractAbi def generate(name: str, abi: str) -> str: diff --git a/python/nekoton/gql/__init__.py b/python/nekoton/gql/__init__.py index 5bd4a15..20238c0 100644 --- a/python/nekoton/gql/__init__.py +++ b/python/nekoton/gql/__init__.py @@ -2,7 +2,10 @@ from nekoton.nekoton import GqlExprPart -from . import acc, filters, msg, tx +from . import acc as acc +from . import filters as filters +from . import msg as msg +from . import tx as tx def and_(expressions: str | GqlExprPart | _List[GqlExprPart]) -> GqlExprPart: