4 import PyQt4.QtCore as QtCore
15 from urllib import urlencode
17 from PyQt4.QtGui import *
18 from PyQt4.QtCore import *
20 from PyQt4.QtWebKit import QWebView
22 except ImportError as e:
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
32 SATOSHIS_PER_BTC = float(100000000)
33 COINBASE_ENDPOINT = 'https://coinbase.com'
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'
42 class Plugin(BasePlugin):
44 def fullname(self): return 'Coinbase BuyBack'
46 def description(self): return 'After sending bitcoin, prompt the user with the option to rebuy them via Coinbase.\n\nMarcell Ortutay, 1FNGQvm29tKM7y3niq63RKi7Qbg7oZ3jrB'
48 def __init__(self, gui, name):
49 BasePlugin.__init__(self, gui, name)
50 self._is_available = self._init()
55 def is_available(self):
56 return self._is_available
59 return BasePlugin.enable(self)
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):
66 web = propose_rebuy_qt(abs(v))
67 except OAuth2Exception as e:
68 rm_local_oauth_credentials()
69 # TODO(ortutay): android flow
72 def propose_rebuy_qt(amount):
75 box.setFixedSize(200, 200)
77 credentials = read_local_oauth_credentials()
78 questionText = _('Rebuy ') + format_satoshis(amount) + _(' BTC?')
81 if credentials and not credentials.invalid:
82 credentials.store_locally()
83 totalPrice = get_coinbase_total_price(credentials, amount)
84 questionText += _('\n(Price: ') + totalPrice + _(')')
86 if not question(box, questionText):
90 do_buy(credentials, amount)
92 do_oauth_flow(web, amount)
95 def do_buy(credentials, amount):
96 conn = httplib.HTTPSConnection('coinbase.com')
97 credentials.authorize(conn)
99 'qty': float(amount)/SATOSHIS_PER_BTC,
100 'agree_btc_amount_varies': False
102 resp = conn.auth_request('POST', '/api/v1/buys', urlencode(params), None)
104 if resp.status != 200:
105 message(_('Error, could not buy bitcoin'))
107 content = json.loads(resp.read())
108 if content['success']:
109 message(_('Success!\n') + content['transfer']['description'])
111 if content['errors']:
112 message(_('Error: ') + string.join(content['errors'], '\n'))
114 message(_('Error, could not buy bitcoin'))
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:
123 content = json.loads(resp.read())
124 return '$' + content['total']['amount']
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)
132 web.titleChanged.connect(lambda(title): complete_oauth_flow(title, web, amount) if re.search('^[a-z0-9]+$', title) else False)
134 def complete_oauth_flow(token, web, amount):
136 credentials = step2_exchange(str(token))
137 credentials.store_locally()
138 do_buy(credentials, amount)
141 dir = user_dir() + '/coinbase_buyback'
142 if not os.access(dir, os.F_OK):
144 return dir + '/token'
146 def read_local_oauth_credentials():
147 if not os.access(token_path(), os.F_OK):
149 f = open(token_path(), 'r')
153 credentials = Credentials.from_json(data)
155 except Exception as e:
158 def rm_local_oauth_credentials():
159 os.remove(token_path())
161 def step1_get_authorize_url():
162 return ('https://coinbase.com/oauth/authorize'
164 + '&redirect_uri=' + REDIRECT_URI
165 + '&response_type=code'
166 + '&client_id=' + CLIENT_ID
167 + '&access_type=offline')
169 def step2_exchange(code):
170 body = urllib.urlencode({
171 'grant_type': 'authorization_code',
172 'client_id': CLIENT_ID,
173 'client_secret': CLIENT_SECRET,
175 'redirect_uri': REDIRECT_URI,
179 'content-type': 'application/x-www-form-urlencoded',
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)
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)
195 raise OAuth2Exception(content)
197 class OAuth2Exception(Exception):
198 """An error related to OAuth2"""
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
206 # Indicates a failed refresh
210 token_expiry = self.token_expiry
211 if (token_expiry and isinstance(token_expiry, datetime.datetime)):
212 token_expiry = token_expiry.strftime(EXPIRY_FORMAT)
215 'access_token': self.access_token,
216 'refresh_token': self.refresh_token,
217 'token_expiry': token_expiry,
221 def store_locally(self):
222 f = open(token_path(), 'w')
223 f.write(self.to_json())
227 def from_json(cls, s):
229 if ('token_expiry' in data
230 and not isinstance(data['token_expiry'], datetime.datetime)):
232 data['token_expiry'] = datetime.datetime.strptime(
233 data['token_expiry'], EXPIRY_FORMAT)
235 data['token_expiry'] = None
236 retval = Credentials(
237 data['access_token'],
238 data['refresh_token'],
239 data['token_expiry'])
242 def apply(self, headers):
243 headers['Authorization'] = 'Bearer ' + self.access_token
245 def authorize(self, conn):
246 request_orig = conn.request
248 def new_request(method, uri, params, 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)
259 request_orig(method, uri, params, headers)
260 return conn.getresponse()
264 conn.auth_request = new_request
270 except OAuth2Exception as e:
271 rm_local_oauth_credentials()
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,
284 'content-type': 'application/x-www-form-urlencoded',
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()
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)
310 app = QApplication(sys.argv)
312 propose_rebuy_qt(int(sys.argv[1]))
313 sys.exit(app.exec_())
315 if __name__ == "__main__":