1+ import base64
2+ import hashlib
3+ import json
14import logging
5+ import uuid
6+ from hmac import compare_digest
27
8+ import aiohttp
39from aiogram import Bot
410from aiogram .fsm .storage .redis import RedisStorage
511from aiogram .utils .i18n import I18n
12+ from aiogram .utils .i18n import gettext as _
613from aiogram .utils .i18n import lazy_gettext as __
714from aiohttp .web import Application , Request , Response
815from sqlalchemy .ext .asyncio import async_sessionmaker
916
1017from app .bot .models import ServicesContainer , SubscriptionData
1118from app .bot .payment_gateways import PaymentGateway
12- from app .bot .utils .constants import CRYPTOMUS_WEBHOOK , Currency
19+ from app .bot .utils .constants import CRYPTOMUS_WEBHOOK , Currency , TransactionStatus
1320from app .bot .utils .navigation import NavSubscription
1421from app .config import Config
22+ from app .db .models import Transaction
1523
1624logger = logging .getLogger (__name__ )
1725
@@ -30,7 +38,7 @@ def __init__(
3038 bot : Bot ,
3139 i18n : I18n ,
3240 services : ServicesContainer ,
33- ):
41+ ) -> None :
3442 self .name = __ ("payment:gateway:cryptomus" )
3543 self .app = app
3644 self .config = config
@@ -40,12 +48,51 @@ def __init__(
4048 self .i18n = i18n
4149 self .services = services
4250
43- self .app .router .add_post (CRYPTOMUS_WEBHOOK , lambda request : self .webhook_handler ( request ) )
51+ self .app .router .add_post (CRYPTOMUS_WEBHOOK , self .webhook_handler )
4452 logger .info ("Cryptomus payment gateway initialized." )
4553
4654 async def create_payment (self , data : SubscriptionData ) -> str :
55+ bot_username = (await self .bot .get_me ()).username
56+ redirect_url = f"https://t.me/{ bot_username } "
57+ order_id = str (uuid .uuid4 ())
58+ price = str (data .price )
59+
60+ payload = {
61+ "amount" : price ,
62+ "currency" : self .currency .code ,
63+ "order_id" : order_id ,
64+ "url_return" : redirect_url ,
65+ "url_success" : redirect_url ,
66+ "url_callback" : self .config .bot .DOMAIN + CRYPTOMUS_WEBHOOK ,
67+ "lifetime" : 1800 ,
68+ "is_payment_multiple" : False ,
69+ }
70+ headers = {
71+ "merchant" : self .config .cryptomus .MERCHANT_ID ,
72+ "sign" : self .generate_signature (json .dumps (payload )),
73+ "Content-Type" : "application/json" ,
74+ }
75+
76+ async with aiohttp .ClientSession () as session :
77+ url = "https://api.cryptomus.com/v1/payment"
78+ async with session .post (url , json = payload , headers = headers ) as response :
79+ result = await response .json ()
80+ if response .status == 200 and result .get ("result" , {}).get ("url" ):
81+ pay_url = result ["result" ]["url" ]
82+ else :
83+ raise Exception (f"Error: { response .status } ; Result: { result } ; Data: { data } " )
84+
85+ async with self .session () as session :
86+ await Transaction .create (
87+ session = session ,
88+ tg_id = data .user_id ,
89+ subscription = data .pack (),
90+ payment_id = result ["result" ]["order_id" ],
91+ status = TransactionStatus .PENDING ,
92+ )
93+
4794 logger .info (f"Payment link created for user { data .user_id } : { pay_url } " )
48- pass
95+ return pay_url
4996
5097 async def handle_payment_succeeded (self , payment_id : str ) -> None :
5198 await self ._on_payment_succeeded (payment_id )
@@ -54,4 +101,57 @@ async def handle_payment_canceled(self, payment_id: str) -> None:
54101 await self ._on_payment_canceled (payment_id )
55102
56103 async def webhook_handler (self , request : Request ) -> Response :
57- pass
104+ logger .debug (f"Received Cryptomus webhook request" )
105+ try :
106+ event_json = await request .json ()
107+
108+ if not self .verify_webhook (request , event_json ):
109+ return Response (status = 403 )
110+
111+ match event_json .get ("status" ):
112+ case "paid" | "paid_over" :
113+ order_id = event_json .get ("order_id" )
114+ await self .handle_payment_succeeded (order_id )
115+ return Response (status = 200 )
116+
117+ case "cancel" :
118+ order_id = event_json .get ("order_id" )
119+ await self .handle_payment_canceled (order_id )
120+ return Response (status = 200 )
121+
122+ case _:
123+ return Response (status = 400 )
124+
125+ except Exception as exception :
126+ logger .exception (f"Error processing Cryptomus webhook: { exception } " )
127+ return Response (status = 400 )
128+
129+ def verify_webhook (self , request : Request , data : dict ) -> bool :
130+ client_ip = (
131+ request .headers .get ("CF-Connecting-IP" )
132+ or request .headers .get ("X-Real-IP" )
133+ or request .headers .get ("X-Forwarded-For" )
134+ or request .remote
135+ )
136+ if client_ip not in ["91.227.144.54" ]:
137+ logger .warning (f"Unauthorized IP: { client_ip } " )
138+ return False
139+
140+ sign = data .pop ("sign" , None )
141+ if not sign :
142+ logger .warning ("Missing signature." )
143+ return False
144+
145+ json_data = json .dumps (data , separators = ("," , ":" ))
146+ hash_value = self .generate_signature (json_data )
147+
148+ if not compare_digest (hash_value , sign ):
149+ logger .warning (f"Invalid signature." )
150+ return False
151+
152+ return True
153+
154+ def generate_signature (self , data : str ) -> str :
155+ base64_encoded = base64 .b64encode (data .encode ()).decode ()
156+ raw_string = f"{ base64_encoded } { self .config .cryptomus .API_KEY } "
157+ return hashlib .md5 (raw_string .encode ()).hexdigest ()
0 commit comments