Add coinpayments

master
Alice 5 years ago
parent c0d6718db8
commit e219562506

@ -249,6 +249,14 @@ PAYMENTS_BACKENDS = {
'currency': 'EUR', 'currency': 'EUR',
# 'title': '', # 'title': '',
}, },
'coinpayments': {
'enabled': False,
'merchant_id': '',
'secret': '',
'currency': 'EUR',
# 'api_base': '',
# 'title': '',
},
} }
PAYMENTS_CURRENCY = ('eur', '') PAYMENTS_CURRENCY = ('eur', '')

@ -174,7 +174,7 @@ def index(request):
ref_url=ref_url, ref_url=ref_url,
twitter_link=twitter_url + urlencode(twitter_args), twitter_link=twitter_url + urlencode(twitter_args),
subscription=request.user.vpnuser.get_subscription(include_unconfirmed=True), subscription=request.user.vpnuser.get_subscription(include_unconfirmed=True),
backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_id), backends=sorted(ACTIVE_BACKENDS.values(), key=lambda x: x.backend_display_name),
subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values() subscr_backends=sorted((b for b in ACTIVE_BACKENDS.values()
if b.backend_has_recurring), if b.backend_has_recurring),
key=lambda x: x.backend_id), key=lambda x: x.backend_id),

@ -6,4 +6,5 @@ from .bitcoin import BitcoinBackend
from .stripe import StripeBackend from .stripe import StripeBackend
from .coinbase import CoinbaseBackend from .coinbase import CoinbaseBackend
from .coingate import CoinGateBackend from .coingate import CoinGateBackend
from .coinpayments import CoinPaymentsBackend

@ -0,0 +1,301 @@
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 = '<form action="https://www.coinpayments.net/index.php" method="POST" id="cp-form">'
for k, v in params.items():
form += '<input type="hidden" name="%s" value="%s" />' % (k, v)
form += '''
<img src="/static/img/spinner.gif" style="margin: auto;" alt="redirecting..." id="cp-spinner" />
<input type="submit" class="button" name="submitbutton" value="Continue" />
</form>
<script>
document.addEventListener("DOMContentLoaded", function(event) {{
var f = document.getElementById("cp-form");
f.elements["submitbutton"].style.display = "none";
document.getElementById("cp-spinner").style.display = "block";
f.submit();
}});
</script>
'''
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

@ -14,6 +14,7 @@ urlpatterns = [
url(r'^callback/coingate/(?P<id>[0-9]+)$', views.callback_coingate, name='cb_coingate'), url(r'^callback/coingate/(?P<id>[0-9]+)$', views.callback_coingate, name='cb_coingate'),
url(r'^callback/stripe/(?P<id>[0-9]+)$', views.callback_stripe, name='cb_stripe'), url(r'^callback/stripe/(?P<id>[0-9]+)$', views.callback_stripe, name='cb_stripe'),
url(r'^callback/coinbase/$', views.callback_coinbase, name='cb_coinbase'), url(r'^callback/coinbase/$', views.callback_coinbase, name='cb_coinbase'),
url(r'^callback/coinpayments/(?P<id>[0-9]+)$', views.callback_coinpayments, name='cb_coinpayments'),
url(r'^callback/paypal_subscr/(?P<id>[0-9]+)$', views.callback_paypal_subscr, name='cb_paypal_subscr'), url(r'^callback/paypal_subscr/(?P<id>[0-9]+)$', views.callback_paypal_subscr, name='cb_paypal_subscr'),
url(r'^callback/stripe_subscr/(?P<id>[0-9]+)$', views.callback_stripe_subscr, name='cb_stripe_subscr'), url(r'^callback/stripe_subscr/(?P<id>[0-9]+)$', views.callback_stripe_subscr, name='cb_stripe_subscr'),

@ -119,6 +119,18 @@ def callback_coinbase(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
@csrf_exempt
def callback_coinpayments(request, id):
""" CoinPayments payment callback """
backend = require_backend('coinpayments')
p = Payment.objects.get(id=id)
if backend.callback(p, request):
return HttpResponse()
else:
return HttpResponseBadRequest()
@csrf_exempt @csrf_exempt
def callback_paypal_subscr(request, id): def callback_paypal_subscr(request, id):
""" PayPal Subscription IPN """ """ PayPal Subscription IPN """

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Loading…
Cancel
Save