04c3cff2df3a2c78a6e99b35c6cf988c4c110c5a
[electrum-nvc.git] / plugins / coinbase_buyback.py
1 import PyQt4
2 import sys
3
4 import PyQt4.QtCore as QtCore
5 import base64
6 import urllib
7 import re
8 import time
9 import os
10 import httplib
11 import datetime
12 import json
13 import string
14
15 from urllib import urlencode
16
17 from PyQt4.QtGui import *
18 from PyQt4.QtCore import *
19 try:
20     from PyQt4.QtWebKit import QWebView
21     loaded_qweb = True
22 except ImportError as e:
23     loaded_qweb = False
24
25 from electrum import BasePlugin
26 from electrum.i18n import _, set_language
27 from electrum.util import user_dir
28 from electrum.util import appdata_dir
29 from electrum.util import format_satoshis
30 from electrum_gui.qt import ElectrumGui
31
32 SATOSHIS_PER_BTC = float(100000000)
33 COINBASE_ENDPOINT = 'https://coinbase.com'
34 SCOPE = 'buy'
35 REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
36 TOKEN_URI = 'https://coinbase.com/oauth/token'
37 CLIENT_ID = '0a930a48b5a6ea10fb9f7a9fec3d093a6c9062ef8a7eeab20681274feabdab06'
38 CLIENT_SECRET = 'f515989e8819f1822b3ac7a7ef7e57f755c9b12aee8f22de6b340a99fd0fd617'
39 # Expiry is stored in RFC3339 UTC format
40 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
41
42 class Plugin(BasePlugin):
43
44     def fullname(self): return 'Coinbase BuyBack'
45
46     def description(self): return 'After sending bitcoin, prompt the user with the option to rebuy them via Coinbase.\n\nMarcell Ortutay, 1FNGQvm29tKM7y3niq63RKi7Qbg7oZ3jrB'
47
48     def __init__(self, gui, name):
49         BasePlugin.__init__(self, gui, name)
50         self._is_available = self._init()
51
52     def _init(self):
53         return loaded_qweb
54
55     def is_available(self):
56         return self._is_available
57
58     def enable(self):
59         return BasePlugin.enable(self)
60
61     def receive_tx(self, tx, wallet):
62         domain = wallet.get_account_addresses(None)
63         is_relevant, is_send, v, fee = tx.get_value(domain, wallet.prevout_values)
64         if isinstance(self.gui, ElectrumGui):
65             try:
66                 web = propose_rebuy_qt(abs(v))
67             except OAuth2Exception as e:
68                 rm_local_oauth_credentials()
69         # TODO(ortutay): android flow
70
71
72 def propose_rebuy_qt(amount):
73     web = QWebView()
74     box = QMessageBox()
75     box.setFixedSize(200, 200)
76
77     credentials = read_local_oauth_credentials()
78     questionText = _('Rebuy ') + format_satoshis(amount) + _(' BTC?')
79     if credentials:
80         credentials.refresh()
81     if credentials and not credentials.invalid:
82         credentials.store_locally()
83         totalPrice = get_coinbase_total_price(credentials, amount)
84         questionText += _('\n(Price: ') + totalPrice + _(')')
85
86     if not question(box, questionText):
87         return
88
89     if credentials:
90         do_buy(credentials, amount)
91     else:
92         do_oauth_flow(web, amount)
93     return web
94
95 def do_buy(credentials, amount):
96     conn = httplib.HTTPSConnection('coinbase.com')
97     credentials.authorize(conn)
98     params = {
99         'qty': float(amount)/SATOSHIS_PER_BTC,
100         'agree_btc_amount_varies': False
101     }
102     resp = conn.auth_request('POST', '/api/v1/buys', urlencode(params), None)
103
104     if resp.status != 200:
105         message(_('Error, could not buy bitcoin'))
106         return
107     content = json.loads(resp.read())
108     if content['success']:
109         message(_('Success!\n') + content['transfer']['description'])
110     else:
111         if content['errors']:
112             message(_('Error: ') + string.join(content['errors'], '\n'))
113         else:
114             message(_('Error, could not buy bitcoin'))
115
116 def get_coinbase_total_price(credentials, amount):
117     conn = httplib.HTTPSConnection('coinbase.com')
118     params={'qty': amount/SATOSHIS_PER_BTC}
119     conn.request('GET', '/api/v1/prices/buy?' + urlencode(params))
120     resp = conn.getresponse()
121     if resp.status != 200:
122         return 'unavailable'
123     content = json.loads(resp.read())
124     return '$' + content['total']['amount']
125
126 def do_oauth_flow(web, amount):
127     # QT expects un-escaped URL
128     auth_uri = step1_get_authorize_url()
129     web.load(QUrl(auth_uri))
130     web.setFixedSize(500, 700)
131     web.show()
132     web.titleChanged.connect(lambda(title): complete_oauth_flow(title, web, amount) if re.search('^[a-z0-9]+$', title) else False)
133
134 def complete_oauth_flow(token, web, amount):
135     web.close()
136     credentials = step2_exchange(str(token))
137     credentials.store_locally()
138     do_buy(credentials, amount)
139
140 def token_path():
141     dir = user_dir() + '/coinbase_buyback'
142     if not os.access(dir, os.F_OK):
143         os.mkdir(dir)
144     return dir + '/token'
145
146 def read_local_oauth_credentials():
147     if not os.access(token_path(), os.F_OK):
148         return None
149     f = open(token_path(), 'r')
150     data = f.read()
151     f.close()
152     try:
153         credentials = Credentials.from_json(data)
154         return credentials
155     except Exception as e:
156         return None
157
158 def rm_local_oauth_credentials():
159     os.remove(token_path())
160
161 def step1_get_authorize_url():
162     return ('https://coinbase.com/oauth/authorize'
163             + '?scope=' + SCOPE
164             + '&redirect_uri=' + REDIRECT_URI
165             + '&response_type=code'
166             + '&client_id=' + CLIENT_ID
167             + '&access_type=offline')
168
169 def step2_exchange(code):
170     body = urllib.urlencode({
171         'grant_type': 'authorization_code',
172         'client_id': CLIENT_ID,
173         'client_secret': CLIENT_SECRET,
174         'code': code,
175         'redirect_uri': REDIRECT_URI,
176         'scope': SCOPE,
177         })
178     headers = {
179         'content-type': 'application/x-www-form-urlencoded',
180     }
181
182     conn = httplib.HTTPSConnection('coinbase.com')
183     conn.request('POST', TOKEN_URI, body, headers)
184     resp = conn.getresponse()
185     if resp.status == 200:
186         d = json.loads(resp.read())
187         access_token = d['access_token']
188         refresh_token = d.get('refresh_token', None)
189         token_expiry = None
190         if 'expires_in' in d:
191             token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
192                 seconds=int(d['expires_in']))
193         return Credentials(access_token, refresh_token, token_expiry)
194     else:
195         raise OAuth2Exception(content)
196
197 class OAuth2Exception(Exception):
198     """An error related to OAuth2"""
199
200 class Credentials(object):
201     def __init__(self, access_token, refresh_token, token_expiry):
202         self.access_token = access_token
203         self.refresh_token = refresh_token
204         self.token_expiry = token_expiry
205         
206         # Indicates a failed refresh
207         self.invalid = False
208
209     def to_json(self):
210         token_expiry = self.token_expiry
211         if (token_expiry and isinstance(token_expiry, datetime.datetime)):
212             token_expiry = token_expiry.strftime(EXPIRY_FORMAT)
213         
214         d = {
215             'access_token': self.access_token,
216             'refresh_token': self.refresh_token,
217             'token_expiry': token_expiry,
218         }
219         return json.dumps(d)
220
221     def store_locally(self):
222         f = open(token_path(), 'w')
223         f.write(self.to_json())
224         f.close()
225
226     @classmethod
227     def from_json(cls, s):
228         data = json.loads(s)
229         if ('token_expiry' in data
230             and not isinstance(data['token_expiry'], datetime.datetime)):
231             try:
232                 data['token_expiry'] = datetime.datetime.strptime(
233                     data['token_expiry'], EXPIRY_FORMAT)
234             except:
235                 data['token_expiry'] = None
236         retval = Credentials(
237             data['access_token'],
238             data['refresh_token'],
239             data['token_expiry'])
240         return retval
241
242     def apply(self, headers):
243         headers['Authorization'] = 'Bearer ' + self.access_token
244
245     def authorize(self, conn):
246         request_orig = conn.request
247
248         def new_request(method, uri, params, headers):
249             if headers == None:
250                 headers = {}
251                 self.apply(headers)
252             request_orig(method, uri, params, headers)
253             resp = conn.getresponse()
254             if resp.status == 401:
255                 # Refresh and try again
256                 self._refresh(request_orig)
257                 self.store_locally()
258                 self.apply(headers)
259                 request_orig(method, uri, params, headers)
260                 return conn.getresponse()
261             else:
262                 return resp
263         
264         conn.auth_request = new_request
265         return conn
266
267     def refresh(self):
268         try:
269             self._refresh()
270         except OAuth2Exception as e:
271             rm_local_oauth_credentials()
272             self.invalid = True
273             raise e
274
275     def _refresh(self):
276         conn = httplib.HTTPSConnection('coinbase.com')
277         body = urllib.urlencode({
278             'grant_type': 'refresh_token',
279             'refresh_token': self.refresh_token,
280             'client_id': CLIENT_ID,
281             'client_secret': CLIENT_SECRET,
282         })
283         headers = {
284             'content-type': 'application/x-www-form-urlencoded',
285         }
286         conn.request('POST', TOKEN_URI, body, headers)
287         resp = conn.getresponse()
288         if resp.status == 200:
289             d = json.loads(resp.read())
290             self.token_response = d
291             self.access_token = d['access_token']
292             self.refresh_token = d.get('refresh_token', self.refresh_token)
293             if 'expires_in' in d:
294                 self.token_expiry = datetime.timedelta(
295                     seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
296         else:
297             raise OAuth2Exception('Refresh failed, ' + content)
298
299 def message(msg):
300     box = QMessageBox()
301     box.setFixedSize(200, 200)
302     return QMessageBox.information(box, _('Message'), msg)
303
304 def question(widget, msg):
305     return (QMessageBox.question(
306         widget, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
307             == QMessageBox.Yes)
308
309 def main():
310     app = QApplication(sys.argv)
311     print sys.argv[1]
312     propose_rebuy_qt(int(sys.argv[1]))
313     sys.exit(app.exec_())
314
315 if __name__ == "__main__":
316     main()