#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
# 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
from electrum import SimpleConfig, Wallet, WalletStorage, format_satoshis
from electrum.bitcoin import is_address, COIN
from electrum import util
from decimal import Decimal
import datetime, re
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:
print "modal input: 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:
print "modal question: result is none"
return modal_question(q,msg, pos_text, neg_text)
return result.get('which') == 'positive'
def edit_label(addr):
v = modal_input('Edit label', None, wallet.labels.get(addr))
if v is not None:
wallet.set_label(addr, v)
droid.fullSetProperty("labelTextView", "text", v)
def select_from_contacts():
title = 'Contacts:'
droid.dialogCreateAlert(title)
l = contacts.keys()
droid.dialogSetItems(l)
droid.dialogSetPositiveButtonText('New contact')
droid.dialogShow()
response = droid.dialogGetResponse().result
droid.dialogDismiss()
if response.get('which') == 'positive':
return 'newcontact'
result = response.get('item')
if result is not None:
t, v = contacts.get(result)
return v
def protocol_name(p):
if p == 't': return 'TCP'
if p == 's': return 'SSL'
def protocol_dialog(host, protocol, z):
droid.dialogCreateAlert('Protocol', host)
protocols = filter(lambda x: x in "ts", z.keys())
l = []
current = protocols.index(protocol)
for p in protocols:
l.append(protocol_name(p))
droid.dialogSetSingleChoiceItems(l, current)
droid.dialogSetPositiveButtonText('OK')
droid.dialogSetNegativeButtonText('Cancel')
droid.dialogShow()
response = droid.dialogGetResponse().result
selected_item = droid.dialogGetSelectedItems().result
droid.dialogDismiss()
if not response:
return
if not selected_item:
return
if response.get('which') == 'positive':
return protocols[selected_item[0]]
def make_layout(s, scrollable = False):
content = """
%s """%s
if scrollable:
content = """
%s
"""%content
return """
%s
"""%content
def main_layout():
h = get_history_layout(15)
l = make_layout("""
%s """%h,True)
return l
def qr_layout(addr, amount, message):
addr_view= """
"""%addr
if amount:
amount_view = """
"""%format_satoshis(amount)
else:
amount_view = ""
if message:
message_view = """
"""%message
else:
message_view = ""
return make_layout("""
%s
%s
%s
"""%(addr_view, amount_view, message_view), True)
payto_layout = make_layout("""
""",False)
settings_layout = make_layout(""" """)
def get_history_values(n):
values = []
h = wallet.get_history()
length = min(n, len(h))
for i in range(length):
tx_hash, conf, value, timestamp, balance = h[-i-1]
try:
dt = datetime.datetime.fromtimestamp( timestamp )
if dt.date() == dt.today().date():
time_str = str( dt.time() )
else:
time_str = str( dt.date() )
except Exception:
time_str = 'pending'
conf_str = 'v' if conf else 'o'
label = wallet.get_label(tx_hash)
label = label.replace('<','').replace('>','')
values.append((conf_str, ' ' + time_str, ' ' + format_satoshis(value, True), ' ' + label))
return values
def get_history_layout(n):
rows = ""
i = 0
values = get_history_values(n)
for v in values:
a,b,c,d = v
color = "#ff00ff00" if a == 'v' else "#ffff0000"
rows += """
"""%(i,a,color,i,b,i,c,i,d)
i += 1
output = """
%s
"""% rows
return output
def set_history_layout(n):
values = get_history_values(n)
i = 0
for v in values:
a,b,c,d = v
droid.fullSetProperty("hl_%d_col1"%i,"text", a)
if a == 'v':
droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ff00ff00")
else:
droid.fullSetProperty("hl_%d_col1"%i, "textColor","#ffff0000")
droid.fullSetProperty("hl_%d_col2"%i,"text", b)
droid.fullSetProperty("hl_%d_col3"%i,"text", c)
droid.fullSetProperty("hl_%d_col4"%i,"text", d)
i += 1
status_text = ''
def update_layout():
global status_text
if not network.is_connected():
text = "Not connected..."
elif not wallet.up_to_date:
text = "Synchronizing..."
else:
c, u, x = wallet.get_balance()
text = "Balance:"+format_satoshis(c)
if u:
text += ' [' + format_satoshis(u,True).strip() + ']'
if x:
text += ' [' + format_satoshis(x,True).strip() + ']'
# vibrate if status changed
if text != status_text:
if status_text and network.is_connected() and wallet.up_to_date:
droid.vibrate()
status_text = text
droid.fullSetProperty("balanceTextView", "text", status_text)
if wallet.up_to_date:
set_history_layout(15)
def pay_to(recipient, amount, label):
if wallet.use_encryption:
password = droid.dialogGetPassword('Password').result
if not password: return
else:
password = None
droid.dialogCreateSpinnerProgress("Electrum", "signing transaction...")
droid.dialogShow()
try:
tx = wallet.mktx([('address', recipient, amount)], password, config)
except Exception as e:
modal_dialog('error', e.message)
droid.dialogDismiss()
return
if label:
wallet.set_label(tx.hash(), label)
droid.dialogDismiss()
r, h = wallet.sendtx( tx )
if r:
modal_dialog('Payment sent', h)
return True
else:
modal_dialog('Error', h)
def make_new_contact():
code = droid.scanBarcode()
r = code.result
if r:
data = str(r['extras']['SCAN_RESULT']).strip()
if data:
if re.match('^bitcoin:', data):
out = util.parse_URI(data)
address = out.get('address')
elif is_address(data):
address = data
else:
address = None
if address:
if modal_question('Add to contacts?', address):
# fixme: ask for key
contacts[address] = ('address', address)
else:
modal_dialog('Invalid address', data)
do_refresh = False
def update_callback(event):
global do_refresh
print "gui callback", network.is_connected()
do_refresh = True
droid.eventPost("refresh",'z')
def main_loop():
global do_refresh
update_layout()
out = None
quitting = False
while out is None:
event = droid.eventWait(1000).result
if event is None:
if do_refresh:
update_layout()
do_refresh = False
continue
print "got event in main loop", repr(event)
if event == 'OK': continue
if event is None: continue
if not event.get("name"): continue
# request 2 taps before we exit
if event["name"]=="key":
if event["data"]["key"] == '4':
if quitting:
out = 'quit'
else:
quitting = True
else: quitting = False
if event["name"]=="click":
id=event["data"]["id"]
elif event["name"]=="settings":
out = 'settings'
elif event["name"] in menu_commands:
out = event["name"]
if out == 'contacts':
global contact_addr
contact_addr = select_from_contacts()
if contact_addr == 'newcontact':
make_new_contact()
contact_addr = None
if not contact_addr:
out = None
elif out == "receive":
global receive_addr
domain = wallet.addresses(include_change = False)
for addr in domain:
if not wallet.history.get(addr):
receive_addr = addr
break
else:
out = None
return out
def payto_loop():
global recipient
if recipient:
droid.fullSetProperty("recipient","text",recipient)
recipient = None
out = None
while out is None:
event = droid.eventWait().result
if not event: continue
print "got event in payto loop", event
if event == 'OK': continue
if not event.get("name"): continue
if event["name"] == "click":
id = event["data"]["id"]
if id=="buttonPay":
droid.fullQuery()
recipient = droid.fullQueryDetail("recipient").result.get('text')
message = droid.fullQueryDetail("message").result.get('text')
amount = droid.fullQueryDetail('amount').result.get('text')
if not is_address(recipient):
modal_dialog('Error','Invalid Bitcoin address')
continue
try:
amount = int(COIN * Decimal(amount))
except Exception:
modal_dialog('Error','Invalid amount')
continue
result = pay_to(recipient, amount, message)
if result:
out = 'main'
elif id=="buttonContacts":
addr = select_from_contacts()
droid.fullSetProperty("recipient", "text", addr)
elif id=="buttonQR":
code = droid.scanBarcode()
r = code.result
if r:
data = str(r['extras']['SCAN_RESULT']).strip()
if data:
print "data", data
if re.match('^bitcoin:', data):
rr = util.parse_URI(data)
amount = rr.get('amount')
address = rr.get('address')
message = rr.get('message', '')
if amount:
amount = str(Decimal(amount)/COIN)
droid.fullSetProperty("recipient", "text", address)
droid.fullSetProperty("amount", "text", amount)
droid.fullSetProperty("message", "text", message)
elif is_address(data):
droid.fullSetProperty("recipient", "text", data)
else:
modal_dialog('Error','cannot parse QR code\n'+data)
elif event["name"] in menu_commands:
out = event["name"]
elif event["name"]=="key":
if event["data"]["key"] == '4':
out = 'main'
#elif event["name"]=="screen":
# if event["data"]=="destroy":
# out = 'main'
return out
receive_addr = ''
receive_amount = None
receive_message = None
contact_addr = ''
recipient = ''
def receive_loop():
global receive_addr, receive_amount, receive_message
print "receive loop"
receive_URI = util.create_URI(receive_addr, receive_amount, receive_message)
make_bitmap(receive_URI)
droid.fullShow(qr_layout(receive_addr, receive_amount, receive_message))
out = None
while out is None:
event = droid.eventWait().result
if not event:
continue
elif event["name"]=="key":
if event["data"]["key"] == '4':
out = 'main'
elif event["name"]=="clipboard":
droid.setClipboard(receive_URI)
modal_dialog('URI copied to clipboard', receive_URI)
elif event["name"]=="amount":
amount = modal_input('Amount', 'Amount you want to receive (in BTC). ', format_satoshis(receive_amount) if receive_amount else None, "numberDecimal")
if amount is not None:
receive_amount = int(COIN * Decimal(amount)) if amount else None
out = 'receive'
elif event["name"]=="message":
message = modal_input('Message', 'Message in your request', receive_message)
if message is not None:
receive_message = unicode(message)
out = 'receive'
return out
def contacts_loop():
global recipient
out = None
while out is None:
event = droid.eventWait().result
print "got event", event
if event["name"]=="key":
if event["data"]["key"] == '4':
out = 'main'
elif event["name"]=="clipboard":
droid.setClipboard(contact_addr)
modal_dialog('Address copied to clipboard',contact_addr)
elif event["name"]=="edit":
edit_label(contact_addr)
elif event["name"]=="paytocontact":
recipient = contact_addr
out = 'send'
elif event["name"]=="deletecontact":
if modal_question('delete contact', contact_addr):
out = 'main'
return out
def server_dialog(servers):
droid.dialogCreateAlert("Public servers")
droid.dialogSetItems( servers.keys() )
droid.dialogSetPositiveButtonText('Private server')
droid.dialogShow()
response = droid.dialogGetResponse().result
droid.dialogDismiss()
if not response: return
if response.get('which') == 'positive':
return modal_input('Private server', None)
i = response.get('item')
if i is not None:
response = servers.keys()[i]
return response
def show_seed():
if wallet.use_encryption:
password = droid.dialogGetPassword('Seed').result
if not password: return
else:
password = None
try:
seed = wallet.get_mnemonic(password)
except Exception:
modal_dialog('error','incorrect password')
return
modal_dialog('Your seed is', seed)
def change_password_dialog():
if wallet.use_encryption:
password = droid.dialogGetPassword('Your wallet 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 wallet is encrypted')
else:
modal_dialog('No password','your wallet is not encrypted')
return True
def settings_loop():
def set_listview():
host, port, p, proxy_config, auto_connect = network.get_parameters()
fee = str(Decimal(wallet.fee_per_kb(config)) / COIN)
is_encrypted = 'yes' if wallet.use_encryption else 'no'
protocol = protocol_name(p)
droid.fullShow(settings_layout)
droid.fullSetList("myListView",['Server: ' + host, 'Protocol: '+ protocol, 'Port: '+port, 'Transaction fee/kb: '+fee, 'Password: '+is_encrypted, 'Seed'])
set_listview()
out = None
while out is None:
event = droid.eventWait()
event = event.result
print "got event", event
if event == 'OK': continue
if not event: continue
servers = network.get_servers()
name = event.get("name")
if not name: continue
if name == "itemclick":
pos = event["data"]["position"]
host, port, protocol, proxy_config, auto_connect = network.get_parameters()
network_changed = False
if pos == "0": #server
host = server_dialog(servers)
if host:
p = servers[host]
port = p[protocol]
network_changed = True
elif pos == "1": #protocol
if host in servers:
protocol = protocol_dialog(host, protocol, servers[host])
if protocol:
z = servers[host]
port = z[protocol]
network_changed = True
elif pos == "2": #port
a_port = modal_input('Port number', 'If you use a public server, this field is set automatically when you set the protocol', port, "number")
if a_port != port:
port = a_port
network_changed = True
elif pos == "3": #fee
fee = modal_input(
'Transaction fee',
'The fee will be this amount multiplied by the number of inputs in your transaction. ',
str(Decimal(wallet.fee_per_kb(config)) / COIN), "numberDecimal")
if fee:
try:
fee = int(COIN * Decimal(fee))
except Exception:
modal_dialog('error','invalid fee value')
config.set_key('fee_per_kb', fee)
set_listview()
elif pos == "4":
if change_password_dialog():
set_listview()
elif pos == "5":
show_seed()
if network_changed:
proxy = None
auto_connect = False
try:
network.set_parameters(host, port, protocol, proxy, auto_connect)
except Exception:
modal_dialog('error','invalid server')
set_listview()
elif name in menu_commands:
out = event["name"]
elif name == 'cancel':
out = 'main'
elif name == "key":
if event["data"]["key"] == '4':
out = 'main'
return out
def add_menu(s):
droid.clearOptionsMenu()
if s == 'main':
droid.addOptionsMenuItem("Send","send",None,"")
droid.addOptionsMenuItem("Receive","receive",None,"")
droid.addOptionsMenuItem("Contacts","contacts",None,"")
droid.addOptionsMenuItem("Settings","settings",None,"")
elif s == 'receive':
droid.addOptionsMenuItem("Copy","clipboard",None,"")
droid.addOptionsMenuItem("Amount","amount",None,"")
droid.addOptionsMenuItem("Message","message",None,"")
elif s == 'contacts':
droid.addOptionsMenuItem("Copy","clipboard",None,"")
droid.addOptionsMenuItem("Label","edit",None,"")
droid.addOptionsMenuItem("Pay to","paytocontact",None,"")
#droid.addOptionsMenuItem("Delete","deletecontact",None,"")
def make_bitmap(data):
# fixme: this is highly inefficient
droid.dialogCreateSpinnerProgress("please wait")
droid.dialogShow()
try:
import qrcode
from electrum import bmp
qr = qrcode.QRCode()
qr.add_data(data)
bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp")
finally:
droid.dialogDismiss()
droid = android.Android()
menu_commands = ["send", "receive", "settings", "contacts", "main"]
wallet = None
network = None
contacts = None
config = None
class ElectrumGui:
def __init__(self, _config, _network, daemon, plugins):
global wallet, network, contacts, config
network = _network
config = _config
network.register_callback(update_callback, ['updated'])
contacts = util.StoreDict(config, 'contacts')
storage = WalletStorage(config.get_wallet_path())
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:
# set to None if it's an empty string
password = None
if action == 'create':
wallet = Wallet(storage)
seed = wallet.make_seed()
modal_dialog('Your seed is:', seed)
wallet.add_seed(seed, password)
wallet.create_master_keys(password)
wallet.create_main_account(password)
elif action == 'restore':
seed = self.seed_dialog()
if not seed:
exit()
if not Wallet.is_seed(seed):
exit()
wallet = Wallet.from_seed(seed, password, storage)
else:
exit()
msg = "Creating wallet" if action == 'create' else "Restoring wallet"
droid.dialogCreateSpinnerProgress("Electrum", msg)
droid.dialogShow()
wallet.start_threads(network)
if action == 'restore':
wallet.wait_until_synchronized()
else:
wallet.synchronize()
droid.dialogDismiss()
droid.vibrate()
else:
wallet = Wallet(storage)
wallet.start_threads(network)
def main(self):
s = 'main'
while True:
add_menu(s)
if s == 'main':
droid.fullShow(main_layout())
s = main_loop()
elif s == 'send':
droid.fullShow(payto_layout)
s = payto_loop()
elif s == 'receive':
s = receive_loop()
elif s == 'contacts':
make_bitmap(contact_addr)
droid.fullShow(qr_layout(contact_addr, None, None))
s = contacts_loop()
elif s == 'settings':
s = settings_loop()
else:
break
droid.makeToast("Bye!")
def restore_or_create(self):
droid.dialogCreateAlert("Wallet not found","Do you want to create a new wallet, or restore an existing one?")
droid.dialogSetPositiveButtonText('Create')
droid.dialogSetNeutralButtonText('Restore')
droid.dialogSetNegativeButtonText('Cancel')
droid.dialogShow()
response = droid.dialogGetResponse().result
droid.dialogDismiss()
if not response: return
if response.get('which') == 'negative':
return
return 'restore' 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 code')
return str(seed)