4 import PyQt4.QtCore as QtCore
15 from urllib import urlencode
17 from PyQt4.QtGui import *
18 from PyQt4.QtCore import *
19 from PyQt4.QtWebKit import QWebView
21 from electrum import BasePlugin
22 from electrum.i18n import _, set_language
23 from electrum.util import user_dir
24 from electrum.util import appdata_dir
25 from electrum.util import format_satoshis
26 from electrum_gui.qt import ElectrumGui
28 SATOSHIS_PER_BTC = float(100000000)
29 COINBASE_ENDPOINT = 'https://coinbase.com'
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 conn = httplib.HTTPSConnection('coinbase.com')
93 credentials.authorize(conn)
95 'qty': float(amount)/SATOSHIS_PER_BTC,
96 'agree_btc_amount_varies': False
98 resp = conn.auth_request('POST', '/api/v1/buys', urlencode(params), None)
100 if resp.status != 200:
101 message(_('Error, could not buy bitcoin'))
103 content = json.loads(resp.read())
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 conn = httplib.HTTPSConnection('coinbase.com')
114 params={'qty': amount/SATOSHIS_PER_BTC}
115 conn.request('GET', '/api/v1/prices/buy?' + urlencode(params))
116 resp = conn.getresponse()
117 if resp.status != 200:
119 content = json.loads(resp.read())
120 return '$' + content['total']['amount']
122 def do_oauth_flow(web, amount):
123 # QT expects un-escaped URL
124 auth_uri = step1_get_authorize_url()
125 web.load(QUrl(auth_uri))
126 web.setFixedSize(500, 700)
128 web.titleChanged.connect(lambda(title): complete_oauth_flow(title, web, amount) if re.search('^[a-z0-9]+$', title) else False)
130 def complete_oauth_flow(token, web, amount):
132 credentials = step2_exchange(str(token))
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):
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 conn = httplib.HTTPSConnection('coinbase.com')
179 conn.request('POST', TOKEN_URI, body, headers)
180 resp = conn.getresponse()
181 if resp.status == 200:
182 d = json.loads(resp.read())
183 access_token = d['access_token']
184 refresh_token = d.get('refresh_token', None)
186 if 'expires_in' in d:
187 token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
188 seconds=int(d['expires_in']))
189 return Credentials(access_token, refresh_token, token_expiry)
191 raise OAuth2Exception(content)
193 class OAuth2Exception(Exception):
194 """An error related to OAuth2"""
196 class Credentials(object):
197 def __init__(self, access_token, refresh_token, token_expiry):
198 self.access_token = access_token
199 self.refresh_token = refresh_token
200 self.token_expiry = token_expiry
202 # Indicates a failed refresh
206 token_expiry = self.token_expiry
207 if (token_expiry and isinstance(token_expiry, datetime.datetime)):
208 token_expiry = token_expiry.strftime(EXPIRY_FORMAT)
211 'access_token': self.access_token,
212 'refresh_token': self.refresh_token,
213 'token_expiry': token_expiry,
217 def store_locally(self):
218 f = open(token_path(), 'w')
219 f.write(self.to_json())
223 def from_json(cls, s):
225 if ('token_expiry' in data
226 and not isinstance(data['token_expiry'], datetime.datetime)):
228 data['token_expiry'] = datetime.datetime.strptime(
229 data['token_expiry'], EXPIRY_FORMAT)
231 data['token_expiry'] = None
232 retval = Credentials(
233 data['access_token'],
234 data['refresh_token'],
235 data['token_expiry'])
238 def apply(self, headers):
239 headers['Authorization'] = 'Bearer ' + self.access_token
241 def authorize(self, conn):
242 request_orig = conn.request
244 def new_request(method, uri, params, headers):
248 request_orig(method, uri, params, headers)
249 resp = conn.getresponse()
250 if resp.status == 401:
251 # Refresh and try again
252 self._refresh(request_orig)
255 request_orig(method, uri, params, headers)
256 return conn.getresponse()
260 conn.auth_request = new_request
266 except OAuth2Exception as e:
267 rm_local_oauth_credentials()
272 conn = httplib.HTTPSConnection('coinbase.com')
273 body = urllib.urlencode({
274 'grant_type': 'refresh_token',
275 'refresh_token': self.refresh_token,
276 'client_id': CLIENT_ID,
277 'client_secret': CLIENT_SECRET,
280 'content-type': 'application/x-www-form-urlencoded',
282 conn.request('POST', TOKEN_URI, body, headers)
283 resp = conn.getresponse()
284 if resp.status == 200:
285 d = json.loads(resp.read())
286 self.token_response = d
287 self.access_token = d['access_token']
288 self.refresh_token = d.get('refresh_token', self.refresh_token)
289 if 'expires_in' in d:
290 self.token_expiry = datetime.timedelta(
291 seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
293 raise OAuth2Exception('Refresh failed, ' + content)
297 box.setFixedSize(200, 200)
298 return QMessageBox.information(box, _('Message'), msg)
300 def question(widget, msg):
301 return (QMessageBox.question(
302 widget, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
306 app = QApplication(sys.argv)
308 propose_rebuy_qt(int(sys.argv[1]))
309 sys.exit(app.exec_())
311 if __name__ == "__main__":