"""가맹점 쪽 검증 도구. 게이트웨이 서버를 믿지 않고 직접 확인할 때 사용. verify_webhook(secret, headers, raw_body) 웹훅 서명·시각 검증 verify_address(xpub, index, address) 주소가 내 xpub 에서 나온 것인지 (B/C) verify_onchain(tx_id, address, amount, api, usdt) 체인에서 실제 입금 확인 check_order(order, xpub=None, fixed_address=None) 주문 만든 응답을 고객에게 보여주기 전에 검사 payout_sig(auto_secret, ref, to, amount) 자동 출금 요청 sig (서명 프로그램이 확인) """ import hashlib import hmac import time from decimal import Decimal import requests MAINNET = ("https://api.trongrid.io", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t") def verify_webhook(secret: str, headers: dict, raw_body: bytes, tolerance_sec: int = 300) -> bool: ts = headers.get("X-Gw-Timestamp", "") sig = headers.get("X-Gw-Signature", "") if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance_sec: return False want = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(want, sig) def payout_sig(auto_secret: str, ref: str, to: str, amount) -> str: micro = int(Decimal(str(amount)) * 1_000_000) return hmac.new(auto_secret.encode(), f"{ref}|{to}|{micro}".encode(), hashlib.sha256).hexdigest() def verify_address(xpub: str, index: int, address: str) -> bool: from bip_utils import Bip44, Bip44Changes, Bip44Coins acc = Bip44.FromExtendedKey(xpub, Bip44Coins.TRON) return acc.Change(Bip44Changes.CHAIN_EXT).AddressIndex(int(index)).PublicKey().ToAddress() == address class AddressMismatch(Exception): pass def check_order(order: dict, xpub: str = None, fixed_address: str = None) -> dict: """게이트웨이 서버가 털려 주소를 바꿔치기해도 고객이 공격자에게 보내지 않게. B/C 는 xpub, A 는 내 받는 주소를 넣음. 다르면 AddressMismatch → 고객에게 보여주지 말 것. 가장 안전: 검사한 order["address"] 로 QR·주소를 내 화면에 직접 띄움 (pay_url 은 게이트웨이 화면).""" addr = order.get("address") or "" if fixed_address: ok = addr == fixed_address elif xpub and order.get("address_index") is not None: ok = verify_address(xpub, order["address_index"], addr) else: raise ValueError("xpub 또는 fixed_address 필요") if not ok: raise AddressMismatch(addr) return order def verify_onchain(tx_id: str, address: str, amount: str, api: str = MAINNET[0], usdt: str = MAINNET[1]) -> bool: """tx_id 안에 usdt 계약의 address 앞 Transfer 가 amount 만큼 있고 확정됐는지.""" want = int(Decimal(amount) * 1_000_000) r = requests.get(f"{api}/v1/transactions/{tx_id}/events", params={"only_confirmed": "true"}, timeout=15) r.raise_for_status() total = 0 for ev in r.json().get("data", []): if ev.get("contract_address") != usdt or ev.get("event_name") != "Transfer": continue res = ev.get("result", {}) to = res.get("to") or res.get("1") or "" if _to_base58(to) == address: total += int(res.get("value") or res.get("2") or 0) return total >= want > 0 def _to_base58(a: str) -> str: if a.startswith("T"): return a from bip_utils import Base58Encoder h = a.lower().removeprefix("0x") if len(h) == 40: h = "41" + h raw = bytes.fromhex(h) chk = hashlib.sha256(hashlib.sha256(raw).digest()).digest()[:4] return Base58Encoder.Encode(raw + chk)