From 637e638c3d4a691172491b0f952c669c2e37e543 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 21 May 2026 20:04:43 +0200 Subject: [PATCH 1/4] fix(python): use public exports --- python/nekoton/generator/__init__.py | 2 +- python/nekoton/gql/__init__.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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: From 704eec47d113a7c34ba90fbe6cc669791d19f8f5 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 21 May 2026 20:26:06 +0200 Subject: [PATCH 2/4] fix(python): use signature context when signing --- docs/src/pages/guides/keys-and-signatures.md | 12 ++++++------ docs/src/pages/guides/working-with-messages.md | 8 ++++---- docs/src/pages/guides/working-with-transport.md | 8 ++++---- python/nekoton/contracts/ever_wallet.py | 9 ++++----- python/nekoton/contracts/giver_v2.py | 8 ++++---- python/nekoton/contracts/highload_wallet_v2.py | 4 ++-- python/nekoton/contracts/walletv3.py | 4 ++-- 7 files changed, 26 insertions(+), 27 deletions(-) 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/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) From 0f6949ed6b4a2ad872fa20de79d1708dd5983ce1 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 21 May 2026 20:34:51 +0200 Subject: [PATCH 3/4] feat(python): add `WalletV5` --- python/nekoton/contracts/__init__.py | 1 + python/nekoton/contracts/walletv5.py | 193 +++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 python/nekoton/contracts/walletv5.py 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/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 From e5e896340f40fc32bcbf8c25610c6d00dfb8bdf5 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 21 May 2026 20:58:18 +0200 Subject: [PATCH 4/4] fix ci --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: