import math from decimal import Decimal from django.shortcuts import redirect from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from constance import config as site_config from django.conf import settings as project_settings from .base import BackendBase import hmac import hashlib import requests import logging import json from urllib.parse import urlencode logger = logging.getLogger(__name__) class CoinPaymentsError(Exception): pass class CoinPayments: def __init__(self, pkey, skey, api_url=None): self.public_key = pkey self.secret_key = skey.encode('utf-8') self.api_url = api_url or 'https://www.coinpayments.net/api.php' def _sign(self, params): body = urlencode(params).encode('utf-8') mac = hmac.new(self.secret_key, body, hashlib.sha512) return body, mac.hexdigest() def _request(self, cmd, params): params.update({ 'cmd': cmd, 'key': self.public_key, 'format': 'json', 'version': 1, }) print(params) post_body, mac = self._sign(params) headers = { 'HMAC': mac, 'Content-Type': 'application/x-www-form-urlencoded', } r = requests.post(self.api_url, data=post_body, headers=headers) try: r.raise_for_status() j = r.json() except Exception as e: raise CoinPaymentsError(str(e)) from e if j.get('error') == 'ok': return j.get('result') else: raise CoinPaymentsError(j.get('error')) def create_transaction(self, **params): assert 'amount' in params assert 'currency1' in params assert 'currency2' in params return self._request('create_transaction', params) def get_account_info(self, **params): return self._request('get_basic_info', params) def get_rates(self, **params): return self._request('rates', params) def get_balances(self, **params): return self._request('balances', params) def get_deposit_address(self, **params): assert 'currency' in params return self._request('get_deposit_address', params) def get_callback_address(self, **params): assert 'currency' in params return self._request('get_callback_address', params) def get_tx_info(self, **params): assert 'txid' in params return self._request('get_tx_info', params) def get_tx_info_multi(self, ids=None, **params): if ids is not None: params['txid'] = '|'.join(str(i) for i in ids) assert 'txid' in params return self._request('get_tx_info_multi', params) def get_tx_ids(self, **params): return self._request('get_tx_ids', params) def create_transfer(self, **params): assert 'amount' in params assert 'currency' in params assert 'merchant' in params or 'pbntag' in params return self._request('create_transfer', params) def create_withdrawal(self, **params): assert 'amount' in params assert 'currency' in params assert 'address' in params or 'pbntag' in params return self._request('create_withdrawal', params) def create_mass_withdrawal(self, **params): assert 'wd' in params return self._request('create_mass_withdrawal', params) def convert(self, **params): assert 'amount' in params assert 'from' in params assert 'to' in params return self._request('convert', params) def get_withdrawal_history(self, **params): return self._request('get_withdrawal_history', params) def get_withdrawal_info(self, **params): assert 'id' in params return self._request('get_withdrawal_info', params) def get_conversion_info(self, **params): assert 'id' in params return self._request('get_conversion_info', params) def get_pbn_info(self, **params): assert 'pbntag' in params return self._request('get_pbn_info', params) def get_pbn_list(self, **params): return self._request('get_pbn_list', params) def update_pbn_tag(self, **params): assert 'tagid' in params return self._request('update_pbn_tag', params) def claim_pbn_tag(self, **params): assert 'tagid' in params assert 'name' in params return self._request('claim_pbn_tag', params) class IpnError(Exception): pass def ipn_assert(request, remote, local, key=None, delta=None): if (delta is None and remote != local) or (delta is not None and not math.isclose(remote, local, abs_tol=delta)): logger.debug("Invalid IPN %r: local=%r remote=%r", key, local, remote) raise IpnError("Unexpected value: %s" % key) def ipn_assert_post(request, key, local): remote = request.POST.get(key) ipn_assert(request, remote, local, key=key) class CoinPaymentsBackend(BackendBase): backend_id = 'coinpayments' backend_verbose_name = _("CoinPayments") backend_display_name = _("Cryptocurrencies") backend_has_recurring = False def __init__(self, settings): self.merchant_id = settings.get('merchant_id') self.currency = settings.get('currency', 'EUR') self.api_base = settings.get('api_base', None) self.title = settings.get('title', 'VPN Payment') self.secret = settings.get('secret', '').encode('utf-8') if self.merchant_id and self.secret: self.backend_enabled = True def new_payment(self, payment): ROOT_URL = project_settings.ROOT_URL params = { 'cmd': '_pay', 'reset': '1', 'want_shipping': '0', 'merchant': self.merchant_id, 'currency': self.currency, 'amountf': '%.2f' % (payment.amount / 100), 'item_name': self.title, 'ipn_url': ROOT_URL + reverse('payments:cb_coinpayments', args=(payment.id,)), 'success_url': ROOT_URL + reverse('payments:view', args=(payment.id,)), 'cancel_url': ROOT_URL + reverse('payments:cancel', args=(payment.id,)), } payment.status_message = _("Waiting for CoinPayments to confirm the transaction... " + "It can take up to a few minutes...") payment.save() form = '
' for k, v in params.items(): form += '' % (k, v) form += ''' redirecting...
''' return form def handle_ipn(self, payment, request): sig = request.META.get('HTTP_HMAC') if not sig: raise IpnError("Missing HMAC") mac = hmac.new(self.secret, request.body, hashlib.sha512).hexdigest() # Sanity checks, if it fails the IPN is to be ignored ipn_assert(request, sig, mac, 'HMAC') ipn_assert_post(request, 'ipn_mode', 'hmac') ipn_assert_post(request, 'merchant', self.merchant_id) try: status = int(request.POST.get('status')) except ValueError: raise IpnError("Invalid status (%r)" % status) # Some states are final (can't cancel a timeout or refund) if payment.status not in ('new', 'confirmed', 'error'): m = "Unexpected state change for %s: is %s, received status=%r" % ( payment.id, payment.status, status ) raise IpnError(m) # whatever the status, we can safely update the text and save the tx id payment.status_text = request.POST.get('status_text') or payment.status_text payment.backend_extid = request.POST.get('txn_id') received_amount = request.POST.get('amount1') if received_amount: payment.paid_amount = float(received_amount) * 100 # And now the actual processing if status == 1: # A payment is confirmed paid if payment.status != 'confirmed': if payment.paid_amount != payment.amount: ipn_assert(request, payment.paid_amount, payment.amount, 'paid', delta=10) vpnuser = payment.user.vpnuser vpnuser.add_paid_time(payment.time) vpnuser.on_payment_confirmed(payment) vpnuser.save() # We save the new state *at the end* # (it will be retried if there's an error) payment.status = 'confirmed' payment.status_message = None payment.save() elif status > 1: # Waiting (that's further confirmation about funds getting moved) # we have nothing to do, except updating status_text payment.save() return elif status == -1: # Cancel / Time out payment.status = 'cancelled' payment.save() elif status == -2: # A refund if payment.status == 'confirmed': # (paid -> refunded) payment.status = 'refunded' # TODO elif status <= -3: # Unknown error payment.status = 'error' payment.save() def callback(self, payment, request): try: self.handle_ipn(payment, request) return True except IpnError as e: payment.status = 'error' payment.status_message = ("Error processing the payment. " "Please contact support.") payment.backend_data['ipn_exception'] = repr(e) payment.backend_data['ipn_last_data'] = repr(request.POST) payment.save() logger.warn("IPN error: %s", e) raise