URI: 
       tMerge pull request #534 from ortutay/rmh2dep - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit e88d25a2bc0598f187109b2581fbd4d19844ee97
   DIR parent 5190cc03fd126cee641b3d482528a1950806ce47
  HTML Author: ThomasV <thomasv1@gmx.de>
       Date:   Thu,  9 Jan 2014 00:24:24 -0800
       
       Merge pull request #534 from ortutay/rmh2dep
       
       remove httplib2 dependency for coinbase buyback
       Diffstat:
         D data/certs/ca-coinbase.crt          |      44 -------------------------------
         A plugins/coinbase_buyback.py         |     312 +++++++++++++++++++++++++++++++
       
       2 files changed, 312 insertions(+), 44 deletions(-)
       ---
   DIR diff --git a/data/certs/ca-coinbase.crt b/data/certs/ca-coinbase.crt
       t@@ -1,44 +0,0 @@
       ------BEGIN CERTIFICATE-----
       -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl
       -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
       -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv
       -b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG
       -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl
       -cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi
       -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c
       -JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP
       -mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+
       -wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4
       -VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/
       -AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB
       -AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
       -BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun
       -pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC
       -dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf
       -fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm
       -NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx
       -H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe
       -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g==
       ------END CERTIFICATE-----
       ------BEGIN CERTIFICATE-----
       -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
       -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
       -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
       -QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
       -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
       -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
       -9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
       -CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
       -nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
       -43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
       -T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
       -gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
       -BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
       -TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
       -DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
       -hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
       -06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
       -PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
       -YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
       -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
       ------END CERTIFICATE-----
   DIR diff --git a/plugins/coinbase_buyback.py b/plugins/coinbase_buyback.py
       t@@ -0,0 +1,312 @@
       +import PyQt4
       +import sys
       +
       +import PyQt4.QtCore as QtCore
       +import base64
       +import urllib
       +import re
       +import time
       +import os
       +import httplib
       +import datetime
       +import json
       +import string
       +
       +from urllib import urlencode
       +
       +from PyQt4.QtGui import *
       +from PyQt4.QtCore import *
       +from PyQt4.QtWebKit import QWebView
       +
       +from electrum import BasePlugin
       +from electrum.i18n import _, set_language
       +from electrum.util import user_dir
       +from electrum.util import appdata_dir
       +from electrum.util import format_satoshis
       +from electrum_gui.qt import ElectrumGui
       +
       +SATOSHIS_PER_BTC = float(100000000)
       +COINBASE_ENDPOINT = 'https://coinbase.com'
       +SCOPE = 'buy'
       +REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
       +TOKEN_URI = 'https://coinbase.com/oauth/token'
       +CLIENT_ID = '0a930a48b5a6ea10fb9f7a9fec3d093a6c9062ef8a7eeab20681274feabdab06'
       +CLIENT_SECRET = 'f515989e8819f1822b3ac7a7ef7e57f755c9b12aee8f22de6b340a99fd0fd617'
       +# Expiry is stored in RFC3339 UTC format
       +EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
       +
       +class Plugin(BasePlugin):
       +
       +    def fullname(self): return 'Coinbase BuyBack'
       +
       +    def description(self): return 'After sending bitcoin, prompt the user with the option to rebuy them via Coinbase.\n\nMarcell Ortutay, 1FNGQvm29tKM7y3niq63RKi7Qbg7oZ3jrB'
       +
       +    def __init__(self, gui, name):
       +        BasePlugin.__init__(self, gui, name)
       +        self._is_available = self._init()
       +
       +    def _init(self):
       +        return True
       +
       +    def is_available(self):
       +        return self._is_available
       +
       +    def enable(self):
       +        return BasePlugin.enable(self)
       +
       +    def receive_tx(self, tx, wallet):
       +        domain = wallet.get_account_addresses(None)
       +        is_relevant, is_send, v, fee = tx.get_value(domain, wallet.prevout_values)
       +        if isinstance(self.gui, ElectrumGui):
       +            try:
       +                web = propose_rebuy_qt(abs(v))
       +            except OAuth2Exception as e:
       +                rm_local_oauth_credentials()
       +        # TODO(ortutay): android flow
       +
       +
       +def propose_rebuy_qt(amount):
       +    web = QWebView()
       +    box = QMessageBox()
       +    box.setFixedSize(200, 200)
       +
       +    credentials = read_local_oauth_credentials()
       +    questionText = _('Rebuy ') + format_satoshis(amount) + _(' BTC?')
       +    if credentials:
       +        credentials.refresh()
       +    if credentials and not credentials.invalid:
       +        credentials.store_locally()
       +        totalPrice = get_coinbase_total_price(credentials, amount)
       +        questionText += _('\n(Price: ') + totalPrice + _(')')
       +
       +    if not question(box, questionText):
       +        return
       +
       +    if credentials:
       +        do_buy(credentials, amount)
       +    else:
       +        do_oauth_flow(web, amount)
       +    return web
       +
       +def do_buy(credentials, amount):
       +    conn = httplib.HTTPSConnection('coinbase.com')
       +    credentials.authorize(conn)
       +    params = {
       +        'qty': float(amount)/SATOSHIS_PER_BTC,
       +        'agree_btc_amount_varies': False
       +    }
       +    resp = conn.auth_request('POST', '/api/v1/buys', urlencode(params), None)
       +
       +    if resp.status != 200:
       +        message(_('Error, could not buy bitcoin'))
       +        return
       +    content = json.loads(resp.read())
       +    if content['success']:
       +        message(_('Success!\n') + content['transfer']['description'])
       +    else:
       +        if content['errors']:
       +            message(_('Error: ') + string.join(content['errors'], '\n'))
       +        else:
       +            message(_('Error, could not buy bitcoin'))
       +
       +def get_coinbase_total_price(credentials, amount):
       +    conn = httplib.HTTPSConnection('coinbase.com')
       +    params={'qty': amount/SATOSHIS_PER_BTC}
       +    conn.request('GET', '/api/v1/prices/buy?' + urlencode(params))
       +    resp = conn.getresponse()
       +    if resp.status != 200:
       +        return 'unavailable'
       +    content = json.loads(resp.read())
       +    return '$' + content['total']['amount']
       +
       +def do_oauth_flow(web, amount):
       +    # QT expects un-escaped URL
       +    auth_uri = step1_get_authorize_url()
       +    web.load(QUrl(auth_uri))
       +    web.setFixedSize(500, 700)
       +    web.show()
       +    web.titleChanged.connect(lambda(title): complete_oauth_flow(title, web, amount) if re.search('^[a-z0-9]+$', title) else False)
       +
       +def complete_oauth_flow(token, web, amount):
       +    web.close()
       +    credentials = step2_exchange(str(token))
       +    credentials.store_locally()
       +    do_buy(credentials, amount)
       +
       +def token_path():
       +    dir = user_dir() + '/coinbase_buyback'
       +    if not os.access(dir, os.F_OK):
       +        os.mkdir(dir)
       +    return dir + '/token'
       +
       +def read_local_oauth_credentials():
       +    if not os.access(token_path(), os.F_OK):
       +        return None
       +    f = open(token_path(), 'r')
       +    data = f.read()
       +    f.close()
       +    try:
       +        credentials = Credentials.from_json(data)
       +        return credentials
       +    except Exception as e:
       +        return None
       +
       +def rm_local_oauth_credentials():
       +    os.remove(token_path())
       +
       +def step1_get_authorize_url():
       +    return ('https://coinbase.com/oauth/authorize'
       +            + '?scope=' + SCOPE
       +            + '&redirect_uri=' + REDIRECT_URI
       +            + '&response_type=code'
       +            + '&client_id=' + CLIENT_ID
       +            + '&access_type=offline')
       +
       +def step2_exchange(code):
       +    body = urllib.urlencode({
       +        'grant_type': 'authorization_code',
       +        'client_id': CLIENT_ID,
       +        'client_secret': CLIENT_SECRET,
       +        'code': code,
       +        'redirect_uri': REDIRECT_URI,
       +        'scope': SCOPE,
       +        })
       +    headers = {
       +        'content-type': 'application/x-www-form-urlencoded',
       +    }
       +
       +    conn = httplib.HTTPSConnection('coinbase.com')
       +    conn.request('POST', TOKEN_URI, body, headers)
       +    resp = conn.getresponse()
       +    if resp.status == 200:
       +        d = json.loads(resp.read())
       +        access_token = d['access_token']
       +        refresh_token = d.get('refresh_token', None)
       +        token_expiry = None
       +        if 'expires_in' in d:
       +            token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
       +                seconds=int(d['expires_in']))
       +        return Credentials(access_token, refresh_token, token_expiry)
       +    else:
       +        raise OAuth2Exception(content)
       +
       +class OAuth2Exception(Exception):
       +    """An error related to OAuth2"""
       +
       +class Credentials(object):
       +    def __init__(self, access_token, refresh_token, token_expiry):
       +        self.access_token = access_token
       +        self.refresh_token = refresh_token
       +        self.token_expiry = token_expiry
       +        
       +        # Indicates a failed refresh
       +        self.invalid = False
       +
       +    def to_json(self):
       +        token_expiry = self.token_expiry
       +        if (token_expiry and isinstance(token_expiry, datetime.datetime)):
       +            token_expiry = token_expiry.strftime(EXPIRY_FORMAT)
       +        
       +        d = {
       +            'access_token': self.access_token,
       +            'refresh_token': self.refresh_token,
       +            'token_expiry': token_expiry,
       +        }
       +        return json.dumps(d)
       +
       +    def store_locally(self):
       +        f = open(token_path(), 'w')
       +        f.write(self.to_json())
       +        f.close()
       +
       +    @classmethod
       +    def from_json(cls, s):
       +        data = json.loads(s)
       +        if ('token_expiry' in data
       +            and not isinstance(data['token_expiry'], datetime.datetime)):
       +            try:
       +                data['token_expiry'] = datetime.datetime.strptime(
       +                    data['token_expiry'], EXPIRY_FORMAT)
       +            except:
       +                data['token_expiry'] = None
       +        retval = Credentials(
       +            data['access_token'],
       +            data['refresh_token'],
       +            data['token_expiry'])
       +        return retval
       +
       +    def apply(self, headers):
       +        headers['Authorization'] = 'Bearer ' + self.access_token
       +
       +    def authorize(self, conn):
       +        request_orig = conn.request
       +
       +        def new_request(method, uri, params, headers):
       +            if headers == None:
       +                headers = {}
       +                self.apply(headers)
       +            request_orig(method, uri, params, headers)
       +            resp = conn.getresponse()
       +            if resp.status == 401:
       +                # Refresh and try again
       +                self._refresh(request_orig)
       +                self.store_locally()
       +                self.apply(headers)
       +                request_orig(method, uri, params, headers)
       +                return conn.getresponse()
       +            else:
       +                return resp
       +        
       +        conn.auth_request = new_request
       +        return conn
       +
       +    def refresh(self):
       +        try:
       +            self._refresh()
       +        except OAuth2Exception as e:
       +            rm_local_oauth_credentials()
       +            self.invalid = True
       +            raise e
       +
       +    def _refresh(self):
       +        conn = httplib.HTTPSConnection('coinbase.com')
       +        body = urllib.urlencode({
       +            'grant_type': 'refresh_token',
       +            'refresh_token': self.refresh_token,
       +            'client_id': CLIENT_ID,
       +            'client_secret': CLIENT_SECRET,
       +        })
       +        headers = {
       +            'content-type': 'application/x-www-form-urlencoded',
       +        }
       +        conn.request('POST', TOKEN_URI, body, headers)
       +        resp = conn.getresponse()
       +        if resp.status == 200:
       +            d = json.loads(resp.read())
       +            self.token_response = d
       +            self.access_token = d['access_token']
       +            self.refresh_token = d.get('refresh_token', self.refresh_token)
       +            if 'expires_in' in d:
       +                self.token_expiry = datetime.timedelta(
       +                    seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
       +        else:
       +            raise OAuth2Exception('Refresh failed, ' + content)
       +
       +def message(msg):
       +    box = QMessageBox()
       +    box.setFixedSize(200, 200)
       +    return QMessageBox.information(box, _('Message'), msg)
       +
       +def question(widget, msg):
       +    return (QMessageBox.question(
       +        widget, _('Message'), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
       +            == QMessageBox.Yes)
       +
       +def main():
       +    app = QApplication(sys.argv)
       +    print sys.argv[1]
       +    propose_rebuy_qt(int(sys.argv[1]))
       +    sys.exit(app.exec_())
       +
       +if __name__ == "__main__":
       +    main()