#!/usr/bin/env python # # Electrum - lightweight Bitcoin client # Copyright (C) 2015 Thomas Voegtlin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import absolute_import import android import sys import os import imp import base64 script_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, os.path.join(script_dir, 'packages')) import qrcode imp.load_module('electrum', *imp.find_module('lib')) from electrum import SimpleConfig, Wallet, WalletStorage, format_satoshis from electrum import util from electrum.transaction import Transaction from electrum.bitcoin import base_encode, base_decode def modal_dialog(title, msg = None): droid.dialogCreateAlert(title,msg) droid.dialogSetPositiveButtonText('OK') droid.dialogShow() droid.dialogGetResponse() droid.dialogDismiss() def modal_input(title, msg, value = None, etype=None): droid.dialogCreateInput(title, msg, value, etype) droid.dialogSetPositiveButtonText('OK') droid.dialogSetNegativeButtonText('Cancel') droid.dialogShow() response = droid.dialogGetResponse() result = response.result droid.dialogDismiss() if result is None: return modal_input(title, msg, value, etype) if result.get('which') == 'positive': return result.get('value') def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'): droid.dialogCreateAlert(q, msg) droid.dialogSetPositiveButtonText(pos_text) droid.dialogSetNegativeButtonText(neg_text) droid.dialogShow() response = droid.dialogGetResponse() result = response.result droid.dialogDismiss() if result is None: return modal_question(q, msg, pos_text, neg_text) return result.get('which') == 'positive' def make_layout(s): content = """ %s """%s return """ %s """%content def qr_layout(title): title_view= """ """%title image_view=""" """ return make_layout(title_view + image_view) def add_menu(): droid.clearOptionsMenu() droid.addOptionsMenuItem("Seed", "seed", None,"") droid.addOptionsMenuItem("Public Key", "xpub", None,"") droid.addOptionsMenuItem("Transaction", "scan", None,"") droid.addOptionsMenuItem("Password", "password", None,"") def make_bitmap(data): # fixme: this is highly inefficient import qrcode from electrum import bmp qr = qrcode.QRCode() qr.add_data(data) bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp") droid = android.Android() wallet = None class Authenticator: def __init__(self): global wallet self.qr_data = None storage = WalletStorage('/sdcard/electrum/authenticator') if not storage.file_exists: action = self.restore_or_create() if not action: exit() password = droid.dialogGetPassword('Choose a password').result if password: password2 = droid.dialogGetPassword('Confirm password').result if password != password2: modal_dialog('Error', 'Passwords do not match') exit() else: password = None if action == 'create': wallet = Wallet(storage) seed = wallet.make_seed() modal_dialog('Your seed is:', seed) elif action == 'import': seed = self.seed_dialog() if not seed: exit() if not Wallet.is_seed(seed): exit() wallet = Wallet.from_seed(seed, storage) else: exit() wallet.add_seed(seed, password) wallet.create_master_keys(password) wallet.create_main_account(password) else: wallet = Wallet(storage) def restore_or_create(self): droid.dialogCreateAlert("Seed not found", "Do you want to create a new seed, or to import it?") droid.dialogSetPositiveButtonText('Create') droid.dialogSetNeutralButtonText('Import') droid.dialogSetNegativeButtonText('Cancel') droid.dialogShow() response = droid.dialogGetResponse().result droid.dialogDismiss() if not response: return if response.get('which') == 'negative': return return 'import' if response.get('which') == 'neutral' else 'create' def seed_dialog(self): if modal_question("Enter your seed", "Input method", 'QR Code', 'mnemonic'): code = droid.scanBarcode() r = code.result if r: seed = r['extras']['SCAN_RESULT'] else: return else: seed = modal_input('Mnemonic', 'Please enter your seed phrase') return str(seed) def show_qr(self, data): path = "/sdcard/sl4a/qrcode.bmp" if data: droid.dialogCreateSpinnerProgress("please wait") droid.dialogShow() try: make_bitmap(data) finally: droid.dialogDismiss() else: with open(path, 'w') as f: f.write('') droid.fullSetProperty("qrView", "src", 'file://'+path) self.qr_data = data def show_title(self, title): droid.fullSetProperty("addrTextView","text", title) def get_password(self): if wallet.use_encryption: password = droid.dialogGetPassword('Password').result try: wallet.check_password(password) except: return False return password def main(self): add_menu() welcome = 'Use the menu to scan a transaction.' droid.fullShow(qr_layout(welcome)) while True: event = droid.eventWait().result if not event: continue elif event["name"] == "key": if event["data"]["key"] == '4': if self.qr_data: self.show_qr(None) self.show_title(welcome) else: break elif event["name"] == "seed": password = self.get_password() if password is False: modal_dialog('Error','incorrect password') continue seed = wallet.get_mnemonic(password) modal_dialog('Your seed is', seed) elif event["name"] == "password": self.change_password_dialog() elif event["name"] == "xpub": mpk = wallet.get_master_public_key() self.show_qr(mpk) self.show_title('master public key') elif event["name"] == "scan": r = droid.scanBarcode() r = r.result if not r: continue data = r['extras']['SCAN_RESULT'] data = base_decode(data.encode('utf8'), None, base=43) data = ''.join(chr(ord(b)) for b in data).encode('hex') tx = Transaction(data) #except: # modal_dialog('Error', 'Cannot parse transaction') # continue if not wallet.can_sign(tx): modal_dialog('Error', 'Cannot sign this transaction') continue lines = map(lambda x: x[0] + u'\t\t' + format_satoshis(x[1]) if x[1] else x[0], tx.get_outputs()) if not modal_question('Sign?', '\n'.join(lines)): continue password = self.get_password() if password is False: modal_dialog('Error','incorrect password') continue droid.dialogCreateSpinnerProgress("Signing") droid.dialogShow() wallet.sign_transaction(tx, password) droid.dialogDismiss() data = base_encode(str(tx).decode('hex'), base=43) self.show_qr(data) self.show_title('Signed Transaction') droid.makeToast("Bye!") def change_password_dialog(self): if wallet.use_encryption: password = droid.dialogGetPassword('Your seed is encrypted').result if password is None: return else: password = None try: wallet.check_password(password) except Exception: modal_dialog('Error', 'Incorrect password') return new_password = droid.dialogGetPassword('Choose a password').result if new_password == None: return if new_password != '': password2 = droid.dialogGetPassword('Confirm new password').result if new_password != password2: modal_dialog('Error', 'passwords do not match') return wallet.update_password(password, new_password) if new_password: modal_dialog('Password updated', 'Your seed is encrypted') else: modal_dialog('No password', 'Your seed is not encrypted') if __name__ == "__main__": a = Authenticator() a.main()