Skip to content

Commit

Permalink
Refactor code and fix minor TonConnect bugs.
Browse files Browse the repository at this point in the history
  • Loading branch information
nessshon committed Jan 22, 2025
1 parent d6b37bf commit 04611a0
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 493 deletions.
95 changes: 31 additions & 64 deletions tonutils/tonconnect/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
import time
from typing import List, Tuple, Union, cast, Callable, Awaitable

from pytoniq_core import Address, Cell, StateInit, begin_cell
from pytoniq_core import Address, Cell, StateInit

from .models import (
Account,
DeviceInfo,
Event,
EventError,
Message,
SendConnectRequest,
SendDisconnectRequest,
SendTransactionRequest,
Expand All @@ -28,7 +27,6 @@
from .storage import IStorage
from .utils.exceptions import *
from .utils.logger import logger
from ..utils import boc_to_base64_string, to_nano
from ..wallet.data import TransferData


Expand Down Expand Up @@ -405,12 +403,6 @@ async def _process_transaction(
:param connection: Stored connection data from IStorage.
:param rpc_request_id: The unique RPC request ID for this transaction.
"""

async def handler_failure() -> None:
self._clean_pending_request(rpc_request_id)
self.bridge.pending_requests[rpc_request_id] = self._create_future()
await self._on_rpc_response_received(response, rpc_request_id)

try:
self._prepare_transaction(transaction)
request = SendTransactionRequest(params=[transaction])
Expand All @@ -425,31 +417,32 @@ async def handler_failure() -> None:
)
# Wait for the response or a timeout.
await asyncio.wait_for(
self.bridge.send_request(request, rpc_request_id), # type: ignore
asyncio.shield(self.bridge.send_request(request, rpc_request_id)), # Защищаем future
timeout=timeout,
)

except asyncio.TimeoutError:
response = {"error": {"code": 500, "message": "Failed to send transaction: timeout."}}
logger.debug(f"Transaction timeout for user_id={self.user_id} with request ID={rpc_request_id}")
await handler_failure()
await self._on_rpc_response_received(response, rpc_request_id)

except Exception as e:
response = {"error": {"code": 0, "message": f"Failed to send transaction: {e}"}}
logger.exception(
"Unexpected error during transaction for user_id=%d with request ID=%d: %s",
self.user_id, rpc_request_id, e
)
await handler_failure()
await self._on_rpc_response_received(response, rpc_request_id)

async def _send_transaction(self, messages: List[Message]) -> int:
async def send_transaction(self, transaction: Transaction) -> int:
"""
Public-facing method to send a transaction (or batch of messages).
:param messages: A list of Message objects representing the transaction(s).
:param transaction: A Transaction object representing the transaction(s).
:return: The RPC request ID associated with this transaction.
:raises TonConnectError: If there's no active bridge session.
"""
logger.debug(f"Sending transaction with {len(messages)} messages: {messages}")
transaction = Transaction(messages=messages)
logger.debug(f"Sending transaction with {len(transaction.messages)} messages: {transaction.messages}")

# Retrieve connection data to increment and store the next RPC request ID.
connection = await self.bridge.get_stored_connection_data() # type: ignore
Expand Down Expand Up @@ -480,7 +473,7 @@ async def connect_wallet(
logger.debug(f"Wallet is already connected for user_id={self.user_id}")
raise TonConnectError("A wallet is already connected.")

if self._connect_timeout_task is not None:
if self._connect_timeout_task is not None and not self._connect_timeout_task.done():
self._connect_timeout_task.cancel()
logger.debug(f"Cancelled existing connect timeout task for user_id={self.user_id}")

Expand Down Expand Up @@ -582,6 +575,21 @@ def connect_wallet_context(self) -> Connector.ConnectWalletContext:
"""
return Connector.ConnectWalletContext(self)

def cancel_connection_request(self) -> None:
"""
Cancels a pending wallet connection request, removing it from the pending list.
"""
future = self.bridge.pending_requests.get(self.bridge.RESERVED_ID)
if future is not None and not future.done():
future.cancel()
logger.debug("Cancelled pending wallet connection request for user_id=%d", self.user_id)

if self.bridge.RESERVED_ID in self.bridge.pending_requests:
del self.bridge.pending_requests[self.bridge.RESERVED_ID]

if self._connect_timeout_task is not None and not self._connect_timeout_task.done():
self._connect_timeout_task.cancel()

def is_transaction_pending(self, rpc_request_id: int) -> bool:
"""
Checks if a particular transaction (by RPC request ID) is still pending.
Expand Down Expand Up @@ -632,49 +640,6 @@ def get_max_supported_messages(self) -> Optional[int]:
logger.debug(f"Max supported messages for user_id={self.user_id}: {max_messages}")
return max_messages

@staticmethod
def create_transfer_message(
destination: Union[Address, str],
amount: Union[float, int],
body: Optional[Union[Cell, str]] = None,
state_init: Optional[StateInit] = None,
**_: Any,
) -> Message:
"""
Creates a basic transfer message compatible with the SendTransactionRequest.
:param destination: The Address object or string representing the recipient.
:param amount: The amount in TONs to be transferred.
:param body: Optional message payload (Cell or string).
:param state_init: Optional StateInit for deploying contracts.
:param _: Any additional keyword arguments are ignored.
:return: A Message object ready to be sent.
"""
destination_str = destination.to_str() if isinstance(destination, Address) else destination
state_init_b64 = boc_to_base64_string(state_init.serialize().to_boc()) if state_init else None

if body is not None:
if isinstance(body, str):
# Convert string payload to a Cell.
body_cell = (
begin_cell()
.store_uint(0, 32)
.store_snake_string(body)
.end_cell()
)
body = boc_to_base64_string(body_cell.to_boc())
else:
# Body is already a Cell; convert to base64.
body = boc_to_base64_string(body.to_boc())

message = Message(
address=destination_str,
amount=str(to_nano(amount)),
payload=body,
state_init=state_init_b64,
)
return message

async def send_transfer(
self,
destination: Union[Address, str],
Expand All @@ -691,8 +656,9 @@ async def send_transfer(
:param state_init: An optional StateInit.
:return: The RPC request ID for the transaction.
"""
message = self.create_transfer_message(destination, amount, body, state_init)
request_id = await self._send_transaction([message])
message = Transaction.create_message(destination, amount, body, state_init)
transaction = Transaction(messages=[message])
request_id = await self.send_transaction(transaction)

logger.debug(f"Transfer sent for user_id={self.user_id} with request ID={request_id}")
return request_id
Expand All @@ -704,8 +670,9 @@ async def send_batch_transfer(self, data_list: List[TransferData]) -> int:
:param data_list: A list of TransferData objects, each describing a transfer.
:return: The RPC request ID for the batched transaction.
"""
messages = [self.create_transfer_message(**data.__dict__) for data in data_list]
request_id = await self._send_transaction(messages)
messages = [Transaction.create_message(**data.__dict__) for data in data_list]
transaction = Transaction(messages=messages)
request_id = await self.send_transaction(transaction)

logger.debug(f"Batch transfer sent for user_id={self.user_id} with request ID={request_id}")
return request_id
Expand Down
2 changes: 1 addition & 1 deletion tonutils/tonconnect/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

from pytoniq_core import Address

from tonutils.tonconnect.utils.exceptions import TonConnectError
from .chain import CHAIN
from ..utils.exceptions import TonConnectError


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion tonutils/tonconnect/models/proof.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from typing import Any, Dict, Optional

from tonutils.tonconnect.utils.exceptions import TonConnectError
from ..utils.exceptions import TonConnectError


@dataclass
Expand Down
92 changes: 88 additions & 4 deletions tonutils/tonconnect/models/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import json
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union

from pytoniq_core import Cell
from pytoniq_core import Cell, Address, begin_cell, StateInit

from .chain import CHAIN
from ..utils.exceptions import TonConnectError
from ...utils import boc_to_base64_string, to_nano


class ItemName(str, Enum):
Expand All @@ -27,6 +27,16 @@ class ConnectItem:
name: str
payload: Optional[Any] = None

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> ConnectItem:
"""
Creates a ConnectItem instance from a dictionary.
:param data: A dictionary containing the item data.
:return: An instance of ConnectItem.
"""
return ConnectItem(name=data["name"], payload=data.get("payload"))

def to_dict(self) -> Dict[str, Any]:
"""
Converts the ConnectItem instance into a dictionary format.
Expand Down Expand Up @@ -57,6 +67,21 @@ def __repr__(self) -> str:
f"state_init={self.state_init})"
)

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Message:
"""
Creates a Message instance from a dictionary.
:param data: A dictionary containing message data.
:return: An instance of Message.
"""
return Message(
address=data["address"],
amount=data["amount"],
payload=data.get("payload"),
state_init=data.get("stateInit"),
)

def to_dict(self) -> Dict[str, Any]:
"""
Converts the Message instance into a dictionary format.
Expand Down Expand Up @@ -92,14 +117,72 @@ def __repr__(self) -> str:
f"messages={self.messages})"
)

@classmethod
def create_message(
cls,
destination: Union[Address, str],
amount: Union[float, int],
body: Optional[Union[Cell, str]] = None,
state_init: Optional[StateInit] = None,
**_: Any,
) -> Message:
"""
Creates a basic transfer message compatible with the SendTransactionRequest.
:param destination: The Address object or string representing the recipient.
:param amount: The amount in TONs to be transferred.
:param body: Optional message payload (Cell or string).
:param state_init: Optional StateInit for deploying contracts.
:param _: Any additional keyword arguments are ignored.
:return: A Message object ready to be sent.
"""
destination_str = destination.to_str() if isinstance(destination, Address) else destination
state_init_b64 = boc_to_base64_string(state_init.serialize().to_boc()) if state_init else None

if body is not None:
if isinstance(body, str):
# Convert string payload to a Cell.
body_cell = (
begin_cell()
.store_uint(0, 32)
.store_snake_string(body)
.end_cell()
)
body = boc_to_base64_string(body_cell.to_boc())
else:
# Body is already a Cell; convert to base64.
body = boc_to_base64_string(body.to_boc())

return Message(
address=destination_str,
amount=str(to_nano(amount)),
payload=body,
state_init=state_init_b64,
)

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> Transaction:
"""
Creates a Transaction instance from a dictionary.
:param data: A dictionary containing transaction data.
:return: An instance of Transaction.
"""
return Transaction(
from_=data.get("from"),
network=CHAIN(data.get("network")),
valid_until=data.get("validUntil"),
messages=[Message.from_dict(message) for message in data.get("messages", [])],
)

def to_dict(self) -> Dict[str, Any]:
"""
Converts the Transaction instance into a dictionary format.
:return: A dictionary representation of the Transaction.
"""
return {
"valid_until": self.valid_until,
"validUntil": self.valid_until,
"from": self.from_,
"network": self.network.value if self.network else None,
"messages": [message.to_dict() for message in self.messages],
Expand Down Expand Up @@ -177,6 +260,7 @@ def cell(self) -> Cell:
:return: A Cell object created from the BOC string.
"""
if not self.boc:
from ..utils.exceptions import TonConnectError
raise TonConnectError("BOC data is missing in the transaction response.")
return Cell.one_from_boc(self.boc)

Expand Down
4 changes: 3 additions & 1 deletion tonutils/tonconnect/models/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def direct_url(self) -> Optional[str]:
"""
if self.universal_url is None:
return None
return self.universal_url_to_direct_url(self.universal_url)

url = self.universal_url_to_direct_url(self.universal_url)
return url + "?startapp=tonconnect" if "t.me/wallet" in url else url

@staticmethod
def universal_url_to_direct_url(universal_url: str) -> str:
Expand Down
4 changes: 2 additions & 2 deletions tonutils/tonconnect/provider/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import aiohttp

from tonutils.tonconnect.utils.exceptions import TonConnectError
from tonutils.tonconnect.utils.logger import logger
from ..models import (
Request,
SendConnectRequest,
Expand All @@ -17,6 +15,8 @@
)
from ..provider.session import BridgeSession, SessionCrypto
from ..storage import IStorage
from ..utils.exceptions import TonConnectError
from ..utils.logger import logger


class HTTPBridge:
Expand Down
6 changes: 3 additions & 3 deletions tonutils/tonconnect/tonconnect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
from copy import copy
from typing import Optional, List, Dict, Callable, Union, Any

from tonutils.tonconnect.utils.exceptions import TonConnectError
from tonutils.tonconnect.utils.logger import logger
from tonutils.tonconnect.utils.wallet_manager import WalletsListManager
from .connector import Connector
from .models import WalletApp
from .models.event import (
Expand All @@ -15,6 +12,9 @@
EventHandlersData,
)
from .storage import IStorage
from .utils.exceptions import TonConnectError
from .utils.logger import logger
from .utils.wallet_manager import WalletsListManager


class TonConnect:
Expand Down
Loading

0 comments on commit 04611a0

Please sign in to comment.