4 import PyQt4.QtCore as QtCore
14 from urllib import urlencode
16 from PyQt4.QtGui import *
17 from PyQt4.QtCore import *
18 from PyQt4.QtWebKit import QWebView
20 from electrum import BasePlugin
21 from electrum.i18n import _, set_language
22 from electrum.util import user_dir
23 from electrum.util import appdata_dir
24 from electrum.util import format_satoshis
25 from electrum_gui.qt import ElectrumGui
27 SATOSHIS_PER_BTC = float(100000000)
28 COINBASE_ENDPOINT = 'https://coinbase.com'
29 CERTS_PATH = appdata_dir() + '/certs/ca-coinbase.crt'
31 REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
32 TOKEN_URI = 'https://coinbase.com/oauth/token'
33 CLIENT_ID = '0a930a48b5a6ea10fb9f7a9fec3d093a6c9062ef8a7eeab20681274feabdab06'
34 CLIENT_SECRET = 'f515989e8819f1822b3ac7a7ef7e57f755c9b12aee8f22de6b340a99fd0fd617'
35 # Expiry is stored in RFC3339 UTC format
36 EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
38 class Plugin(BasePlugin):
40 def fullname(self): return 'Coinbase BuyBack'
42 def description(self): return 'After sending bitcoin, prompt the user with the option to rebuy them via Coinbase.\n\nMarcell Ortutay, 1FNGQvm29tKM7y3niq63RKi7Qbg7oZ3jrB'
44 def __init__(self, gui, name):
45 BasePlugin.__init__(self, gui, name)
46 self._is_available = self._init()
51 def is_available(self):
52 return self._is_available
55 return BasePlugin.enable(self)
57 def receive_tx(self, tx, wallet):
58 domain = wallet.get_account_addresses(None)
59 is_relevant, is_send, v, fee = tx.get_value(domain, wallet.prevout_values)
60 if isinstance(self.gui, ElectrumGui):
62 web = propose_rebuy_qt(abs(v))
63 except OAuth2Exception as e:
64 rm_local_oauth_credentials()
65 # TODO(ortutay): android flow
68 def propose_rebuy_qt(amount):
71 box.setFixedSize(200, 200)
73 credentials = read_local_oauth_credentials()
74 questionText = _('Rebuy ') + format_satoshis(amount) + _(' BTC?')
77 if credentials and not credentials.invalid:
78 credentials.store_locally()
79 totalPrice = get_coinbase_total_price(credentials, amount)
80 questionText += _('\n(Price: ') + totalPrice + _(')')
82 if not question(box, questionText):
86 do_buy(credentials, amount)
88 do_oauth_flow(web, amount)
91 def do_buy(credentials, amount):
92 h = httplib2.Http(ca_certs=CERTS_PATH)
93 h = credentials.authorize(h)
95 'qty': float(amount)/SATOSHIS_PER_BTC,
96 'agree_btc_amount_varies': False
98 resp, content = h.request(
99 COINBASE_ENDPOINT + '/api/v1/buys', 'POST', urlencode(params))
100 if resp['status'] != '200':
101 message(_('Error, could not buy bitcoin'))
103 content = json.loads(content)
104 if content['success']:
105 message(_('Success!\n') + content['transfer']['description'])
107 if content['errors']:
108 message(_('Error: ') + string.join(content['errors'], '\n'))
110 message(_('Error, could not buy bitcoin'))
112 def get_coinbase_total_price(credentials, amount):
113 h = httplib2.Http(ca_certs=CERTS_PATH)
114 params={'qty': amount/SATOSHIS_PER_BTC}
115 resp, content = h.request(COINBASE_ENDPOINT + '/api/v1/prices/buy?' + urlencode(params),'GET')
116 content = json.loads(content)
117 if resp['status'] != '200':
119 return '$' + content['total']['amount']
121 def do_oauth_flow(web, amount):
122 # QT expects un-escaped URL
123 auth_uri = step1_get_authorize_url()
124 web.load(QUrl(auth_uri))
125 web.setFixedSize(500, 700)
127 web.titleChanged.connect(lambda(title): complete_oauth_flow(title, web, amount) if re.search('^[a-z0-9]+$', title) else False)
129 def complete_oauth_flow(token, web, amount):
131 http = httplib2.Http(ca_certs=CERTS_PATH)
132 credentials = step2_exchange(str(token), http)
133 credentials.store_locally()
134 do_buy(credentials, amount)
137 dir = user_dir() + '/coinbase_buyback'
138 if not os.access(dir, os.F_OK):
140 return dir + '/token'
142 def read_local_oauth_credentials():
143 if not os.access(token_path(), os.F_OK):
145 f = open(token_path(), 'r')
149 credentials = Credentials.from_json(data)
151 except Exception as e:
154 def rm_local_oauth_credentials():
155 os.remove(token_path())
157 def step1_get_authorize_url():
158 return ('https://coinbase.com/oauth/authorize'
160 + '&redirect_uri=' + REDIRECT_URI
161 + '&response_type=code'
162 + '&client_id=' + CLIENT_ID
163 + '&access_type=offline')
165 def step2_exchange(code, http):
166 body = urllib.urlencode({
167 'grant_type': 'authorization_code',
168 'client_id': CLIENT_ID,
169 'client_secret': CLIENT_SECRET,
171 'redirect_uri': REDIRECT_URI,
175 'content-type': 'application/x-www-form-urlencoded',
178 resp, content = http.request(TOKEN_URI, method='POST', body=body,
180 if resp.status == 200:
181 d = json.loads(content)
182 access_token = d['access_token']
183 refresh_token = d.get('refresh_token', None)
185 if 'expires_in' in d:
186 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
187 seconds=int(d['expires_in']))
188 return Credentials(access_token, refresh_token, token_expiry)
190 raise OAuth2Exception(content)
192 class OAuth2Exception(Exception):
193 """An error related to OAuth2"""
195 class Credentials(object):
196 def __init__(self, access_token, refresh_token, token_expiry):
197 self.access_token = access_token
198 self.refresh_token = refresh_token
199 self.token_expiry = token_expiry
201 # Indicates a failed refresh
205 token_expiry = self.token_expiry
206 if (token_expiry and isinstance(token_expiry, datetime.datetime)):
207 token_expiry = token_expiry.strftime(EXPIRY_FORMAT)
210 'access_token': self.access_token,
211 'refresh_token': self.refresh_token,
212 'token_expiry': token_expiry,
216 def store_locally(self):
217 f = open(token_path(), 'w')
218 f.write(self.to_json())
222 def from_json(cls, s):
224 if ('token_expiry' in data
225 and not isinstance(data['token_expiry'], datetime.datetime)):
227 data['token_expiry'] = datetime.datetime.strptime(
228 data['token_expiry'], EXPIRY_FORMAT)
230 data['token_expiry'] = None
231 retval = Credentials(
232 data['access_token'],
233 data['refresh_token'],
234 data['token_expiry'])
237 def apply(self, headers):
238 headers['Authorization'] = 'Bearer ' + self.access_token
240 def authorize(self, http):
241 request_orig = http.request
243 # The closure that will replace 'httplib2.Http.request'.
244 def new_request(uri, method='GET', body=None, headers=None,
245 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
246 connection_type=None):
252 resp, content = request_orig(uri, method, body, headers,
253 redirections, connection_type)
254 if resp.status == 401:
255 self._refresh(request_orig)
258 return request_orig(uri, method, body, headers,
259 redirections, connection_type)
261 return (resp, content)
263 http.request = new_request
264 setattr(http.request, 'credentials', self)
268 h = httplib2.Http(ca_certs=CERTS_PATH)
270 self._refresh(h.request)
271 except OAuth2Exception as e:
272 rm_local_oauth_credentials()
276 def _refresh(self, http_request):
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,
284 'content-type': 'application/x-www-form-urlencoded',
286 resp, content = http_request(
287 TOKEN_URI, method='POST', body=body, headers=headers)
288 if resp.status == 200:
289 d = json.loads(content)
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()
297 raise OAuth2Exception('Refresh failed, ' + content)
301 box.setFixedSize(200, 200)
302 return QMessageBox.information(box, _('Message'), msg)
304 def question(widget, msg):
305 return (QMessageBox.question(
306 widget, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)