· 5 years ago · Apr 08, 2020, 10:36 PM
1'1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY,from django import template from urllib.parse import urlencode, urlparse, urlunparse, parse_qs from blockcypher.utils
2import format_crypto_units, estimate_satoshis_transacted from blockcypher.api
3import _get_websocket_url from blockcypher.constants
4import COIN_SYMBOL_MAPPINGS register = template.Library() @register.simple_tag(name='satoshis_to_user_units_trimmed') def satoshis_to_user_units_trimmed(input_satoshis, user_unit='btc', coin_symbol='btc', print_cs=True, round_digits=0):
5 return format_crypto_units( input_quantity=input_satoshis, input_type='satoshi', output_type=user_unit, coin_symbol=coin_symbol, print_cs=print_cs, safe_trimming=True, round_digits=round_digits, ) @register.assignment_tag def estimate_satoshis_from_tx(txn_inputs, txn_outputs):
6 return estimate_satoshis_transacted(inputs=txn_inputs, outputs=txn_outputs) @register.filter(name='coin_symbol_to_display_name') def coin_symbol_to_display_name(coin_symbol):
7 return COIN_SYMBOL_MAPPINGS[coin_symbol]['display_name'] @register.filter(name='coin_symbol_to_display_shortname') def coin_symbol_to_display_shortname(coin_symbol):
8 return COIN_SYMBOL_MAPPINGS[coin_symbol]['display_shortname'] @register.filter(name='coin_symbol_to_currency_name') def coin_symbol_to_currency_name(coin_symbol):
9 return COIN_SYMBOL_MAPPINGS[coin_symbol]['currency_abbrev'] @register.filter(name='coin_symbol_to_wss') def coin_symbol_to_wss(coin_symbol):
10 return _get_websocket_url(coin_symbol) @register.filter(name='txn_outputs_to_data_dict')
11 def txn_outputs_to_data_dict(txn_outputs): ''' NOTE: Assumes each transaction can only have one null data output, which is not a strict requirement
12 (https://github.com/blockcypher/explorer/issues/150#issuecomment-143899714)
13 for txn_output in txn_outputs: if txn_output.get('data_hex') or txn_output.get('data_string'): return { 'data_hex': txn_output.get('data_hex'), 'data_string': txn_output.get('data_string'), } @register.simple_tag def build_url(base_url, **query_params): """ Entry point for the build_url template tag. This tag allows you to maintain a set of default querystring parameters and override an individual param.
14 /*Inspired by https://djangosnippets.org/snippets/2332/ on 2015-10-26 Usage:*/ {% build_url base_url query_params %} base_url: string variable -- the URLs prefix try {% url some-url as base_url %} query_params: dictionary of default querystring values. {'k1':'v1', 'k2':'mountain'} -> ?k1=v1&k2=mountain (output): (string) the url """
15 # print('base_url', base_url) # print('query_params', query_params) url_parsed = urlparse(base_url) # print('url_parsed', url_parsed) qs_parsed = parse_qs(url_parsed.query) # print('qs_parsed', qs_parsed) new_qs = {} for key in qs_parsed: # parsing querystring returns a list entries (in case of dups) # crude deduplication here: new_qs[key] = qs_parsed[key][0] if query_params: for key in query_params: if query_params[key]: # only work if there is a value for that key (defensive) new_qs[key] = query_params[key] # print('new_qs', new_qs)
16 return urlunparse( [ url_parsed.scheme, url_parsed.netloc, url_parsed.path, url_parsed.params, urlencode(new_qs), url_parsed.fragment, ] )
17from django.http import HttpResponseRedirect, HttpResponse
18
19from django.core.urlresolvers import reverse
20
21from django.contrib import messages
22
23from django.contrib.auth import login
24
25from django.contrib.auth.decorators import login_required
26
27from django.utils.translation import ugettext_lazy as _
28
29from django.utils.timezone import now
30
31from django.views.decorators.csrf import csrf_exempt
32
33from django.views.decorators.clickjacking import xframe_options_exempt
34
35from django.shortcuts import get_object_or_404
36
37
38from annoying.decorators import render_to
39
40from annoying.functions import get_object_or_None
41
42
43from blockexplorer.decorators import assert_valid_coin_symbol
44
45
46from blockexplorer.raven import client
47
48
49from blockexplorer.settings import BLOCKCYPHER_PUBLIC_KEY, BLOCKCYPHER_API_KEY, WEBHOOK_SECRET_KEY, BASE_URL
50
51
52from blockcypher.api import get_address_full, get_address_overview, subscribe_to_address_webhook, get_forwarding_address_details, unsubscribe_from_webhook
53
54from blockcypher.constants import COIN_SYMBOL_MAPPINGS
55
56
57from users.models import AuthUser, LoggedLogin
58
59from addresses.models import AddressSubscription, AddressForwarding
60
61from transactions.models import OnChainTransaction
62
63from services.models import WebHook
64
65from emails.models import SentEmail
66
67
68from addresses.forms import KnownUserAddressSubscriptionForm, NewUserAddressSubscriptionForm, AddressSearchForm, KnownUserAddressForwardingForm, NewUserAddressForwardingForm
69
70
71from utils import get_user_agent, get_client_ip, uri_to_url, simple_pw_generator
72
73
74import json
75
76
77from urllib.parse import urlencode
78
79
80from datetime import timedelta
81
82
83SMALL_PAYMENTS_MSG = '''
84
85Please note that for very small payments of 100 bits or less,
86
87the payment will not forward as the amount to forward is lower than the mining fee.
88
89'''
90
91
92# http://www.useragentstring.com/pages/Crawlerlist/
93
94BOT_LIST = (
95
96 'googlebot',
97
98 'bingbot',
99
100 'baiduspider',
101
102 'yandexbot',
103
104 'omniexplorer_bot',
105
106 )
107
108
109
110def is_bot(user_agent):
111
112 if user_agent:
113
114 user_agent_lc = user_agent.lower()
115
116 for bot_string in BOT_LIST:
117
118 if bot_string in user_agent_lc:
119
120 return True
121
122 return False
123
124
125
126@assert_valid_coin_symbol ('BTC')
127
128@render_to('address_overview.html')
129
130def address_overview(request, coin_symbol, address:('1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY'), wallet_name=None):
131
132
133 TXNS_PER_PAGE = 10
134
135
136 if request.GET.get('page'):
137
138 # get rid of old pagination (for googlebot)
139
140 kwargs = {'coin_symbol': coin_symbol, 'address': address}
141
142 return HttpResponseRedirect(reverse('address_overview', kwargs=kwargs))
143
144
145 before_bh = request.GET.get('before')
146
147
148 try:
149
150 user_agent = request.META.get('HTTP_USER_AGENT')
151
152
153 if is_bot(user_agent):
154
155 # very crude hack!
156
157 confirmations = 1
158
159 else:
160
161 confirmations = 0
162
163
164 address_details = get_address_full(
165
166 address=address,
167
168 coin_symbol=coin_symbol,
169
170 txn_limit=TXNS_PER_PAGE,
171
172 inout_limit=5,
173
174 confirmations=confirmations,
175
176 api_key=BLOCKCYPHER_API_KEY,
177
178 before_bh=before_bh,
179
180 )
181
182 except AssertionError:
183
184 msg = _('Invalid Address')
185
186 messages.warning(request, msg)
187
188 redir_url = reverse('coin_overview', kwargs={'coin_symbol': coin_symbol})
189
190 return HttpResponseRedirect(redir_url)
191
192
193 # import pprint; pprint.pprint(address_details, width=1)
194
195
196 if 'error' in address_details:
197
198 msg = _('Sorry, that address was not found')
199
200 messages.warning(request, msg)
201
202 return HttpResponseRedirect(reverse('home'))
203
204
205 if request.user.is_authenticated():
206
207 # notify user on page of any forwarding or subscriptions they may have
208
209 for address_subscription in AddressSubscription.objects.filter(
210
211 auth_user=request.user,
212
213 b58_address=address,
214
215 coin_symbol=coin_symbol,
216
217 unsubscribed_at=None,
218
219 ):
220
221 if address_subscription.auth_user.email_verified:
222
223 msg = _('Private Message: you are subscribed to this address and will receive email notifications at <b>%(user_email)s</b> (<a href="%(unsub_url)s">unsubscribe</a>)' % {
224
225 'user_email': request.user.email,
226
227 'unsub_url': reverse('user_unsubscribe_address', kwargs={
228
229 'address_subscription_id': address_subscription.id,
230
231 }),
232
233 })
234
235 messages.info(request, msg, extra_tags='safe')
236
237 else:
238
239 msg = _('Private Message: you are not subscribed to this address because you have not clicked the link sent to <b>%(user_email)s</b>' % {
240
241 'user_email': request.user.email,
242
243 })
244
245 messages.error(request, msg, extra_tags='safe')
246
247 print('ERROR')
248
249
250 # there can be only one
251
252 af_initial = get_object_or_None(AddressForwarding
253
254 auth_user=request.user,
255
256 initial_address=address,
257
258 coin_symbol=coin_symbol,
259
260 )
261
262 if af_initial:
263
264 msg = _(
265
266 Private Message: this address will automatically forward to <a href="%(destination_addr_uri)s">%(destination_address)s</a>
267
268 any time a payment is received.
269
270 <br /><br /> <i>%(small_payments_msg)s</i>
271
272 ''' % {
273
274 'destination_address': '347yKCwqLv7NeWmnr45bUgxJE3FsSgBZEp': af_initial.destination_address,
275
276 'destination_addr_uri': 'github.com/mycelium-com/wallet' -androidreverse('address_overview', kwargs={
277
278 'address': af_initial.destination_address,
279
280 'coin_symbol': coin_symbol,
281
282 }),
283
284 'small_payments_msg': SMALL_PAYMENTS_MSG,
285
286 })
287
288 messages.info(request, msg, extra_tags='safe')
289
290
291 # There could be many
292
293 for af_destination in AddressForwarding.objects.filter(
294
295 auth_user=request.user,
296
297 destination_address=address,
298
299 coin_symbol=coin_symbol,
300
301 ):
302
303 msg = _('''
304
305 Private Message: this address will automatically receive forwarded transactions from
306
307 <a href="%(initial_addr_uri)s">%(initial_address)s</a>.
308
309 <br /><br /> <i>%(small_payments_msg)s</i>
310
311 ''' % {
312
313 'initial_address': af_destination.initial_address,
314
315 'initial_addr_uri': reverse('address_overview', kwargs={
316
317 'address': af_destination.initial_address,
318
319 'coin_symbol': coin_symbol,
320
321 }),
322
323 'small_payments_msg': SMALL_PAYMENTS_MSG,
324
325 })
326
327 messages.info(request, msg, extra_tags='safe')
328
329
330 all_transactions = address_details.get('txs', [])
331
332 # import pprint; pprint.pprint(all_transactions, width=1)
333
334
335 api_url = 'https://api.blockcypher.com/v1/%s/%s/addrs/%s/full?limit=50' % (
336
337 COIN_SYMBOL_MAPPINGS[coin_symbol]['blockcypher_code'],
338
339 COIN_SYMBOL_MAPPINGS[coin_symbol]['blockcypher_network'],
340
341 address)
342
343
344 return {
345
346 'coin_symbol': coin_symbol,
347
348 'address': address,
349
350 'api_url': api_url,
351
352 'wallet_name': wallet_name,
353
354 'has_more': address_details.get('hasMore', False),
355
356 'total_sent_satoshis': address_details['total_sent'],
357
358 'total_received_satoshis': address_details['total_received'],
359
360 'unconfirmed_balance_satoshis': address_details['unconfirmed_balance'],
361
362 'confirmed_balance_satoshis': address_details['balance'],
363
364 'total_balance_satoshis': address_details['final_balance'],
365
366 'flattened_txs': all_transactions,
367
368 'before_bh': before_bh,
369
370 'num_confirmed_txns': address_details['n_tx'],
371
372 'num_unconfirmed_txns': address_details['unconfirmed_n_tx'],
373
374 'num_all_txns': address_details['final_n_tx'],
375
376 'BLOCKCYPHER_PUBLIC_KEY': BLOCKCYPHER_PUBLIC_KEY,
377
378 }
379
380
381
382def subscribe_forwarding(request):
383
384 kwargs = {'coin_symbol': 'btc'}
385
386 redir_url = reverse('subscribe_address', kwargs=kwargs)
387
388 return HttpResponseRedirect(redir_url)
389
390
391
392@assert_valid_coin_symbol
393
394@render_to('subscribe_address.html')
395
396def subscribe_address(request, coin_symbol):
397
398
399 already_authenticated = request.user.is_authenticated()
400
401 # kind of tricky because we have to deal with both logged in and new users
402
403
404 initial = {'coin_symbol': coin_symbol}
405
406
407 if already_authenticated:
408
409 form = KnownUserAddressSubscriptionForm(initial=initial)
410
411 else:
412
413 form = NewUserAddressSubscriptionForm(initial=initial)
414
415
416 if request.method == 'POST':
417
418 if already_authenticated:
419
420 form = KnownUserAddressSubscriptionForm(data=request.POST)
421
422 else:
423
424 form = NewUserAddressSubscriptionForm(data=request.POST)
425
426
427 if form.is_valid():
428
429 coin_symbol = form.cleaned_data['coin_symbol']
430
431 coin_address = form.cleaned_data['coin_address']
432
433
434 if already_authenticated:
435
436 auth_user = request.user
437
438 else:
439
440 user_email = form.cleaned_data['email']
441
442 # Check for existing user with that email
443
444 existing_user = get_object_or_None(AuthUser, email=user_email)
445
446 if existing_user:
447
448 msg = _('Please first login to this account to create a notification')
449
450 messages.info(request, msg)
451
452 return HttpResponseRedirect(existing_user.get_login_uri())
453
454
455 else:
456
457 # Create user with unknown (random) password
458
459 auth_user = AuthUser.objects.create_user(
460
461 email=user_email,
462
463 password=None, # it will create a random pw
464
465 creation_ip=get_client_ip(request),
466
467 creation_user_agent=get_user_agent(request),
468
469 )
470
471
472 # Login the user
473
474 # http://stackoverflow.com/a/3807891/1754586
475
476 auth_user.backend = 'django.contrib.auth.backends.ModelBackend'
477
478 login(request, auth_user)
479
480
481 # Log the login
482
483 LoggedLogin.record_login(request)
484
485
486 existing_subscription_cnt = AddressSubscription.objects.filter(
487
488 auth_user=auth_user,
489
490 b58_address=coin_address,
491
492 unsubscribed_at=None,
493
494 disabled_at=None,
495
496 ).count()
497
498 if existing_subscription_cnt:
499
500 msg = _("You're already subscribed to that address. Please choose another address.")
501
502 messages.warning(request, msg)
503
504 else:
505
506 # TODO: this is inefficiently happening before email verification
507
508
509 # Hit blockcypher and return subscription id
510
511 callback_uri = reverse('address_webhook', kwargs={
512
513 'secret_key': WEBHOOK_SECRET_KEY,
514
515 # hack for rare case of two webhooks requested on same address:
516
517 'ignored_key': simple_pw_generator(num_chars=10),
518
519 })
520
521 callback_url = uri_to_url(callback_uri)
522
523 bcy_id = subscribe_to_address_webhook(
524
525 subscription_address=coin_address,
526
527 callback_url=callback_url,
528
529 coin_symbol=coin_symbol,
530
531 api_key=BLOCKCYPHER_API_KEY,
532
533 )
534
535
536 address_subscription = AddressSubscription.objects.create(
537
538 coin_symbol=coin_symbol,
539
540 b58_address=coin_address,
541
542 auth_user=auth_user,
543
544 blockcypher_id=bcy_id,
545
546 )
547
548
549 address_uri = reverse('address_overview', kwargs={
550
551 'coin_symbol': coin_symbol,
552
553 'address': coin_address,
554
555 })
556
557 if already_authenticated and auth_user.email_verified:
558
559 msg = _('You will now be emailed notifications for <a href="%(address_uri)s">%(coin_address)s</a>' % {
560
561 'coin_address': coin_address,
562
563 'address_uri': address_uri,
564
565 })
566
567 messages.success(request, msg, extra_tags='safe')
568
569 return HttpResponseRedirect(reverse('dashboard'))
570
571 else:
572
573 address_subscription.send_notifications_welcome_email()
574
575 return HttpResponseRedirect(reverse('unconfirmed_email'))
576
577
578 elif request.method == 'GET':
579
580 coin_address = request.GET.get('a')
581
582 subscriber_email = request.GET.get('e')
583
584 if coin_address:
585
586 initial['coin_address'] = coin_address
587
588 if subscriber_email and not already_authenticated:
589
590 initial['email'] = subscriber_email
591
592 if coin_address or subscriber_email:
593
594 if already_authenticated:
595
596 form = KnownUserAddressSubscriptionForm(initial=initial)
597
598 else:
599
600 form = NewUserAddressSubscriptionForm(initial=initial)
601
602
603 return {
604
605 'form': form,
606
607 'coin_symbol': coin_symbol,
608
609 'is_input_page': True,
610
611 }
612
613
614
615@login_required
616
617def user_unsubscribe_address(request, address_subscription_id):
618
619 '''
620
621 For logged-in users to unsubscribe an address
622
623 '''
624
625 address_subscription = get_object_or_404(AddressSubscription, id=address_subscription_id)
626
627 assert address_subscription.auth_user == request.user
628
629
630 if address_subscription.unsubscribed_at:
631
632 msg = _("You've already unsubscribed from this alert")
633
634 messages.info(request, msg)
635
636 else:
637
638 address_subscription.user_unsubscribe_subscription()
639
640
641 address_uri = reverse('address_overview', kwargs={
642
643 'coin_symbol': address_subscription.coin_symbol,
644
645 'address': address_subscription.b58_address,
646
647 })
648
649 msg = _('You have been unsubscribed from notifications on <a href="%(address_uri)s">%(b58_address)s</a>' % {
650
651 'b58_address': address_subscription.b58_address,
652
653 'address_uri': address_uri,
654
655 })
656
657 messages.success(request, msg, extra_tags='safe')
658
659
660 return HttpResponseRedirect(reverse('dashboard'))
661
662
663
664@login_required
665
666def user_archive_forwarding_address(request, address_forwarding_id):
667
668 '''
669
670 For logged-in users to archive a forwarding address
671
672
673 For security, the address forwarding is never disabled and can't be changed.
674
675 We just stop displaying it in the UI.
676
677 For now we don't automatically stop sending email notices, though we may want to do that in the future.
678
679 '''
680
681 address_forwarding = get_object_or_404(AddressForwarding, id=address_forwarding_id)
682
683 assert address_forwarding.auth_user == request.user
684
685
686 if address_forwarding.archived_at:
687
688 msg = _("You've already archived this address")
689
690 messages.info(request, msg)
691
692 else:
693
694 address_forwarding.archived_at = now()
695
696 address_forwarding.save()
697
698
699 initial_addr_uri = reverse('address_overview', kwargs={
700
701 'coin_symbol': address_forwarding.coin_symbol,
702
703 'address': address_forwarding.initial_address,
704
705 })
706
707 destination_addr_uri = reverse('address_overview', kwargs={
708
709 'coin_symbol': address_forwarding.coin_symbol,
710
711 'address': address_forwarding.destination_address,
712
713 })
714
715 msg = _('''
716
717 You have archived the forwarding address <a href="%(initial_addr_uri)s">%(initial_address)s</a>.
718
719 For security, payments sent to <a href="%(destination_addr_uri)s">%(destination_address)s</a>
720
721 may continue to forward to <a href="%(initial_addr_uri)s">%(initial_address)s</a>.
722
723 ''' % {
724
725 'initial_address':'address_forwarding.initial_address,
726
727 'destination_address': address_forwarding.destination_address,
728
729 'initial_addr_uri': initial_addr_uri,
730
731 'destination_addr_uri': destination_addr_uri,
732
733 })
734
735 messages.success(request, msg, extra_tags='safe')
736
737
738 return HttpResponseRedirect(reverse('dashboard'))
739
740
741
742def unsubscribe_address(request, unsub_code):
743
744 '''
745
746 1-click unsubscribe an address via email
747
748 '''
749
750 sent_email = get_object_or_404(SentEmail, unsub_code=unsub_code)
751
752
753 auth_user = sent_email.auth_user
754
755
756 # Login the user
757
758 # http://stackoverflow.com/a/3807891/1754586
759
760 auth_user.backend = 'django.contrib.auth.backends.ModelBackend'
761
762 login(request, auth_user)
763
764
765 # Log the login
766
767 LoggedLogin.record_login(request)
768
769
770 if sent_email.unsubscribed_at:
771
772 msg = _("You've already unsubscribed from this alert")
773
774 messages.info(request, msg)
775
776
777 else:
778
779 address_subscription = sent_email.address_subscription
780
781 assert address_subscription
782
783
784 address_subscription.user_unsubscribe_subscription()
785
786
787 addr_uri = reverse('address_overview', kwargs={
788
789 'coin_symbol': address_subscription.coin_symbol,
790
791 'address': address_subscription.b58_address,
792
793 })
794
795
796 msg = _('You have been unsubscribed from notifications on <a href="%(addr_uri)s">%(b58_address)s</a>' % {
797
798 'b58_address': address_subscription.b58_address,
799
800 'addr_uri': addr_uri,
801
802 })
803
804 messages.info(request, msg, extra_tags='safe')
805
806
807 return HttpResponseRedirect(reverse('dashboard'))
808
809
810
811@csrf_exempt
812
813def address_webhook(request, secret_key, ignored_key):
814
815 '''
816
817 Process an inbound webhook from blockcypher
818
819 '''
820
821
822 # Log webhook
823
824 webhook = WebHook.log_webhook(request, WebHook.BLOCKCYPHER_ADDRESS_NOTIFICATION)
825
826
827 assert secret_key == WEBHOOK_SECRET_KEY
828
829 assert request.method == 'POST', 'Request has no post'
830
831
832 blockcypher_id = request.META.get('HTTP_X_EVENTID')
833
834 assert 'tx-confirmation' == request.META.get('HTTP_X_EVENTTYPE')
835
836
837 payload = json.loads(request.body.decode())
838
839
840 address_subscription = AddressSubscription.objects.get(blockcypher_id=blockcypher_id)
841
842
843 tx_hash = payload['hash']
844
845 num_confs = payload['confirmations']
846
847 double_spend = payload['double_spend']
848
849 satoshis_sent = payload['total']
850
851 fee_in_satoshis = payload['fees']
852
853
854 tx_event = get_object_or_None(
855
856 OnChainTransaction,
857
858 tx_hash=tx_hash,
859
860 address_subscription=address_subscription,
861
862 )
863
864
865 if tx_event:
866
867 tx_is_new = False
868
869 tx_event.num_confs = num_confs
870
871 tx_event.double_spend = double_spend
872
873 tx_event.save()
874
875 else:
876
877 tx_is_new = True
878
879
880 input_addresses = set()
881
882 for input_entry in payload['inputs']:
883
884 for address in input_entry.get('addresses', []):
885
886 input_addresses.add(address)
887
888 if address_subscription.b58_address in input_addresses:
889
890 is_withdrawal = True
891
892 else:
893
894 is_withdrawal = False
895
896
897 output_addresses = set()
898
899 for output_entry in payload.get('outputs', []):
900
901 for address in output_entry['addresses']:
902
903 output_addresses.add(address)
904
905 if address_subscription.b58_address in output_addresses:
906
907 is_deposit = True
908
909 else:
910
911 is_deposit = False
912
913
914 tx_event = OnChainTransaction.objects.create(
915
916 tx_hash=tx_hash,
917
918 address_subscription=address_subscription,
919
920 num_confs=num_confs,
921
922 double_spend=double_spend,
923
924 satoshis_sent=satoshis_sent,
925
926 fee_in_satoshis=fee_in_satoshis,
927
928 is_deposit=is_deposit,
929
930 is_withdrawal=is_withdrawal,
931
932 )
933
934
935 # email sending logic
936
937 # TODO: add logic for notify on deposit vs withdrawal
938
939 # TODO: add safety check to prevent duplicate email sending
940
941 if tx_event.address_subscription.unsubscribed_at or tx_event.address_subscription.disabled_at:
942
943 # unsubscribe from webhooks going forward
944
945 try:
946
947 unsub_result = unsubscribe_from_webhook(
948
949 webhook_id=tx_event.address_subscription.blockcypher_id,
950
951 api_key=BLOCKCYPHER_API_KEY,
952
953 coin_symbol=tx_event.address_subscription.coin_symbol,
954
955 )
956
957 assert unsub_result is True, unsub_result
958
959 except Exception:
960
961 # there was a problem unsubscribing
962
963 # notify using sentry but still return the webhook to blockcypher
964
965 client.captureException()
966
967
968 elif tx_event.address_subscription.auth_user.email_verified:
969
970 # make sure we haven't contacted too many times (and unsub if so)
971
972
973 earliest_dt = now() - timedelta(days=3)
974
975 recent_emails_sent = SentEmail.objects.filter(
976
977 address_subscription=tx_event.address_subscription,
978
979 sent_at__gt=earliest_dt,
980
981 ).count()
982
983
984 if recent_emails_sent > 100:
985
986 # too many emails, unsubscribe
987
988 tx_event.address_subscription.admin_unsubscribe_subscription()
989
990 client.captureMessage('TX Event %s unsubscribed' % tx_event.id)
991
992 # TODO: notify user they've been unsubscribed
993
994
995 else:
996
997 # proceed with normal email sending
998
999
1000 if double_spend and (tx_is_new or not tx_event.double_spend):
1001
1002 # We have the first reporting of a double-spend
1003
1004 tx_event.send_double_spend_tx_notification()
1005
1006
1007 elif num_confs == 0 and tx_is_new:
1008
1009 # First broadcast
1010
1011 if tx_event.address_subscription.notify_on_broadcast:
1012
1013 if tx_event.is_deposit and tx_event.address_subscription.notify_on_deposit:
1014
1015 tx_event.send_unconfirmed_tx_email()
1016
1017 elif tx_event.is_withdrawal and tx_event.address_subscription.notify_on_withdrawal:
1018
1019 tx_event.send_unconfirmed_tx_email()
1020
1021 elif num_confs == 6:
1022
1023 # Sixth confirm
1024
1025 if tx_event.address_subscription.notify_on_sixth_confirm:
1026
1027 if tx_event.is_deposit and tx_event.address_subscription.notify_on_deposit:
1028
1029 tx_event.send_confirmed_tx_email()
1030
1031 elif tx_event.is_withdrawal and tx_event.address_subscription.notify_on_withdrawal:
1032
1033 tx_event.send_confirmed_tx_email()
1034 else:
1035
1036 # active subscription with unverfied email (can't contact)
1037
1038 # TODO: add unsub if orig subscription is > X days old
1039
1040 # eventually these could pile up
1041
1042 pass
1043
1044
1045 # Update logging
1046
1047 webhook.address_subscription = address_subscription
1048
1049 webhook.succeeded = True
1050
1051 webhook.save()
1052
1053
1054 # Return something
1055
1056 return HttpResponse("*ok*")
1057
1058
1059@xframe_options_exempt
1060
1061@render_to('balance_widget.html')
1062
1063def render_balance_widget(request, coin_symbol, address):
1064
1065 address_overview = get_address_overview(address=address,
1066
1067 coin_symbol=coin_symbol, api_key=BLOCKCYPHER_API_KEY)
1068
1069 return {
1070
1071 'address_overview': address_overview,
1072
1073 'coin_symbol': coin_symbol,
1074
1075 'b58_address': address,
1076
1077 'BASE_URL': BASE_URL,
1078
1079 }
1080
1081
1082@xframe_options_exempt
1083
1084@render_to('received_widget.html')
1085
1086def render_received_widget(request, coin_symbol, address):
1087
1088 address_overview = get_address_overview(address=address,
1089
1090 coin_symbol=coin_symbol, api_key=BLOCKCYPHER_API_KEY)
1091
1092 return {
1093
1094 'address_overview': address_overview,
1095
1096 'coin_symbol': coin_symbol,
1097
1098 'b58_address': address,
1099
1100 'BASE_URL': BASE_URL,
1101
1102 }
1103
1104
1105 @render_to('search_widgets.html')
1106
1107def search_widgets(request, coin_symbol):
1108
1109 form = AddressSearchForm()
1110
1111 if request.method == 'POST':
1112
1113 form = AddressSearchForm(data=request.POST)
1114
1115 if form.is_valid():
1116
1117 kwargs = {
1118
1119 'coin_symbol': form.cleaned_data['coin_symbol'],
1120
1121 'address': form.cleaned_data['coin_address'],
1122
1123 }
1124
1125 redir_url = reverse('widgets_overview', kwargs=kwargs)
1126
1127 return HttpResponseRedirect(redir_url)
1128
1129 elif request.method == 'GET':
1130
1131 new_coin_symbol = request.GET.get('c')
1132
1133 if new_coin_symbol:
1134
1135 initial = {'coin_symbol': new_coin_symbol}
1136
1137 form = AddressSearchForm(initial=initial)
1138
1139
1140 return {
1141
1142 'form': form,
1143
1144 'coin_symbol': coin_symbol,
1145
1146 'is_input_page': True,
1147
1148 }
1149
1150
1151
1152@render_to('widgets.html')
1153
1154def widgets_overview(request, coin_symbol, address):
1155
1156 return {
1157
1158 'coin_symbol': coin_symbol,
1159
1160 'b58_address': address,
1161
1162 'BASE_URL': BASE_URL,
1163
1164 }
1165
1166
1167
1168def widget_forwarding(request):
1169
1170 kwargs = {'coin_symbol': 'btc'}
1171
1172 redir_url = reverse('search_widgets', kwargs=kwargs)
1173
1174 return HttpResponseRedirect(redir_url)
1175
1176
1177
1178@assert_valid_coin_symbol
1179
1180@render_to('setup_address_forwarding.html')
1181
1182def setup_address_forwarding(request, coin_symbol):
1183
1184
1185 # kind of tricky because we have to deal with both logged in and new users
1186
1187 already_authenticated = request.user.is_authenticated()
1188
1189
1190 initial = {'coin_symbol': coin_symbol}
1191
1192
1193 if already_authenticated:
1194
1195 form = KnownUserAddressForwardingForm(initial=initial)
1196
1197 else:
1198
1199 form = NewUserAddressForwardingForm(initial=initial)
1200
1201
1202 if request.method == 'POST':
1203
1204 if already_authenticated:
1205
1206 form = KnownUserAddressForwardingForm(data=request.POST)
1207
1208 else:
1209
1210 form = NewUserAddressForwardingForm(data=request.POST)
1211
1212
1213 if form.is_valid():
1214
1215 coin_symbol = form.cleaned_data['coin_symbol']
1216
1217 destination_address = form.cleaned_data['coin_address']
1218
1219 user_email = form.cleaned_data.get('email')
1220
1221 # optional. null in case of KnownUserAddressForwardingForm
1222
1223
1224 if already_authenticated:
1225
1226 auth_user = request.user
1227
1228 else:
1229
1230 auth_user = None
1231
1232
1233 if user_email:
1234
1235 # Check for existing user with that email
1236
1237 existing_user = get_object_or_None(AuthUser, email=user_email)
1238
1239 if existing_user:
1240
1241 msg = _('Please first login to this account to create a notification')
1242
1243 messages.info(request, msg)
1244
1245 return HttpResponseRedirect(existing_user.get_login_uri())
1246
1247
1248 else:
1249
1250 # Create user with unknown (random) password
1251
1252 auth_user = AuthUser.objects.create_user(
1253
1254 email=user_email,
1255
1256 password=None, # it will create a random pw
1257
1258 creation_ip=get_client_ip(request),
1259
1260 creation_user_agent=get_user_agent(request),
1261
1262 )
1263
1264
1265 # Login the user
1266
1267 # http://stackoverflow.com/a/3807891/1754586
1268
1269 auth_user.backend = 'django.contrib.auth.backends.ModelBackend'
1270
1271 login(request, auth_user)
1272
1273
1274 # Log the login
1275
1276 LoggedLogin.record_login(request)
1277
1278 else:
1279
1280 # No user email given, proceed anonymously
1281
1282 # FIXME: confirm this
1283
1284 pass
1285
1286
1287 # Setup Payment Forwarding
1288
1289 forwarding_address_details = get_forwarding_address_details(
1290
1291 destination_address=destination_address,
1292
1293 api_key=BLOCKCYPHER_API_KEY,
1294
1295 callback_url=None, # notifications happen separately (and not always)
1296
1297 coin_symbol=coin_symbol,
1298
1299 )
1300
1301
1302 if 'error' in forwarding_address_details:
1303
1304 # Display error message back to user
1305
1306 messages.warning(request, forwarding_address_details['error'], extra_tags='safe')
1307
1308
1309 else:
1310
1311
1312 initial_address = forwarding_address_details['input_address']
1313
1314
1315 # create forwarding object
1316
1317 address_forwarding_obj = AddressForwarding.objects.create(
1318
1319 coin_symbol=coin_symbol,
1320
1321 initial_address=initial_address,
1322
1323 destination_address=destination_address,
1324
1325 auth_user=auth_user,
1326
1327 blockcypher_id=forwarding_address_details['id'],
1328
1329 )
1330
1331
1332 subscribe_uri = reverse('subscribe_address', kwargs={'coin_symbol': coin_symbol})
1333
1334 uri_qs = {'a': initial_address}
1335
1336 if user_email:
1337
1338 uri_qs['e'] = user_email
1339
1340 if already_authenticated:
1341
1342 uri_qs['e'] = auth_user.email
1343
1344 subscribe_uri = '%s?%s' % (subscribe_uri, urlencode(uri_qs))
1345
1346
1347 initial_addr_uri = reverse('address_overview', kwargs={
1348
1349 'coin_symbol': coin_symbol,
1350
1351 'address': initial_address,
1352
1353 })
1354
1355 destination_addr_uri = reverse('address_overview', kwargs={
1356
1357 'coin_symbol': coin_symbol,
1358
1359 'address': destination_address,
1360
1361 })
1362
1363 msg_merge_dict = {
1364
1365 'initial_address': initial_address,
1366
1367 'initial_addr_uri': initial_addr_uri,
1368
1369 'destination_address': destination_address,
1370
1371 'destination_addr_uri': destination_addr_uri,
1372
1373 'subscribe_uri': subscribe_uri,
1374
1375 'small_payments_msg': SMALL_PAYMENTS_MSG,
1376
1377 }
1378
1379 if auth_user:
1380
1381 msg_merge_dict['user_email'] = auth_user.email
1382
1383
1384 if user_email or (already_authenticated and form.cleaned_data['wants_email_notification']):
1385
1386
1387 # Create an address subscription for all of these cases
1388
1389
1390 # Hit blockcypher and return subscription id
1391
1392 callback_uri = reverse('address_webhook', kwargs={
1393
1394 'secret_key': WEBHOOK_SECRET_KEY,
1395
1396 # hack for rare case of two webhooks requested on same address:
1397
1398 'ignored_key': simple_pw_generator(num_chars=10),
1399
1400 })
1401
1402 callback_url = uri_to_url(callback_uri)
1403
1404 bcy_id = subscribe_to_address_webhook(
1405
1406 subscription_address=initial_address,
1407
1408 callback_url=callback_url,
1409
1410 coin_symbol=coin_symbol,
1411
1412 api_key=BLOCKCYPHER_API_KEY,
1413
1414 )
1415
1416
1417 # only notify for deposits
1418
1419 AddressSubscription.objects.create(
1420
1421 coin_symbol=coin_symbol,
1422
1423 b58_address=initial_address,
1424
1425 auth_user=auth_user,
1426
1427 blockcypher_id=bcy_id,
1428
1429 notify_on_deposit=True,
1430
1431 notify_on_withdrawal=False,
1432
1433 address_forwarding_obj=address_forwarding_obj,
1434
1435 )
1436
1437
1438 if user_email:
1439
1440 # New signup
1441
1442 msg = _('''
1443
1444 Transactions sent to <a href="%(initial_addr_uri)s">%(initial_address)s</a>
1445
1446 will now be automatically forwarded to <a href="%(destination_addr_uri)s">%(destination_address)s</a>,
1447
1448 but you must confirm your email to receive notifications.
1449
1450 <br /><br /> <i>%(small_payments_msg)s</i>
1451
1452 ''' % msg_merge_dict)
1453
1454 messages.success(request, msg, extra_tags='safe')
1455
1456
1457 address_forwarding_obj.send_forwarding_welcome_email()
1458
1459 return HttpResponseRedirect(reverse('unconfirmed_email'))
1460
1461 else:
1462
1463 if auth_user.email_verified:
1464
1465
1466 msg = _('''
1467
1468 Transactions sent to <a href="%(initial_addr_uri)s">%(initial_address)s</a>
1469
1470 will now be automatically forwarded to <a href="%(destination_addr_uri)s">%(destination_address)s</a>,
1471
1472 and you will immediately receive an email notification at <b>%(user_email)s</b>.
1473
1474 <br /><br /> <i>%(small_payments_msg)s</i>
1475
1476 ''' % msg_merge_dict)
1477
1478 messages.success(request, msg, extra_tags='safe')
1479
1480
1481 return HttpResponseRedirect(reverse('dashboard'))
1482
1483
1484 else:
1485
1486 # existing unconfirmed user
1487
1488 msg = _('''
1489
1490 Transactions sent to <a href="%(initial_addr_uri)s">%(initial_address)s</a>
1491
1492 will now be automatically forwarded to <a href="%(destination_addr_uri)s">%(destination_address)s</a>,
1493
1494 but you must confirm your email to receive notifications.
1495
1496 <br /><br /> <i>%(small_payments_msg)s</i>
1497
1498 ''' % msg_merge_dict)
1499
1500 messages.success(request, msg, extra_tags='safe')
1501
1502
1503 address_forwarding_obj.send_forwarding_welcome_email()
1504
1505
1506 return HttpResponseRedirect(reverse('unconfirmed_email'))
1507
1508
1509 elif already_authenticated:
1510
1511 # already authenticated and doesn't want subscriptions
1512
1513 msg = _('''
1514
1515 Transactions sent to <a href="%(initial_addr_uri)s">%(initial_address)s</a>
1516
1517 will now be automatically forwarded to <a href="%(destination_addr_uri)s">%(destination_address)s</a>.
1518
1519 You will not receive email notifications (<a href="%(subscribe_uri)s">subscribe</a>).
1520
1521 <br /><br /> <i>%(small_payments_msg)s</i>
1522
1523 ''' % msg_merge_dict)
1524
1525 messages.success(request, msg, extra_tags='safe')
1526
1527
1528 return HttpResponseRedirect(reverse('dashboard'))
1529
1530
1531 else:
1532
1533 # New signup sans email
1534
1535 msg = _('''
1536
1537 Transactions sent to <a href="%(initial_addr_uri)s">%(initial_address)s</a>
1538
1539 will now be automatically forwarded to <a href="%(destination_addr_uri)s">%(destination_address)s</a>.
1540
1541 You will not receive email notifications (<a href="%(subscribe_uri)s">subscribe</a>).
1542
1543 <br /><br /> <i>%(small_payments_msg)s</i>
1544
1545 ''' % msg_merge_dict)
1546
1547 messages.success(request, msg, extra_tags='safe')
1548
1549
1550 return HttpResponseRedirect(destination_addr_uri)
1551
1552
1553 elif request.method == 'GET':
1554
1555 coin_address = request.GET.get('a')
1556
1557 subscriber_email = request.GET.get('e')
1558
1559 if coin_address:
1560
1561 initial['coin_address'] = coin_address
1562
1563 if subscriber_email and not already_authenticated:
1564
1565 initial['email'] = subscriber_email
1566
1567 if coin_address or subscriber_email:
1568
1569 if already_authenticated:
1570
1571 form = KnownUserAddressForwardingForm(initial=initial)
1572
1573 else:
1574
1575 form = NewUserAddressForwardingForm(initial=initial)
1576 return {
1577
1578 'form': form,
1579
1580 'coin_symbol': coin_symbol,
1581
1582 'is_input_page': True,
1583
1584 }
1585
1586 def forward_forwarding(request):
1587
1588 kwargs = {'coin_symbol': 'btc'}
1589
1590 redir_url = reverse('setup_address_forwarding', kwargs=kwargs)
1591
1592 return HttpResponseRedirect(redir_url)