Add coinpayments
parent
c0d6718db8
commit
e219562506
@ -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
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
Loading…
Reference in New Issue