Files
pyfragment/app/utils/wallet.py
T
bohd4nx efcdda6b74 refactor: migrate to tonutils 2.0.0 and restructure codebase
- Migrate from tonutils 0.5.x to 2.0.0 API:
  - ToncenterClient → TonapiClient with NetworkGlobalID
  - tonutils.client → tonutils.clients
  - tonutils.wallet → tonutils.contracts.wallet
  - pub_key.as_hex property, wallet.balance nanotons
  - async with client context manager

- Centralize TON client init into initialize_ton_client() in wallet.py
- Move process_transaction logic into wallet.py, transaction.py re-exports
- Add WALLET_VERSION config (V4R2/V5R1, default V5R1)
- Add WALLET_CLASSES and SUPPORTED_WALLET_VERSIONS to constants.py
- Restore balance check before broadcasting
- Wait for seqno confirmation after transfer (120×2s) to prevent duplicate seqno

- Fix Fragment hash fetching: use browser navigation headers
- Remove accept-encoding from BASE_HEADERS (httpx handles decompression)
- Add cookies.example.json template
- Add cookies.json to .gitignore
2026-03-05 05:47:27 +02:00

122 lines
3.9 KiB
Python

import asyncio
import base64
import json
import logging
from typing import Any
import httpx
from tonutils.clients import TonapiClient
from tonutils.types import NetworkGlobalID
from app.core import config
from app.core.constants import DEVICE, WALLET_CLASSES
from app.core.exceptions import TransactionError, WalletError
from app.utils.decoder import clean_decode
logger = logging.getLogger(__name__)
def initialize_ton_client() -> TonapiClient:
return TonapiClient(network=NetworkGlobalID.MAINNET, api_key=config.API_KEY)
async def process_transaction(transaction_data: dict) -> str:
logger.debug("transaction_data: %s", transaction_data)
if "transaction" not in transaction_data or "messages" not in transaction_data["transaction"]:
raise TransactionError(
"Fragment returned an invalid transaction payload. "
"The API response is missing expected 'transaction.messages' data."
)
async with initialize_ton_client() as client:
wallet_cls = WALLET_CLASSES[config.WALLET_VERSION]
wallet, _, _, _ = wallet_cls.from_mnemonic(client=client, mnemonic=config.SEED)
# Check balance before broadcasting
# try:
# await wallet.refresh()
# balance_ton = wallet.balance / 1_000_000_000
# if balance_ton < 0.056:
# raise WalletError(
# f"TON wallet balance is too low: {balance_ton:.2f} TON. "
# "Minimum required is 0.056 TON."
# )
# except WalletError:
# raise
# except Exception as exc:
# raise WalletError(f"Wallet balance check failed: {exc}") from exc
try:
message = transaction_data["transaction"]["messages"][0]
payload = clean_decode(message["payload"])
seqno_before = wallet.seqno
result = await wallet.transfer(
destination=message["address"],
amount=int(message["amount"]), # nanotons, not TON
body=payload,
)
# Wait for on-chain confirmation so the next call sees updated seqno
for _ in range(30):
await asyncio.sleep(3)
await wallet.refresh()
if wallet.seqno != seqno_before:
break
return result
except (WalletError, TransactionError):
raise
except Exception as exc:
raise TransactionError(f"Transaction broadcast failed: {exc}") from exc
async def get_account_info() -> dict[str, Any]:
async with initialize_ton_client() as client:
try:
wallet_cls = WALLET_CLASSES[config.WALLET_VERSION]
wallet, pub_key, _, _ = wallet_cls.from_mnemonic(client=client, mnemonic=config.SEED)
boc = wallet.state_init.serialize().to_boc()
return {
"address": wallet.address.to_str(False, False),
"publicKey": pub_key.as_hex,
"chain": "-239",
"walletStateInit": base64.b64encode(boc).decode(),
}
except Exception as exc:
raise WalletError(f"Failed to retrieve wallet account info: {exc}") from exc
async def link_wallet(
client: httpx.AsyncClient,
headers: dict,
cookies: dict,
account: dict[str, Any],
fragment_hash: str,
) -> bool:
resp = await client.post(
f"https://fragment.com/api?hash={fragment_hash}",
headers=headers,
cookies=cookies,
data={
"account": json.dumps(account),
"device": DEVICE,
"method": "linkWallet",
},
)
result = resp.json()
if result.get("ok"):
return True
if "transaction" in result:
try:
await process_transaction(result)
return True
except (TransactionError, WalletError):
return False
return False