You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
357 lines
11 KiB
357 lines
11 KiB
#!/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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
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 = """
|
|
|
|
<LinearLayout
|
|
android:id="@+id/zz"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="wrap_content"
|
|
android:background="#ff222222">
|
|
|
|
<TextView
|
|
android:id="@+id/textElectrum"
|
|
android:text="Electrum Authenticator"
|
|
android:textSize="7pt"
|
|
android:textColor="#ff4444ff"
|
|
android:gravity="left"
|
|
android:layout_height="wrap_content"
|
|
android:layout_width="match_parent"
|
|
/>
|
|
</LinearLayout>
|
|
|
|
%s """%s
|
|
|
|
|
|
return """<?xml version="1.0" encoding="utf-8"?>
|
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
android:id="@+id/background"
|
|
android:orientation="vertical"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="match_parent"
|
|
android:background="#ff000022">
|
|
|
|
%s
|
|
</LinearLayout>"""%content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def qr_layout(title):
|
|
title_view= """
|
|
<TextView android:id="@+id/addrTextView"
|
|
android:layout_width="match_parent"
|
|
android:layout_height="50"
|
|
android:text="%s"
|
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
|
android:gravity="center_vertical|center_horizontal|center">
|
|
</TextView>"""%title
|
|
|
|
image_view="""
|
|
<ImageView
|
|
android:id="@+id/qrView"
|
|
android:gravity="center"
|
|
android:layout_width="match_parent"
|
|
android:antialias="false"
|
|
android:src=""
|
|
/>
|
|
"""
|
|
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, password, 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()
|
|
|